Commits

Yuya Nishihara committed 6392b33

sigpipe patches

  • Participants
  • Parent commits fc98f0b

Comments (0)

Files changed (5)

+win32sigpipe.diff
+win32sigpipe-use.diff
 serve-useproc-early.diff
+win32sigpipe-dirtyhack.diff
 cmdui-useagent.diff
 cmdcore-nostartedsig.diff
 cmdcore-sessinit.diff

File serve-useproc-early.diff

 # HG changeset patch
 # Date 1378022851 -32400
 # Parent 705468a76be5792b98c1eb7ef7eaa3feb6c10444
-serve: run hgweb in separate process
+serve: run hgweb server in separate process
 
 It can eliminate several dirty hacks, and will contribute to the stability.
 
+XXX abort() not work on Windows. It can't terminate the whole process tree.
+
 diff --git a/tortoisehg/hgqt/serve.py b/tortoisehg/hgqt/serve.py
 --- a/tortoisehg/hgqt/serve.py
 +++ b/tortoisehg/hgqt/serve.py

File win32sigpipe-dirtyhack.diff

+# HG changeset patch
+# Parent 7947835bb12223c8e8273eefe78d33d3658b3c45
+
+diff --git a/tortoisehg/hgqt/cmdcore.py b/tortoisehg/hgqt/cmdcore.py
+--- a/tortoisehg/hgqt/cmdcore.py
++++ b/tortoisehg/hgqt/cmdcore.py
+@@ -152,12 +152,14 @@ class CmdProc(QObject):
+ 
+     def start(self):
+         cmdline, self._procid = _preparecmdproc(self.cmdline)
+-        self._proc.start(_findhgexe(), cmdline, QIODevice.ReadOnly)
++        self._proc.start(_findhgexe(), cmdline, QIODevice.ReadWrite)
+ 
+     def abort(self):
+         if not self.isRunning():
+             return
+         _killcmdproc(self._procid or int(self._proc.pid()), signal.SIGINT)
++        if os.name == 'nt' and _findhgexe().lower().endswith('.bat'):
++            self._proc.write('Y\r\n')  # answer to "Terminate Batch job (Y/N)?"
+ 
+     def isRunning(self):
+         return self._proc.state() != QProcess.NotRunning

File win32sigpipe-use.diff

+# HG changeset patch
+# Date 1378047277 -32400
+# Parent aa6aab023503f87b27c4d90556a86ac71dd4279f
+cmdcore: implement graceful abort on process-based command executor
+
+diff --git a/tortoisehg/hgqt/cmdcore.py b/tortoisehg/hgqt/cmdcore.py
+--- a/tortoisehg/hgqt/cmdcore.py
++++ b/tortoisehg/hgqt/cmdcore.py
+@@ -5,7 +5,7 @@
+ # This software may be used and distributed according to the terms of the
+ # GNU General Public License version 2, incorporated herein by reference.
+ 
+-import os, struct, sys, time
++import os, signal, struct, sys, time
+ 
+ from PyQt4.QtCore import QIODevice, QObject, QProcess
+ from PyQt4.QtCore import pyqtSignal, pyqtSlot
+@@ -89,6 +89,34 @@ class SigPipeServer(QObject):
+         raise ValueError('unregistered connection: %r' % conn)
+ 
+ 
++if os.name == 'nt':
++    _sigpipeserver = SigPipeServer()
++    _sigpipeserial = iter(xrange(1, sys.maxint))
++
++    def _preparecmdproc(cmdline):
++        if not _sigpipeserver.isListening():
++            _sigpipeserver.listen('thg-sigpipe-%d' % os.getpid())
++            # TODO: error handling, shutdown, etc.
++
++        servername = _sigpipeserver.fullServerName()
++        clientid = _sigpipeserial.next()
++        fullcmdline = ['--config', 'extensions.tortoisehg.util.win32sigpipe=',
++                       '--sigpipefile', hglib.fromunicode(servername),
++                       '--sigpipeid', str(clientid)]
++        fullcmdline.extend(cmdline)
++        return fullcmdline, clientid
++
++    def _killcmdproc(clientid, sig):
++        _sigpipeserver.sendSignal(clientid, sig)
++
++else:
++    def _preparecmdproc(cmdline):
++        return cmdline, None
++
++    def _killcmdproc(pid, sig):
++        os.kill(pid, sig)
++
++
+ def _findhgexe():
+     exepath = None
+     if hasattr(sys, 'frozen'):
+@@ -120,14 +148,16 @@ class CmdProc(QObject):
+         proc.readyReadStandardOutput.connect(self._stdout)
+         proc.readyReadStandardError.connect(self._stderr)
+         proc.error.connect(self._handleerror)
++        self._procid = None
+ 
+     def start(self):
+-        self._proc.start(_findhgexe(), self.cmdline, QIODevice.ReadOnly)
++        cmdline, self._procid = _preparecmdproc(self.cmdline)
++        self._proc.start(_findhgexe(), cmdline, QIODevice.ReadOnly)
+ 
+     def abort(self):
+         if not self.isRunning():
+             return
+-        self._proc.close()
++        _killcmdproc(self._procid or int(self._proc.pid()), signal.SIGINT)
+ 
+     def isRunning(self):
+         return self._proc.state() != QProcess.NotRunning

