diff mercurial/win32.py @ 13375:f1fa8f481c7c

port win32.py to using the Python ctypes library The pywin32 package is no longer needed. ctypes is now required for running Mercurial on Windows. ctypes is included in Python since version 2.5. For Python 2.4, ctypes is available as an extra installer package for Windows. Moved spawndetached() from windows.py to win32.py and fixed it, using ctypes as well. spawndetached was defunct with Python 2.6.6 because Python removed their undocumented subprocess.CreateProcess. This fixes 'hg serve -d' on Windows.
author Adrian Buehlmann <adrian@cadifra.com>
date Mon, 14 Feb 2011 11:12:26 +0100
parents 1c613c1ae43d
children 60b5c6c3fd12
line wrap: on
line diff
--- a/mercurial/win32.py	Mon Feb 14 11:12:22 2011 +0100
+++ b/mercurial/win32.py	Mon Feb 14 11:12:26 2011 +0100
@@ -5,74 +5,173 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-"""Utility functions that use win32 API.
+import osutil, encoding
+import ctypes, errno, os, struct, subprocess
+
+_kernel32 = ctypes.windll.kernel32
+
+_BOOL = ctypes.c_long
+_WORD = ctypes.c_ushort
+_DWORD = ctypes.c_ulong
+_LPCSTR = _LPSTR = ctypes.c_char_p
+_HANDLE = ctypes.c_void_p
+_HWND = _HANDLE
+
+_INVALID_HANDLE_VALUE = -1
+
+# GetLastError
+_ERROR_SUCCESS = 0
+_ERROR_INVALID_PARAMETER = 87
+_ERROR_INSUFFICIENT_BUFFER = 122
+
+# WPARAM is defined as UINT_PTR (unsigned type)
+# LPARAM is defined as LONG_PTR (signed type)
+if ctypes.sizeof(ctypes.c_long) == ctypes.sizeof(ctypes.c_void_p):
+    _WPARAM = ctypes.c_ulong
+    _LPARAM = ctypes.c_long
+elif ctypes.sizeof(ctypes.c_longlong) == ctypes.sizeof(ctypes.c_void_p):
+    _WPARAM = ctypes.c_ulonglong
+    _LPARAM = ctypes.c_longlong
+
+class _FILETIME(ctypes.Structure):
+    _fields_ = [('dwLowDateTime', _DWORD),
+                ('dwHighDateTime', _DWORD)]
+
+class _BY_HANDLE_FILE_INFORMATION(ctypes.Structure):
+    _fields_ = [('dwFileAttributes', _DWORD),
+                ('ftCreationTime', _FILETIME),
+                ('ftLastAccessTime', _FILETIME),
+                ('ftLastWriteTime', _FILETIME),
+                ('dwVolumeSerialNumber', _DWORD),
+                ('nFileSizeHigh', _DWORD),
+                ('nFileSizeLow', _DWORD),
+                ('nNumberOfLinks', _DWORD),
+                ('nFileIndexHigh', _DWORD),
+                ('nFileIndexLow', _DWORD)]
+
+# CreateFile 
+_FILE_SHARE_READ = 0x00000001
+_FILE_SHARE_WRITE = 0x00000002
+_FILE_SHARE_DELETE = 0x00000004
+
+_OPEN_EXISTING = 3
+
+# Process Security and Access Rights
+_PROCESS_QUERY_INFORMATION = 0x0400
+
+# GetExitCodeProcess
+_STILL_ACTIVE = 259
+
+# registry
+_HKEY_CURRENT_USER = 0x80000001L
+_HKEY_LOCAL_MACHINE = 0x80000002L
+_KEY_READ = 0x20019
+_REG_SZ = 1
+_REG_DWORD = 4
 
-Mark Hammond's win32all package allows better functionality on
-Windows. This module overrides definitions in util.py. If not
-available, import of this module will fail, and generic code will be
-used.
-"""
+class _STARTUPINFO(ctypes.Structure):
+    _fields_ = [('cb', _DWORD),
+                ('lpReserved', _LPSTR),
+                ('lpDesktop', _LPSTR),
+                ('lpTitle', _LPSTR),
+                ('dwX', _DWORD),
+                ('dwY', _DWORD),
+                ('dwXSize', _DWORD),
+                ('dwYSize', _DWORD),
+                ('dwXCountChars', _DWORD),
+                ('dwYCountChars', _DWORD),
+                ('dwFillAttribute', _DWORD),
+                ('dwFlags', _DWORD),
+                ('wShowWindow', _WORD),
+                ('cbReserved2', _WORD),
+                ('lpReserved2', ctypes.c_char_p),
+                ('hStdInput', _HANDLE),
+                ('hStdOutput', _HANDLE),
+                ('hStdError', _HANDLE)]
+
+class _PROCESS_INFORMATION(ctypes.Structure):
+    _fields_ = [('hProcess', _HANDLE),
+                ('hThread', _HANDLE),
+                ('dwProcessId', _DWORD),
+                ('dwThreadId', _DWORD)]
+
+_DETACHED_PROCESS = 0x00000008
+_STARTF_USESHOWWINDOW = 0x00000001
+_SW_HIDE = 0
 
