This instance will be upgraded to Heptapod 0.21.0rc2 on 2021-04-16 at 14:00 UTC+2 (a few minutes of down time)

cmdcore.py 34.8 KB
Newer Older
1 2 3 4 5 6 7
# cmdcore.py - run Mercurial commands in a separate thread or process
#
# Copyright 2010 Yuki KODAMA <endflow.net@gmail.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

8
from __future__ import absolute_import
9

10 11 12 13 14
import os
import signal
import struct
import sys
import time
15

16 17 18 19
from mercurial import (
    pycompat,
)

20 21 22 23 24
from .qtcore import (
    QBuffer,
    QIODevice,
    QObject,
    QProcess,
25
    QProcessEnvironment,
26 27 28 29 30 31 32 33 34 35 36
    QTimer,
    pyqtSignal,
    pyqtSlot,
)

from ..util import (
    hglib,
    paths,
    pipeui,
)
from ..util.i18n import _
37

38 39
if hglib.TYPE_CHECKING:
    from typing import (
40 41 42
        Callable,
        Dict,
        List,
43
        Optional,
44 45
        Text,
        Union,
46
    )
47 48 49 50 51 52
    from mercurial import (
        ui as uimod,
    )
    from .qtcore import (
        QByteArray,
    )
53 54 55 56
    from .qtgui import (
        QWidget,
    )

57 58 59
class ProgressMessage(tuple):
    __slots__ = ()

kiilerix's avatar
kiilerix committed
60
    def __new__(cls, topic, pos, item=b'', unit=b'', total=None):
61
        # TODO: accept Text instead of bytes
kiilerix's avatar
kiilerix committed
62 63
        return tuple.__new__(cls, (hglib.tounicode(topic), pos,
            hglib.tounicode(item), hglib.tounicode(unit), total))
64

65 66 67 68 69 70 71 72 73 74 75 76
    if hglib.TYPE_CHECKING:
        # pseudo implementation to help pytype (TODO: replace with attr.s)
        def __init__(self, topic, pos, item=b'', unit=b'', total=None):
            # type: (Union[bytes, Text], Optional[int], Union[bytes, Text], Union[bytes, Text], Optional[int]) -> None
            super(ProgressMessage, self).__init__((
                hglib.tounicode(topic),
                pos,
                hglib.tounicode(item),
                hglib.tounicode(unit),
                total,
            ))

77 78
    @property
    def topic(self):
79 80
        # type: () -> pycompat.unicode
        return self[0]
81 82
    @property
    def pos(self):
83 84
        # type: () -> Optional[int]
        return self[1]
85 86
    @property
    def item(self):
87 88
        # type: () -> pycompat.unicode
        return self[2]
89 90
    @property
    def unit(self):
91 92
        # type: () -> pycompat.unicode
        return self[3]
93 94
    @property
    def total(self):
95 96
        # type: () -> Optional[int]
        return self[4]
97 98 99 100 101 102

    def __repr__(self):
        names = ('topic', 'pos', 'item', 'unit', 'total')
        fields = ('%s=%r' % (n, v) for n, v in zip(names, self))
        return '%s(%s)' % (self.__class__.__name__, ', '.join(fields))

103

104
class UiHandler(object):
105 106 107 108 109 110 111
    """Interface to handle user interaction of Mercurial commands"""

    NoInput = 0
    TextInput = 1
    PasswordInput = 2
    ChoiceInput = 3

112
    def __init__(self):
113
        # type: () -> None
114
        self._datain = None
115 116
        self._dataout = None

117
    def setPrompt(self, text, mode, default=None):
118
        # type: (Text, int, Optional[Text]) -> None
119 120 121
        pass

    def getLineInput(self):
122
        # type: () -> Optional[Text]
123 124 125
        # '' to use default; None to abort
        return ''

126
    def setDataInputDevice(self, device):
127
        # type: (Optional[QIODevice]) -> None
128 129 130
        # QIODevice to read data from; None to disable data input
        self._datain = device

131
    def setDataOutputDevice(self, device):
132
        # type: (Optional[QIODevice]) -> None
133 134 135
        # QIODevice to write data output; None to disable capturing
        self._dataout = device

136
    def inputAtEnd(self):
137
        # type: () -> bool
138 139 140 141 142
        if not self._datain:
            return True
        return self._datain.atEnd()

    def readInput(self, size):
