/tortoisehg/hgqt/thread.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 314 lines · 247 code · 44 blank · 23 comment · 61 complexity · a3d6edeb72c67c530f92b7cb195d5392 MD5 · raw file

  1. # thread.py - A seprated thread to run Mercurial command
  2. #
  3. # Copyright 2009 Steve Borho <steve@borho.org>
  4. # Copyright 2010 Yuki KODAMA <endflow.net@gmail.com>
  5. #
  6. # This software may be used and distributed according to the terms of the
  7. # GNU General Public License version 2, incorporated herein by reference.
  8. import os
  9. import Queue
  10. import time
  11. import urllib2
  12. from PyQt4.QtCore import *
  13. from PyQt4.QtGui import *
  14. from mercurial import util, error, dispatch
  15. from mercurial import ui as uimod
  16. from tortoisehg.util import thread2, hglib
  17. from tortoisehg.hgqt.i18n import _, localgettext
  18. from tortoisehg.hgqt import qtlib
  19. local = localgettext()
  20. class DataWrapper(object):
  21. def __init__(self, data):
  22. self.data = data
  23. class UiSignal(QObject):
  24. writeSignal = pyqtSignal(QString, QString)
  25. progressSignal = pyqtSignal(QString, object, QString, QString, object)
  26. interactSignal = pyqtSignal(DataWrapper)
  27. def __init__(self, responseq):
  28. QObject.__init__(self)
  29. self.responseq = responseq
  30. def write(self, *args, **opts):
  31. msg = hglib.tounicode(''.join(args))
  32. label = hglib.tounicode(opts.get('label', ''))
  33. self.writeSignal.emit(msg, label)
  34. def write_err(self, *args, **opts):
  35. msg = hglib.tounicode(''.join(args))
  36. label = hglib.tounicode(opts.get('label', 'ui.error'))
  37. self.writeSignal.emit(msg, label)
  38. def prompt(self, msg, choices, default):
  39. try:
  40. r = self._waitresponse(msg, False, choices, None)
  41. if r is None:
  42. raise EOFError
  43. if not r:
  44. return default
  45. if choices:
  46. # return char for Mercurial 1.3
  47. choice = choices[r]
  48. return choice[choice.index('&')+1].lower()
  49. return r
  50. except EOFError:
  51. raise util.Abort(local._('response expected'))
  52. def promptchoice(self, msg, choices, default):
  53. try:
  54. r = self._waitresponse(msg, False, choices, default)
  55. if r is None:
  56. raise EOFError
  57. return r
  58. except EOFError:
  59. raise util.Abort(local._('response expected'))
  60. def getpass(self, prompt, default):
  61. r = self._waitresponse(prompt, True, None, default)
  62. if r is None:
  63. raise util.Abort(local._('response expected'))
  64. return r
  65. def _waitresponse(self, msg, password, choices, default):
  66. """Request interaction with GUI and wait response from it"""
  67. data = DataWrapper((msg, password, choices, default))
  68. self.interactSignal.emit(data)
  69. # await response
  70. return self.responseq.get(True)
  71. def progress(self, topic, pos, item, unit, total):
  72. topic = hglib.tounicode(topic)
  73. item = hglib.tounicode(item)
  74. unit = hglib.tounicode(unit)
  75. self.progressSignal.emit(topic, pos, item, unit, total)
  76. class QtUi(uimod.ui):
  77. def __init__(self, src=None, responseq=None):
  78. super(QtUi, self).__init__(src)
  79. if src:
  80. self.sig = src.sig
  81. else:
  82. self.sig = UiSignal(responseq)
  83. self.setconfig('ui', 'interactive', 'on')
  84. self.setconfig('progress', 'disable', 'True')
  85. os.environ['TERM'] = 'dumb'
  86. def write(self, *args, **opts):
  87. if self._buffers:
  88. self._buffers[-1].extend([str(a) for a in args])
  89. else:
  90. self.sig.write(*args, **opts)
  91. def write_err(self, *args, **opts):
  92. self.sig.write_err(*args, **opts)
  93. def label(self, msg, label):
  94. return msg
  95. def flush(self):
  96. pass
  97. def prompt(self, msg, choices=None, default='y'):
  98. if not self.interactive(): return default
  99. return self.sig.prompt(msg, choices, default)
  100. def promptchoice(self, msg, choices, default=0):
  101. if not self.interactive(): return default
  102. return self.sig.promptchoice(msg, choices, default)
  103. def getpass(self, prompt=_('password: '), default=None):
  104. return self.sig.getpass(prompt, default)
  105. def progress(self, topic, pos, item='', unit='', total=None):
  106. return self.sig.progress(topic, pos, item, unit, total)
  107. class CmdThread(QThread):
  108. """Run an Mercurial command in a background thread, implies output
  109. is being sent to a rendered text buffer interactively and requests
  110. for feedback from Mercurial can be handled by the user via dialog
  111. windows.
  112. """
  113. # (msg=str, label=str)
  114. outputReceived = pyqtSignal(QString, QString)
  115. # (topic=str, pos=int, item=str, unit=str, total=int)
  116. # pos and total are emitted as object, since they may be None
  117. progressReceived = pyqtSignal(QString, object, QString, QString, object)
  118. # result: -1 - command is incomplete, possibly exited with exception
  119. # 0 - command is finished successfully
  120. # others - return code of command
  121. commandFinished = pyqtSignal(int)
  122. def __init__(self, cmdline, display, parent=None):
  123. super(CmdThread, self).__init__(parent)
  124. self.cmdline = cmdline
  125. self.display = display
  126. self.ret = -1
  127. self.abortbyuser = False
  128. self.responseq = Queue.Queue()
  129. self.rawoutput = QStringList()
  130. self.topics = {}
  131. self.curstrs = QStringList()
  132. self.curlabel = None
  133. self.timer = QTimer(self)
  134. self.timer.timeout.connect(self.flush)
  135. self.timer.start(100)
  136. self.finished.connect(self.thread_finished)
  137. def abort(self):
  138. if self.isRunning() and hasattr(self, 'thread_id'):
  139. self.abortbyuser = True
  140. try:
  141. thread2._async_raise(self.thread_id, KeyboardInterrupt)
  142. except ValueError:
  143. pass
  144. def thread_finished(self):
  145. self.timer.stop()
  146. self.flush()
  147. self.commandFinished.emit(self.ret)
  148. def flush(self):
  149. if self.curlabel is not None:
  150. self.outputReceived.emit(self.curstrs.join(''), self.curlabel)
  151. self.curlabel = None
  152. if self.timer.isActive():
  153. keys = self.topics.keys()
  154. for topic in keys:
  155. pos, item, unit, total = self.topics[topic]
  156. self.progressReceived.emit(topic, pos, item, unit, total)
  157. if pos is None:
  158. del self.topics[topic]
  159. else:
  160. # Close all progress bars
  161. for topic in self.topics:
  162. self.progressReceived.emit(topic, None, '', '', None)
  163. self.topics = {}
  164. @pyqtSlot(QString, QString)
  165. def output_handler(self, msg, label):
  166. if label != 'control':
  167. self.rawoutput.append(msg)
  168. if label == self.curlabel:
  169. self.curstrs.append(msg)
  170. else:
  171. if self.curlabel is not None:
  172. self.outputReceived.emit(self.curstrs.join(''), self.curlabel)
  173. self.curstrs = QStringList(msg)
  174. self.curlabel = label
  175. @pyqtSlot(QString, object, QString, QString, object)
  176. def progress_handler(self, topic, pos, item, unit, total):
  177. self.topics[topic] = (pos, item, unit, total)
  178. @pyqtSlot(DataWrapper)
  179. def interact_handler(self, wrapper):
  180. prompt, password, choices, default = wrapper.data
  181. prompt = hglib.tounicode(prompt)
  182. if choices:
  183. dlg = QMessageBox(QMessageBox.Question,
  184. _('TortoiseHg Prompt'), prompt,
  185. QMessageBox.Yes | QMessageBox.Cancel, self.parent())
  186. dlg.setDefaultButton(QMessageBox.Cancel)
  187. dlg.setWindowFlags(Qt.Sheet)
  188. dlg.setWindowModality(Qt.WindowModal)
  189. rmap = {}
  190. for index, choice in enumerate(choices):
  191. button = dlg.addButton(hglib.tounicode(choice),
  192. QMessageBox.ActionRole)
  193. rmap[id(button)] = index
  194. dlg.exec_()
  195. button = dlg.clickedButton()
  196. if button is 0:
  197. result = default
  198. else:
  199. result = rmap[id(button)]
  200. self.responseq.put(result)
  201. else:
  202. mode = password and QLineEdit.Password \
  203. or QLineEdit.Normal
  204. dlg = QInputDialog(self.parent(), Qt.Sheet)
  205. dlg.setWindowModality(Qt.WindowModal)
  206. dlg.setWindowTitle(_('TortoiseHg Prompt'))
  207. dlg.setLabelText(prompt.title())
  208. dlg.setTextEchoMode(mode)
  209. if dlg.exec_():
  210. text = hglib.fromunicode(dlg.textValue())
  211. else:
  212. text = None
  213. self.responseq.put(text)
  214. def run(self):
  215. ui = QtUi(responseq=self.responseq)
  216. ui.sig.writeSignal.connect(self.output_handler,
  217. Qt.QueuedConnection)
  218. ui.sig.progressSignal.connect(self.progress_handler,
  219. Qt.QueuedConnection)
  220. ui.sig.interactSignal.connect(self.interact_handler,
  221. Qt.QueuedConnection)
  222. if self.display:
  223. cmd = '%% hg %s\n' % self.display
  224. else:
  225. cmd = '%% hg %s\n' % ' '.join(self.cmdline)
  226. ui.write(cmd, label='control')
  227. try:
  228. # save thread id in order to terminate by KeyboardInterrupt
  229. self.thread_id = int(QThread.currentThreadId())
  230. for k, v in ui.configitems('defaults'):
  231. ui.setconfig('defaults', k, '')
  232. self.ret = 255
  233. self.ret = dispatch._dispatch(ui, self.cmdline) or 0
  234. except util.Abort, e:
  235. ui.write_err(local._('abort: ') + str(e) + '\n')
  236. if e.hint:
  237. ui.write_err(local._('hint: ') + str(e.hint) + '\n')
  238. except (error.RepoError, urllib2.HTTPError), e:
  239. ui.write_err(str(e) + '\n')
  240. except urllib2.URLError, e:
  241. import ssl
  242. err = str(e)
  243. if isinstance(e.args[0], ssl.SSLError):
  244. parts = e.args[0].strerror.split(':')
  245. if len(parts) == 7:
  246. file, line, level, errno, lib, func, reason = parts
  247. if func == 'SSL3_GET_SERVER_CERTIFICATE':
  248. err = local._('SSL: Server certificate verify failed')
  249. elif errno == '00000000':
  250. err = local._('SSL: unknown error %s:%s') % (file, line)
  251. else:
  252. err = local._('SSL error: %s') % reason
  253. ui.write_err(err + '\n')
  254. except (Exception, OSError, IOError), e:
  255. if 'THGDEBUG' in os.environ:
  256. import traceback
  257. traceback.print_exc()
  258. ui.write_err(str(e) + '\n')
  259. except KeyboardInterrupt:
  260. self.ret = -1
  261. if self.ret == -1:
  262. if self.abortbyuser:
  263. msg = _('[command terminated by user %s]')
  264. else:
  265. msg = _('[command interrupted %s]')
  266. elif self.ret:
  267. msg = _('[command returned code %d %%s]') % int(self.ret)
  268. else:
  269. msg = _('[command completed successfully %s]')
  270. ui.write(msg % time.asctime() + '\n', label='control')