-import win32api
+class _COORD(ctypes.Structure):
+    _fields_ = [('X', ctypes.c_short),
+                ('Y', ctypes.c_short)]
+
+class _SMALL_RECT(ctypes.Structure):
+    _fields_ = [('Left', ctypes.c_short),
+                ('Top', ctypes.c_short),
+                ('Right', ctypes.c_short),
+                ('Bottom', ctypes.c_short)]
+
+class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
+    _fields_ = [('dwSize', _COORD),
+                ('dwCursorPosition', _COORD),
+                ('wAttributes', _WORD),
+                ('srWindow', _SMALL_RECT),
+                ('dwMaximumWindowSize', _COORD)]
 
-import errno, os, sys, pywintypes, win32con, win32file, win32process
-import winerror, win32gui, win32console
-import osutil, encoding
-from win32com.shell import shell, shellcon
+_STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
+
+def _raiseoserror(name):
+    err = ctypes.WinError()
+    raise OSError(err.errno, '%s: %s' % (name, err.strerror))
+
+def _getfileinfo(name):
+    fh = _kernel32.CreateFileA(name, 0,
+            _FILE_SHARE_READ | _FILE_SHARE_WRITE | _FILE_SHARE_DELETE,
+            None, _OPEN_EXISTING, 0, None)
+    if fh == _INVALID_HANDLE_VALUE:
+        _raiseoserror(name)
+    try:
+        fi = _BY_HANDLE_FILE_INFORMATION()
+        if not _kernel32.GetFileInformationByHandle(fh, ctypes.byref(fi)):
+            _raiseoserror(name)
+        return fi
+    finally:
+        _kernel32.CloseHandle(fh)
 
 def os_link(src, dst):
-    try:
-        win32file.CreateHardLink(dst, src)
-    except pywintypes.error:
-        raise OSError(errno.EINVAL, 'target implements hardlinks improperly')
-    except NotImplementedError: # Another fake error win Win98
-        raise OSError(errno.EINVAL, 'Hardlinking not supported')
+    if not _kernel32.CreateHardLinkA(dst, src, None):
+        _raiseoserror(src)
 
-def _getfileinfo(pathname):
-    """Return number of hardlinks for the given file."""
-    try:
-        fh = win32file.CreateFile(pathname, 0,
-            win32file.FILE_SHARE_READ | win32file.FILE_SHARE_WRITE |
-            win32file.FILE_SHARE_DELETE,
-            None, win32file.OPEN_EXISTING, 0, None)
-    except pywintypes.error:
-        raise OSError(errno.ENOENT, 'The system cannot find the file specified')
-    try:
-        return win32file.GetFileInformationByHandle(fh)
-    finally:
-        fh.Close()
-
-def nlinks(pathname):
-    """Return number of hardlinks for the given file."""
-    return _getfileinfo(pathname)[7]
+def nlinks(name):
+    '''return number of hardlinks for the given file'''
+    return _getfileinfo(name).nNumberOfLinks
 
 def samefile(fpath1, fpath2):