143
        # type: (int) -> Optional[bytes]
144 145 146 147 148
        # b'' for EOF; None for error (per PyQt's QIODevice.read() convention)
        if not self._datain:
            return b''
        return self._datain.read(size)

149
    def writeOutput(self, data, label):
150
        # type: (Union[QByteArray, bytes, bytearray], bytes) -> int
kiilerix's avatar
kiilerix committed
151
        if not self._dataout or label.startswith(b'ui.') or b' ui.' in label:
152 153
            return -1
        return self._dataout.write(data)
154

155 156

def _createDefaultUiHandler(uiparent):
157
    # type: (Optional[QWidget]) -> UiHandler
158 159
    if uiparent is None:
        return UiHandler()
160 161 162 163 164 165 166
    # this makes layering violation but is handy to create GUI handler by
    # default.  nobody would want to write
    #   uihandler = cmdui.InteractiveUiHandler(self)
    #   cmdagent.runCommand(..., uihandler)
    # in place of
    #   cmdagent.runCommand(..., self)
    from tortoisehg.hgqt import cmdui
167
    return cmdui.InteractiveUiHandler(uiparent)
168 169


170 171 172 173
class _ProtocolError(Exception):
    """Error while processing server message; must be caught by CmdWorker"""


174 175 176
class CmdWorker(QObject):
    """Back-end service to run Mercurial commands"""

177 178 179 180 181 182
    # If worker has permanent service, serviceState() should be overridden
    # to represent the availability of the service.  NoService denotes that
    # it can run command or quit immediately.
    NoService = 0
    Starting = 1
    Ready = 2
183
    Stopping = 3
184 185
    Restarting = 4
    NotRunning = 5
186 187

    serviceStateChanged = pyqtSignal(int)
188
    commandFinished = pyqtSignal(int)
189
    outputReceived = pyqtSignal(str, str)
190
    progressReceived = pyqtSignal(ProgressMessage)
191

192
    def serviceState(self):
193
        # type: () -> int
194
        return CmdWorker.NoService
195
    def startService(self):
196
        # type: () -> None
197
        # NotRunning->Starting; Stopping->Restarting->Starting; *->*
198
        pass
199
    def stopService(self):
200
        # type: () -> None
201
        # {Starting,Ready,Restarting}->Stopping; *->*
202
        pass
203
    def startCommand(self, cmdline, uihandler):
204
        # type: (List[Text], UiHandler) -> None
205 206
        raise NotImplementedError
    def abortCommand(self):
207
        # type: () -> None
208 209
        raise NotImplementedError
    def isCommandRunning(self):
210
        # type: () -> bool
211 212 213
        raise NotImplementedError


214
_localprocexts = [
215
    'tortoisehg.util.hgcommands',
216
    'tortoisehg.util.partialcommit',
217 218
    'tortoisehg.util.pipeui',
    ]
219 220 221
_localserverexts = [
    'tortoisehg.util.hgdispatch',
    ]
222

223 224 225 226
if os.name == 'nt':
    # to translate WM_CLOSE posted by QProcess.terminate()
    _localprocexts.append('tortoisehg.util.win32ill')

227 228 229 230 231 232 233
    def _interruptproc(proc):
        proc.terminate()

else:
    def _interruptproc(proc):
        os.kill(int(proc.pid()), signal.SIGINT)

234
def _fixprocenv(proc):
235
    env = QProcessEnvironment.systemEnvironment()
236 237
    # disable flags and extensions that might break our output parsing
    # (e.g. "defaults" arguments, "PAGER" of "email --test")
238
    env.insert('HGPLAINEXCEPT', 'alias,i18n,revsetalias')
239 240 241 242 243 244
    # since sys.path may contain the script directory which the hg process
    # wouldn't see, we have to filter it out
    libpaths = set(sys.path)
    libpaths.discard(os.path.dirname(os.path.realpath(sys.argv[0])))
    thgroot = paths.get_prog_root()
    if not getattr(sys, 'frozen', False) and thgroot not in libpaths:
245
        # make sure hg process can look up our modules
246
        pypath = hglib.tounicode(thgroot)
247
        if env.contains('PYTHONPATH'):
248
            pypath += os.pathsep + pycompat.unicode(env.value('PYTHONPATH'))
