mercurial/win32.py
changeset 13375 f1fa8f481c7c
parent 13374 1c613c1ae43d
child 13376 60b5c6c3fd12
--- 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