-    """Returns whether fpath1 and fpath2 refer to the same file. This is only
-    guaranteed to work for files, not directories."""
+    '''Returns whether fpath1 and fpath2 refer to the same file. This is only
+    guaranteed to work for files, not directories.'''
     res1 = _getfileinfo(fpath1)
     res2 = _getfileinfo(fpath2)
-    # Index 4 is the volume serial number, and 8 and 9 contain the file ID
-    return res1[4] == res2[4] and res1[8] == res2[8] and res1[9] == res2[9]
+    return (res1.dwVolumeSerialNumber == res2.dwVolumeSerialNumber
+        and res1.nFileIndexHigh == res2.nFileIndexHigh
+        and res1.nFileIndexLow == res2.nFileIndexLow)
 
 def samedevice(fpath1, fpath2):
-    """Returns whether fpath1 and fpath2 are on the same device. This is only
-    guaranteed to work for files, not directories."""
+    '''Returns whether fpath1 and fpath2 are on the same device. This is only
+    guaranteed to work for files, not directories.'''
     res1 = _getfileinfo(fpath1)
     res2 = _getfileinfo(fpath2)
-    return res1[4] == res2[4]
+    return res1.dwVolumeSerialNumber == res2.dwVolumeSerialNumber
 
 def testpid(pid):
     '''return True if pid is still running or unable to
     determine, False otherwise'''
-    try:
-        handle = win32api.OpenProcess(
-            win32con.PROCESS_QUERY_INFORMATION, False, pid)
-        if handle:
-            status = win32process.GetExitCodeProcess(handle)
-            return status == win32con.STILL_ACTIVE
-    except pywintypes.error, details:
-        return details[0] != winerror.ERROR_INVALID_PARAMETER
-    return True
+    h = _kernel32.OpenProcess(_PROCESS_QUERY_INFORMATION, False, pid)
+    if h:
+        try:
+            status = _DWORD()
+            if _kernel32.GetExitCodeProcess(h, ctypes.byref(status)):
+                return status.value == _STILL_ACTIVE
+        finally:
+            _kernel32.CloseHandle(h)
+    return _kernel32.GetLastError() != _ERROR_INVALID_PARAMETER
 
 def lookup_reg(key, valname=None, scope=None):
     ''' Look up a key/value name in the Windows registry.
@@ -83,101 +182,169 @@
     a sequence of scopes to look up in order. Default (CURRENT_USER,
     LOCAL_MACHINE).
     '''
-    try:
-        from _winreg import HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, \
-            QueryValueEx, OpenKey
-    except ImportError:
-        return None
-
+    adv = ctypes.windll.advapi32
+    byref = ctypes.byref
     if scope is None:
-        scope = (HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE)
+        scope = (_HKEY_CURRENT_USER, _HKEY_LOCAL_MACHINE)
     elif not isinstance(scope, (list, tuple)):
         scope = (scope,)
     for s in scope:
+        kh = _HANDLE()
+        res = adv.RegOpenKeyExA(s, key, 0, _KEY_READ, ctypes.byref(kh))
+        if res != _ERROR_SUCCESS:
+            continue
         try:
-            val = QueryValueEx(OpenKey(s, key), valname)[0]
-            # never let a Unicode string escape into the wild
-            return encoding.tolocal(val.encode('UTF-8'))
-        except EnvironmentError:
-            pass
+            size = _DWORD(600)
+            type = _DWORD()
+            buf = ctypes.create_string_buffer(size.value + 1)
+            res = adv.RegQueryValueExA(kh.value, valname, None,
+                                       byref(type), buf, byref(size))
+            if res != _ERROR_SUCCESS:
+                continue
+            if type.value == _REG_SZ:
+                # never let a Unicode string escape into the wild
+                return encoding.tolocal(buf.value.encode('UTF-8'))
+            elif type.value == _REG_DWORD:
+                fmt = '<L'
+                s = ctypes.string_at(byref(buf), struct.calcsize(fmt))
+                return struct.unpack(fmt, s)[0]
+        finally:
+            adv.RegCloseKey(kh.value)
 
 def system_rcpath_win32():
     '''return default os-specific hgrc search path'''