249 250
        env.insert('PYTHONPATH', pypath)
    proc.setProcessEnvironment(env)
251

252
def _proccmdline(ui, exts):
253 254 255
    configs = [(hglib.tounicode(section),
                hglib.tounicode(name),
                hglib.tounicode(value))
256
               for section, name, value in ui.walkconfig()
257
               if ui.configsource(section, name) == b'--config']
258
    configs.extend(('extensions', e, '') for e in exts)
259 260 261
    # Override user aliases (for example "alias.log=log --graph") that could
    # break searching through the filter toolbar (CTRL+S).
    configs.append(('alias', 'log', 'log'))
262
    cmdline = list(paths.get_hg_command())
263
    for section, name, value in configs:
264 265
        cmdline.extend(('--config', '%s.%s=%s' % (section, name, value)))
    return cmdline
266 267


268
class CmdProc(CmdWorker):
269
    'Run mercurial command in separate process'
270

271
    def __init__(self, ui, parent=None, cwd=None):
272
        # type: (uimod.ui, Optional[QObject], Optional[Text]) -> None
273
        super(CmdProc, self).__init__(parent)
274
        self._ui = ui
275
        self._uihandler = None
276
        self._proc = proc = QProcess(self)
277
        _fixprocenv(proc)
278 279
        if cwd:
            proc.setWorkingDirectory(cwd)
280
        proc.finished.connect(self._finish)
281 282 283 284
        proc.readyReadStandardOutput.connect(self._stdout)
        proc.readyReadStandardError.connect(self._stderr)
        proc.error.connect(self._handleerror)

285
    def startCommand(self, cmdline, uihandler):
286
        # type: (List[Text], UiHandler) -> None
287
        self._uihandler = uihandler
288
        fullcmdline = _proccmdline(self._ui, _localprocexts)
289
        fullcmdline.extend(cmdline)
290
        self._proc.start(fullcmdline[0], fullcmdline[1:], QIODevice.ReadOnly)
291

292
    def abortCommand(self):
293
        # type: () -> None
294
        if not self.isCommandRunning():
295
            return
296
        _interruptproc(self._proc)
297

298
    def isCommandRunning(self):
299
        # type: () -> bool
300 301
        return self._proc.state() != QProcess.NotRunning

302 303 304 305 306
    @pyqtSlot(int)
    def _finish(self, ret):
        self._uihandler = None
        self.commandFinished.emit(ret)

307
    @pyqtSlot(QProcess.ProcessError)
308
    def _handleerror(self, error):
309
        if error == QProcess.FailedToStart:
310 311
            self.outputReceived.emit(_('failed to start command\n'),
                                     'ui.error')
312
            self._finish(-1)
313
        elif error != QProcess.Crashed:
314 315
            self.outputReceived.emit(_('error while running command\n'),
                                     'ui.error')
316

317
    @pyqtSlot()
318 319
    def _stdout(self):
        data = self._proc.readAllStandardOutput().data()
kiilerix's avatar
kiilerix committed
320
        self._processRead(data, b'')
321

322
    @pyqtSlot()
323 324
    def _stderr(self):
        data = self._proc.readAllStandardError().data()
kiilerix's avatar
kiilerix committed
325
        self._processRead(data, b'ui.error')
326 327 328 329

    def _processRead(self, fulldata, defaultlabel):
        for data in pipeui.splitmsgs(fulldata):
            msg, label = pipeui.unpackmsg(data)
330 331 332
            if (not defaultlabel  # only stdout
                and self._uihandler.writeOutput(msg, label) >= 0):
                continue
kiilerix's avatar
kiilerix committed
333
            if b'ui.progress' in label.split():
334 335
                progress = ProgressMessage(*pipeui.unpackprogress(msg))
                self.progressReceived.emit(progress)
336
            else:
kiilerix's avatar
kiilerix committed
337 338 339
                self.outputReceived.emit(
                    hglib.tounicode(msg),
                    hglib.tounicode(label or defaultlabel))
340 341


342 343 344
class CmdServer(CmdWorker):
    """Run Mercurial commands in command server process"""

345
    def __init__(self, ui, parent=None, cwd=None):
346
        # type: (uimod.ui, Optional[QObject], Optional[Text]) -> None
347
        super(CmdServer, self).__init__(parent)
348
        self._ui = ui
349
        self._uihandler = UiHandler()