File win32sigpipe.diff

+# HG changeset patch
+# Date 1378045895 -32400
+# Parent 705468a76be5792b98c1eb7ef7eaa3feb6c10444
+cmdcore: add extension and server to kill hg process by SIGINT on Windows
+
+diff --git a/tortoisehg/hgqt/cmdcore.py b/tortoisehg/hgqt/cmdcore.py
+--- a/tortoisehg/hgqt/cmdcore.py
++++ b/tortoisehg/hgqt/cmdcore.py
+@@ -5,15 +5,90 @@
+ # This software may be used and distributed according to the terms of the
+ # GNU General Public License version 2, incorporated herein by reference.
+ 
+-import os, sys, time
++import os, struct, sys, time
+ 
+ from PyQt4.QtCore import QIODevice, QObject, QProcess
+ from PyQt4.QtCore import pyqtSignal, pyqtSlot
++from PyQt4.QtNetwork import QLocalServer
+ 
+ from tortoisehg.util import hglib, paths
+ from tortoisehg.hgqt.i18n import _
+ from tortoisehg.hgqt import thread
+ 
++class SigPipeServer(QObject):
++    """Side channel to send Ctrl+C to command process on Windows"""
++
++    # XXX debug print should be removed
++
++    def __init__(self, parent=None):
++        super(SigPipeServer, self).__init__(parent)
++        self._server = QLocalServer(self)
++        self._server.newConnection.connect(self._acceptConnections)
++        self._connections = {}  # by clientid
++
++    def isListening(self):
++        return self._server.isListening()
++
++    def listen(self, name):
++        self._server.listen(name)
++
++    def close(self):
++        self._connections.clear()
++        self._server.close()
++
++    def fullServerName(self):
++        return self._server.fullServerName()
++
++    def sendSignal(self, clientid, signum):
++        print 'sending signal %d to id %d' % (signum, clientid)
++        try:
++            conn = self._connections[clientid]
++        except KeyError:
++            raise ValueError('unknown id %d' % clientid)
++        conn.write(struct.pack('>L', signum))
++
++    @pyqtSlot()
++    def _acceptConnections(self):
++        while self._server.hasPendingConnections():
++            print 'accepted new connection'
++            conn = self._server.nextPendingConnection()
++            conn.readyRead.connect(self._registerConnection)
++
++    @pyqtSlot()
++    def _registerConnection(self):
++        conn = self.sender()
++        if conn.bytesAvailable() < 4:
++            return
++        clientid, = struct.unpack('>L', str(conn.read(4)))
++        print 'register connection (id = %d)' % clientid
++        self._connections[clientid] = conn
++        conn.readyRead.disconnect(self._registerConnection)
++        conn.readyRead.connect(self._handleResponse)
++        conn.disconnected.connect(self._unregisterConnection)
++
++    @pyqtSlot()
++    def _handleResponse(self):
++        conn = self.sender()
++        if conn.bytesAvailable() < 4:
++            return
++        code, = struct.unpack('>L', str(conn.read(4)))
++        clientid = self._findclientid(conn)
++        print 'response %d got (id = %d)' % (code, clientid)
++
++    @pyqtSlot()
++    def _unregisterConnection(self):
++        conn = self.sender()
++        clientid = self._findclientid(conn)
++        print 'unregister connection (id = %d)' % clientid
++        del self._connections[clientid]
++
++    def _findclientid(self, conn):
++        for clientid, e in self._connections.items():
++            if e == conn:
++                return clientid
++        raise ValueError('unregistered connection: %r' % conn)
++
++
+ def _findhgexe():
+     exepath = None
+     if hasattr(sys, 'frozen'):
+diff --git a/tortoisehg/util/win32sigpipe.py b/tortoisehg/util/win32sigpipe.py
+new file mode 100644
+--- /dev/null
++++ b/tortoisehg/util/win32sigpipe.py
+@@ -0,0 +1,162 @@
++# win32sigpipe.py - listen to SIGINT request on named pipe
++#
++# Copyright 2012 Yuya Nishihara <yuya@tcha.org>
++#
++# This software may be used and distributed according to the terms of the
++# GNU General Public License version 2 or any later version.
++
++"""listen to SIGINT request on Windows named pipe
++
++In short, this extension provides alternative to `os.kill(pid, signal.SIGINT)`
++on Windows.
++
++Background:
++
++Windows cannot send Ctrl-C signal to CLI process without a console window, which
++means there is no easy way to abort 'hg push' safely from your application.
++`GenerateConsoleCtrlEvent()` is very attractive, but look now, it's just able
++to signal `CTRL_C_EVENT` to all processes sharing the console.
++
++- http://stackoverflow.com/questions/1453520/
++- http://msdn.microsoft.com/en-us/library/windows/desktop/ms683155(v=vs.85).aspx
++
++How it works:
++
++1. master process starts listening on `\\.\pipe\{pipename}`
++2. master executes hg command with `--sigpipefile \\.\pipe\{pipename}` and
++   optional `--sigpipeid {clientid}`
++3. sub hg process connects to the specified pipe, then writes its id
++4. to abort running command, master writes SIGINT number to the pipe
++5. sub hg process signals `CTRL_C_EVENT` to all processes sharing the console,
++   i.e to itself
++
++Make sure to set `CREATE_NO_WINDOW` or `CREATE_NEW_CONSOLE` to `dwCreationFlags`
++when creating hg process; otherwise the master process will also receive
++`CTRL_C_EVENT`.
++
++If the master process communicates with the sub hg process via stdio, the master
++also needs to close the write channel of the sub.
++
++Communication protocol::
++
++    master
++       |      create process
++       |--------------------------> hg
++       |                            |  open(pipe)
++       |<---------------------------|
++       |         write(id)          |
++       |                            |
++       |       write(SIGINT)        |
++       |--------------------------->|
++       |                            |---
++       |                            |   | CTRL_C_EVENT
++       |                            |<--
++       |                            |
++       |       write(errno)         | (could be omitted if no error occured)
++       |<---------------------------|
++       |                            x abort by KeyboardInterrupt
++       :
++
++    (each message is packed as 4-byte unsigned integer, big endian)
++"""
++
++import ctypes, errno, os, signal, struct, threading
++from mercurial import commands, dispatch, extensions, util
++from mercurial.i18n import _
++
++testedwith = '2.3 2.7'
++
++def killme(signum):
++    if signum != signal.SIGINT:
++        raise ValueError('unsupported signal: %d' % signum)
++
++    GenerateConsoleCtrlEvent = ctypes.windll.kernel32.GenerateConsoleCtrlEvent
++    CTRL_C_EVENT = 0
++    # dwProcessGroupId=0 means all processes sharing the same console,
++    # which is the only choice for CTRL_C_EVENT.
++    r = GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)
++    if not r:
++        raise ctypes.WinError()
++
++class sigpipelistener(object):
++    def __init__(self):
++        self._thread = threading.Thread(target=self._mainloop)
++        self._thread.daemon = True  # killable
++
++    def listen(self, pipename, clientid):
++        try:
++            self._pipefd = os.open(pipename, os.O_RDWR | os.O_BINARY)
++        except IOError, err:
++            if err.errno != errno.ENOENT:
++                raise
++            raise util.Abort(_('cannot open named pipe: %s') % pipename)
++        try:
++            self._writecode(clientid)
++            self._thread.start()
++        except Exception:
++            os.close(self._pipefd)
++            raise
++
++    def _mainloop(self):
++        try:
++            while True:
++                signum = self._readsig()
++                if signum is None:
++                    return
++                try:
++                    killme(signum)
++                    r = 0
++                except OSError, err:
++                    r = err.errno
++                except ValueError:
++                    r = 0xffffffff
++                self._writecode(r)
++        finally:
++            os.close(self._pipefd)
++
++    def _readsig(self):
++        buf = ''
++        while len(buf) < 4:
++            s = os.read(self._pipefd, 4 - len(buf))
++            if not s:
++                return
++            buf += s
++        return struct.unpack('>L', buf)[0]
++
++    def _writecode(self, num):
++        os.write(self._pipefd, struct.pack('>L', num))
++
++def _runcommand(orig, lui, repo, cmd, fullargs, ui, options, d, cmdpats,
++                cmdoptions):
++    pipename, clientid = options['sigpipefile'], options['sigpipeid']
++    if pipename:
++        if clientid:
++            try:
++                clientidnum = int(clientid, 0)
++            except ValueError:
++                raise util.Abort(_('invalid sigpipeid: %s') % clientid)
++        else:
++            clientidnum = os.getpid()
++
++        listener = sigpipelistener()
++        listener.listen(pipename, clientidnum)
++
++    return orig(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions)
++
++def uisetup(ui):
++    if os.name != 'nt':
++        ui.warn(_('win32sigpipe: unsupported platform: %s\n' % os.name))
++        return
++    extensions.wrapfunction(dispatch, 'runcommand', _runcommand)
++
++def extsetup(ui):
++    if os.name != 'nt':
++        return
++    commands.globalopts.extend([
++        ('', 'sigpipefile', '',
++         _('connect to named pipe for listening to SIGINT request'),
++         _('NAME')),
++        ('', 'sigpipeid', '',
++         _('integer to identify the client process (default: pid)'),
++         _('NUM')),
++        ])