-    filename = win32api.GetModuleFileName(0)
+    rcpath = []
+    size = 600
+    buf = ctypes.create_string_buffer(size + 1)
+    len = _kernel32.GetModuleFileNameA(None, ctypes.byref(buf), size)
+    if len == 0:
+        raise ctypes.WinError()
+    elif len == size:
+        raise ctypes.WinError(_ERROR_INSUFFICIENT_BUFFER)
+    filename = buf.value
     # Use mercurial.ini found in directory with hg.exe
     progrc = os.path.join(os.path.dirname(filename), 'mercurial.ini')
     if os.path.isfile(progrc):
-        return [progrc]
+        rcpath.append(progrc)
+        return rcpath
     # Use hgrc.d found in directory with hg.exe
     progrcd = os.path.join(os.path.dirname(filename), 'hgrc.d')
     if os.path.isdir(progrcd):
-        rcpath = []
         for f, kind in osutil.listdir(progrcd):
             if f.endswith('.rc'):
                 rcpath.append(os.path.join(progrcd, f))
         return rcpath
     # else look for a system rcpath in the registry
-    try:
-        value = win32api.RegQueryValue(
-                win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Mercurial')
-        rcpath = []
-        for p in value.split(os.pathsep):
-            if p.lower().endswith('mercurial.ini'):
-                rcpath.append(p)
-            elif os.path.isdir(p):
-                for f, kind in osutil.listdir(p):
-                    if f.endswith('.rc'):
-                        rcpath.append(os.path.join(p, f))
+    value = lookup_reg('SOFTWARE\\Mercurial', None, _HKEY_LOCAL_MACHINE)
+    if not isinstance(value, str) or not value:
         return rcpath
-    except pywintypes.error:
-        return []
+    value = value.replace('/', os.sep)
+    for p in value.split(os.pathsep):
+        if p.lower().endswith('mercurial.ini'):
+            rcpath.append(p)
+        elif os.path.isdir(p):
+            for f, kind in osutil.listdir(p):
+                if f.endswith('.rc'):
+                    rcpath.append(os.path.join(p, f))
+    return rcpath
 
 def user_rcpath_win32():
     '''return os-specific hgrc search path to the user dir'''
     userdir = os.path.expanduser('~')
-    if sys.getwindowsversion()[3] != 2 and userdir == '~':
-        # We are on win < nt: fetch the APPDATA directory location and use
-        # the parent directory as the user home dir.
-        appdir = shell.SHGetPathFromIDList(
-            shell.SHGetSpecialFolderLocation(0, shellcon.CSIDL_APPDATA))
-        userdir = os.path.dirname(appdir)
     return [os.path.join(userdir, 'mercurial.ini'),
             os.path.join(userdir, '.hgrc')]
 
 def getuser():
     '''return name of current user'''
-    return win32api.GetUserName()
+    adv = ctypes.windll.advapi32
+    size = _DWORD(300)
+    buf = ctypes.create_string_buffer(size.value + 1)
+    if not adv.GetUserNameA(ctypes.byref(buf), ctypes.byref(size)):
+        raise ctypes.WinError()
+    return buf.value
 
-def set_signal_handler_win32():
-    """Register a termination handler for console events including
+_SIGNAL_HANDLER = ctypes.WINFUNCTYPE(_BOOL, _DWORD)
+_signal_handler = []
+
+def set_signal_handler():
+    '''Register a termination handler for console events including
     CTRL+C. python signal handlers do not work well with socket
     operations.
-    """
+    '''
     def handler(event):
-        win32process.ExitProcess(1)
-    win32api.SetConsoleCtrlHandler(handler)
+        _kernel32.ExitProcess(1)
+
+    if _signal_handler:
+        return # already registered
+    h = _SIGNAL_HANDLER(handler)
+    _signal_handler.append(h) # needed to prevent garbage collection
+    if not _kernel32.SetConsoleCtrlHandler(h, True):
+        raise ctypes.WinError()
+
+_WNDENUMPROC = ctypes.WINFUNCTYPE(_BOOL, _HWND, _LPARAM)
 
 def hidewindow():
-    def callback(*args, **kwargs):
-        hwnd, pid = args
-        wpid = win32process.GetWindowThreadProcessId(hwnd)[1]
-        if pid == wpid:
-            win32gui.ShowWindow(hwnd, win32con.SW_HIDE)
+    user32 = ctypes.windll.user32
 
-    pid =  win32process.GetCurrentProcessId()
-    win32gui.EnumWindows(callback, pid)
+    def callback(hwnd, pid):
+        wpid = _DWORD()
+        user32.GetWindowThreadProcessId(hwnd, ctypes.byref(wpid))
+        if pid == wpid.value:
+            user32.ShowWindow(hwnd, _SW_HIDE)
+            return False # stop enumerating windows
+        return True
+
+    pid = _kernel32.GetCurrentProcessId()
+    user32.EnumWindows(_WNDENUMPROC(callback), pid)
 
 def termwidth():
-    try:
-        # Query stderr to avoid problems with redirections
-        screenbuf = win32console.GetStdHandle(win32console.STD_ERROR_HANDLE)
-        if screenbuf is None:
-            return 79
-        try:
-            window = screenbuf.GetConsoleScreenBufferInfo()['Window']
-            width = window.Right - window.Left
-            return width
-        finally:
-            screenbuf.Detach()
-    except pywintypes.error:
-        return 79
+    # cmd.exe does not handle CR like a unix console, the CR is
+    # counted in the line length. On 80 columns consoles, if 80
+    # characters are written, the following CR won't apply on the
+    # current line but on the new one. Keep room for it.
+    width = 79
+    # Query stderr to avoid problems with redirections
+    screenbuf = _kernel32.GetStdHandle(
+                  _STD_ERROR_HANDLE) # don't close the handle returned
+    if screenbuf is None or screenbuf == _INVALID_HANDLE_VALUE:
+        return width
+    csbi = _CONSOLE_SCREEN_BUFFER_INFO()
+    if not _kernel32.GetConsoleScreenBufferInfo(
+                        screenbuf, ctypes.byref(csbi)):
+        return width
+    width = csbi.srWindow.Right - csbi.srWindow.Left
+    return width
+
+def spawndetached(args):
+    # No standard library function really spawns a fully detached
+    # process under win32 because they allocate pipes or other objects
+    # to handle standard streams communications. Passing these objects
+    # to the child process requires handle inheritance to be enabled
+    # which makes really detached processes impossible.
+    si = _STARTUPINFO()
+    si.cb = ctypes.sizeof(_STARTUPINFO)
+    si.dwFlags = _STARTF_USESHOWWINDOW
+    si.wShowWindow = _SW_HIDE
+
+    pi = _PROCESS_INFORMATION()
+
+    env = ''
+    for k in os.environ:
+        env += "%s=%s\0" % (k, os.environ[k])
+    if not env:
+        env = '\0'
+    env += '\0'
+
+    args = subprocess.list2cmdline(args)
+    # Not running the command in shell mode makes python26 hang when
+    # writing to hgweb output socket.
+    comspec = os.environ.get("COMSPEC", "cmd.exe")
+    args = comspec + " /c " + args
+
+    res = _kernel32.CreateProcessA(
+        None, args, None, None, False, _DETACHED_PROCESS,
+        env, os.getcwd(), ctypes.byref(si), ctypes.byref(pi))
+    if not res:
+        raise ctypes.WinError()
+
+    return pi.dwProcessId