350 351
        self._readchtable = self._idlechtable
        self._readq = []  # (ch, data or datasize), ...
352
        # deadline for arrival of hello message and immature data
353
        sec = ui.configint(b'tortoisehg', b'cmdserver.readtimeout')
354
        self._readtimer = QTimer(self, interval=sec * 1000, singleShot=True)
355
        self._readtimer.timeout.connect(self._onReadTimeout)
356 357
        self._proc = self._createProc(cwd)
        self._servicestate = CmdWorker.NotRunning
358

359 360
    def _createProc(self, cwd):
        proc = QProcess(self)
361 362 363 364 365 366 367 368
        _fixprocenv(proc)
        if cwd:
            proc.setWorkingDirectory(cwd)
        proc.error.connect(self._onServiceError)
        proc.finished.connect(self._onServiceFinished)
        proc.setReadChannel(QProcess.StandardOutput)
        proc.readyRead.connect(self._onReadyRead)
        proc.readyReadStandardError.connect(self._onReadyReadError)
369
        return proc
370 371

    def serviceState(self):
372
        # type: () -> int
373 374 375 376 377 378 379 380
        return self._servicestate

    def _changeServiceState(self, newstate):
        if self._servicestate == newstate:
            return
        self._servicestate = newstate
        self.serviceStateChanged.emit(newstate)

381
    def startService(self):
382
        # type: () -> None
383 384 385 386 387 388
        if self._servicestate == CmdWorker.NotRunning:
            self._startService()
        elif self._servicestate == CmdWorker.Stopping:
            self._changeServiceState(CmdWorker.Restarting)

    def _startService(self):
389 390 391 392 393 394 395
        if self._proc.bytesToWrite() > 0:
            # QTBUG-44517: recreate QProcess to discard remainder of last
            # request; otherwise it would be written to new process
            oldproc = self._proc
            self._proc = self._createProc(oldproc.workingDirectory())
            oldproc.setParent(None)

396
        cmdline = _proccmdline(self._ui, _localprocexts + _localserverexts)
397 398
        cmdline.extend(['serve', '--cmdserver', 'pipe',
                        '--config', 'ui.interactive=True'])
399
        self._readchtable = self._hellochtable
400
        self._readtimer.start()
401 402 403
        self._changeServiceState(CmdWorker.Starting)
        self._proc.start(cmdline[0], cmdline[1:])

404
    def stopService(self):
405
        # type: () -> None
406 407 408 409 410 411
        if self._servicestate in (CmdWorker.Starting, CmdWorker.Ready):
            self._stopService()
        elif self._servicestate == CmdWorker.Restarting:
            self._changeServiceState(CmdWorker.Stopping)

    def _stopService(self):
412
        self._changeServiceState(CmdWorker.Stopping)
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
        _interruptproc(self._proc)
        # make sure "serve" loop ends by EOF (necessary on Windows)
        self._proc.closeWriteChannel()

    def _emitError(self, msg):
        self.outputReceived.emit('cmdserver: %s\n' % msg, 'ui.error')

    @pyqtSlot(QProcess.ProcessError)
    def _onServiceError(self, error):
        self._emitError(self._proc.errorString())
        if error == QProcess.FailedToStart:
            self._onServiceFinished()

    @pyqtSlot()
    def _onServiceFinished(self):
428
        self._uihandler = UiHandler()
429 430
        self._readchtable = self._idlechtable
        del self._readq[:]
431
        self._readtimer.stop()
432 433 434
        if self._servicestate == CmdWorker.Restarting:
            self._startService()
            return
435 436 437
        if self._servicestate != CmdWorker.Stopping:
            self._emitError(_('process exited unexpectedly with code %d')
                            % self._proc.exitCode())
438
        self._changeServiceState(CmdWorker.NotRunning)
439 440

    def isCommandRunning(self):
441
        # type: () -> bool
442 443
        return self._readchtable is self._runcommandchtable

444
    def startCommand(self, cmdline, uihandler):
445
        # type: (List[Text], UiHandler) -> None
446
        assert self._servicestate == CmdWorker.Ready, self._servicestate
447
        assert not self.isCommandRunning()
448 449
        try:
            data = hglib.fromunicode('\0'.join(cmdline))
450
        except UnicodeEncodeError as inst:
451 452 453
            self._emitError(_('failed to encode command: %s') % inst)
            self._finishCommand(-1)
            return
454
        self._uihandler = uihandler
455
        self._readchtable = self._runcommandchtable
456
        self._proc.write(b'runcommand\n')
457 458 459
        self._writeBlock(data)

    def abortCommand(self):
460
        # () -> None
461 462 463 464 465
        if not self.isCommandRunning():
            return
        _interruptproc(self._proc)

    def _finishCommand(self, ret):
466
        self._uihandler = UiHandler()
467 468 469 470 471 472 473 474 475 476 477 478 479
        self._readchtable = self._idlechtable
        self.commandFinished.emit(ret)

    def _writeBlock(self, data):
        self._proc.write(struct.pack('>I', len(data)))
        self._proc.write(data)

    @pyqtSlot()
    def _onReadyRead(self):
        proc = self._proc
        headersize = 5
        try:
            while True:
480
                header = proc.peek(headersize).data()
481 482 483
                if not header:
                    self._readtimer.stop()
                    break
484
                if len(header) < headersize:
485
                    self._readtimer.start()
486 487
                    break
                ch, datasize = struct.unpack('>cI', header)
488
                if ch in b'IL':
489 490 491 492 493
                    # input channel has no data
                    proc.read(headersize)
                    self._readq.append((ch, datasize))
                    continue
                if proc.bytesAvailable() < headersize + datasize:
494
                    self._readtimer.start()
495 496
                    break
                proc.read(headersize)
497
                data = proc.read(datasize)
498 499 500 501 502
                self._readq.append((ch, data))

            # don't do much things in readyRead slot for simplicity
            QTimer.singleShot(0, self._dispatchRead)
        except Exception:
503
            self.stopService()
504 505
            raise

506 507
    @pyqtSlot()
    def _onReadTimeout(self):
508
        startbytes = self._proc.peek(20).data()
509 510 511 512 513 514 515
        if startbytes:
            # data corruption because bad extension might write to stdout?
            self._emitError(_('timed out while reading: %r...') % startbytes)
        else:
            self._emitError(_('timed out waiting for message'))
        self.stopService()

516 517 518 519 520 521 522 523 524 525
    @pyqtSlot()
    def _dispatchRead(self):
        try:
            while self._readq:
                ch, dataorsize = self._readq.pop(0)
                try:
                    chfunc = self._readchtable[ch]
                except KeyError:
                    if not ch.isupper():
                        continue
526 527
                    raise _ProtocolError(_('unexpected response on required '
                                           'channel %r') % ch)
528
                chfunc(self, ch, dataorsize)
529
        except _ProtocolError as inst:
530 531
            self._emitError(inst.args[0])
            self.stopService()
532
        except Exception:
533
            self.stopService()
534 535 536 537
            raise

    @pyqtSlot()
    def _onReadyReadError(self):
538
        fulldata = self._proc.readAllStandardError().data()
539 540
        for data in pipeui.splitmsgs(fulldata):
            msg, label = pipeui.unpackmsg(data)
kiilerix's avatar
kiilerix committed
541 542 543
            self.outputReceived.emit(
                hglib.tounicode(msg),
                hglib.tounicode(label) or 'ui.error')
544

545
    def _processHello(self, _ch, data):
546
        try:
547 548
            fields = dict(l.split(b':', 1) for l in data.splitlines())
            capabilities = fields[b'capabilities'].split()
549
        except (KeyError, ValueError):
550
            raise _ProtocolError(_('invalid "hello" message: %r') % data)
551
        if b'runcommand' not in capabilities:
552
            raise _ProtocolError(_('no "runcommand" capability'))
553 554 555
        self._readchtable = self._idlechtable
        self._changeServiceState(CmdWorker.Ready)

556
    def _processOutput(self, ch, data):
557
        msg, label = pipeui.unpackmsg(data)
558
        if ch == b'o' and self._uihandler.writeOutput(msg, label) >= 0:
559
            return
560
        labelset = label.split()
kiilerix's avatar
kiilerix committed
561
        if b'ui.progress' in labelset:
562 563
            progress = ProgressMessage(*pipeui.unpackprogress(msg))
            self.progressReceived.emit(progress)
kiilerix's avatar
kiilerix committed
564 565
        elif b'ui.prompt' in labelset:
            if b'ui.getpass' in labelset:
566
                mode = UiHandler.PasswordInput
kiilerix's avatar
kiilerix committed
567
            elif b'ui.promptchoice' in labelset:
568 569 570 571
                mode = UiHandler.ChoiceInput
            else:
                mode = UiHandler.TextInput
            prompt, default = pipeui.unpackprompt(msg)
kiilerix's avatar
kiilerix committed
572 573
            self._uihandler.setPrompt(hglib.tounicode(prompt), mode,
                                      hglib.tounicode(default))
574
        else:
kiilerix's avatar
kiilerix committed
575 576 577
            self.outputReceived.emit(
                hglib.tounicode(msg),
                hglib.tounicode(label))
578

579
    def _processCommandResult(self, _ch, data):
580 581 582
        try:
            ret, = struct.unpack('>i', data)
        except struct.error:
583
            raise _ProtocolError(_('corrupted command result: %r') % data)
584 585
        self._finishCommand(ret)

586 587 588 589 590 591 592 593 594 595 596
    def _processInputRequest(self, _ch, size):
        data = self._uihandler.readInput(size)
        if data is None:
            self._emitError(_('error while reading from data input'))
            self._writeBlock(b'')
            return
        if not data and not self._uihandler.inputAtEnd():
            # TODO: should stop processing until readyRead()
            self._emitError(_('asynchronous read is not implement yet'))
        self._writeBlock(data)

597
    def _processLineRequest(self, _ch, size):
598 599
        # TODO: if no prompt observed, this should be redirected to
        # self._uihandler.readLineInput(size + 1)
600 601
        text = self._uihandler.getLineInput()
        if text is None:
kiilerix's avatar
kiilerix committed
602
            self._writeBlock(b'')
603
            return
604
        try:
kiilerix's avatar
kiilerix committed
605
            data = hglib.fromunicode(text) + b'\n'
606
        except UnicodeEncodeError as inst:
607 608 609
            self._emitError(_('failed to encode input: %s') % inst)
            self.abortCommand()
            return
610
        for start in pycompat.xrange(0, len(data), size):
611 612
            self._writeBlock(data[start:start + size])

613
    _idlechtable = {
614 615
        b'o': _processOutput,
        b'e': _processOutput,
616 617 618
        }

    _hellochtable = {
619 620
        b'o': _processHello,
        b'e': _processOutput,
621 622 623
        }

    _runcommandchtable = {
624 625 626 627 628
        b'o': _processOutput,
        b'e': _processOutput,
        b'r': _processCommandResult,
        b'I': _processInputRequest,
        b'L': _processLineRequest,
629 630 631
        }


632 633
_workertypes = {
    'proc': CmdProc,
634
    'server': CmdServer,
635
}  # type: Dict[Text, Callable[[uimod.ui, Optional[QObject], Optional[Text]], CmdWorker]]
636 637


638
class CmdSession(QObject):
639
    """Run Mercurial commands in a background thread or process"""
640 641

    commandFinished = pyqtSignal(int)
642 643
    # in order to receive only notification messages of session state, use
    # "controlMessage"; otherwise use "outputReceived"
644 645
    controlMessage = pyqtSignal(str)
    outputReceived = pyqtSignal(str, str)
646
    progressReceived = pyqtSignal(ProgressMessage)
647
    readyRead = pyqtSignal()
648

649
    def __init__(self, cmdlines, uihandler, parent=None):
650
        # type: (List[List[Text]], UiHandler, Optional[QObject]) -> None
651
        super(CmdSession, self).__init__(parent)
652
        self._uihandler = uihandler
653
        self._worker = None  # type: Optional[CmdWorker]
654
        self._queue = list(cmdlines)
655
        self._qnextp = 0
656
        self._abortbyuser = False
657
        self._erroroutputs = []
658
        self._warningoutputs = []
659
        self._dataoutrbuf = QBuffer(self)
660
        self._exitcode = 0
661 662 663
        if not cmdlines:
            # assumes null session is failure for convenience
            self._exitcode = -1
664

665
    def run(self, worker):
666
        # type: (CmdWorker) -> None
667
        '''Execute Mercurial command'''
668
        if self._worker or self._qnextp >= len(self._queue):
669
            return
670
        self._connectWorker(worker)
671 672
        if worker.serviceState() in (CmdWorker.NoService, CmdWorker.Ready):
            self._runNext()
673

674
    def abort(self):
675
        # type: () -> None
676
        '''Cancel running Mercurial command'''
677
        if self.isRunning():
678
            self._worker.abortCommand()
679
            self._qnextp = len(self._queue)
680 681
            self._abortbyuser = True
        elif not self.isFinished():
682
            self._abortbyuser = True
683 684
            # -1 instead of 255 for compatibility with CmdThread
            self._finish(-1)
685 686

    def isAborted(self):
687
        # type: () -> bool
688 689 690 691
        """True if commands have finished by user abort"""
        return self.isFinished() and self._abortbyuser

    def isFinished(self):
692
        # type: () -> bool
693
        """True if all pending commands have finished or been aborted"""
694
        return self._qnextp >= len(self._queue) and not self.isRunning()
695

696
    def isRunning(self):
697
        # type: () -> bool
698
        """True if a command is running; False if finished or not started yet"""
699
        return bool(self._worker) and self._qnextp > 0
700

701
    def errorString(self):
702
        # type: () -> Text
703 704 705 706 707 708
        """Error message received in the last command"""
        if self._abortbyuser:
            return _('Terminated by user')
        else:
            return ''.join(self._erroroutputs).rstrip()

709
    def warningString(self):
710
        # type: () -> Text
711 712 713
        """Warning message received in the last command"""
        return ''.join(self._warningoutputs).rstrip()

714
    def exitCode(self):
715
        # type: () -> int
716 717 718
        """Integer return code of the last command"""
        return self._exitcode

719
    def setCaptureOutput(self, enabled):
720
        # type: (bool) -> None
721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736
        """If enabled, data outputs (without "ui.*" label) are queued and
        outputReceived signal is not emitted in that case.  This is useful
        for receiving data to be parsed or copied to the clipboard.
        """
        # pseudo FIFO between client "rbuf" and worker "wbuf"; not efficient
        # for large data since all outputs will be stored in memory
        if enabled:
            self._dataoutrbuf.open(QIODevice.ReadOnly | QIODevice.Truncate)
            dataoutwbuf = QBuffer(self._dataoutrbuf.buffer())
            dataoutwbuf.bytesWritten.connect(self.readyRead)
            dataoutwbuf.open(QIODevice.WriteOnly)
        else:
            self._dataoutrbuf.close()
            dataoutwbuf = None
        self.setOutputDevice(dataoutwbuf)

737
    def setInputDevice(self, device):
738
        # type: (Optional[QIODevice]) -> None
739 740 741 742 743 744
        """If set, data will be read from the specified device on data input
        request"""
        if self.isRunning():
            raise RuntimeError('command already running')
        self._uihandler.setDataInputDevice(device)

745
    def setOutputDevice(self, device):
746
        # type: (Optional[QIODevice]) -> None
747
        """If set, data outputs will be sent to the specified device"""
748 749
        if self.isRunning():
            raise RuntimeError('command already running')
750 751 752
        self._uihandler.setDataOutputDevice(device)

    def read(self, maxlen):
753
        # type: (int) -> bytes
754 755 756 757
        """Read output if capturing enabled; ui messages are not included"""
        return self._dataoutrbuf.read(maxlen)

    def readAll(self):
758
        # type: () -> QByteArray
759 760 761
        return self._dataoutrbuf.readAll()

    def readLine(self, maxlen=0):
762
        # type: (int) -> bytes
763 764 765
        return self._dataoutrbuf.readLine(maxlen)

    def canReadLine(self):
766
        # type: () -> bool
767 768 769
        return self._dataoutrbuf.canReadLine()

    def peek(self, maxlen):
770
        # type: (int) -> QByteArray
771 772
        return self._dataoutrbuf.peek(maxlen)

773 774
    def _connectWorker(self, worker):
        self._worker = worker
775
        worker.serviceStateChanged.connect(self._onWorkerStateChanged)
776 777 778 779
        worker.commandFinished.connect(self._onCommandFinished)
        worker.outputReceived.connect(self.outputReceived)
        worker.outputReceived.connect(self._captureOutput)
        worker.progressReceived.connect(self.progressReceived)
780 781 782 783 784

    def _disconnectWorker(self):
        worker = self._worker
        if not worker:
            return
785
        worker.serviceStateChanged.disconnect(self._onWorkerStateChanged)
786 787 788