gconsole.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. """!
  2. @package core.gconsole
  3. @brief Command output widgets
  4. Classes:
  5. - goutput::CmdThread
  6. - goutput::GStdout
  7. - goutput::GStderr
  8. - goutput::GConsole
  9. (C) 2007-2012 by the GRASS Development Team
  10. This program is free software under the GNU General Public License
  11. (>=v2). Read the file COPYING that comes with GRASS for details.
  12. @author Michael Barton (Arizona State University)
  13. @author Martin Landa <landa.martin gmail.com>
  14. @author Vaclav Petras <wenzeslaus gmail.com> (refactoring)
  15. @author Anna Kratochvilova <kratochanna gmail.com> (refactoring)
  16. """
  17. import os
  18. import sys
  19. import re
  20. import time
  21. import threading
  22. import Queue
  23. import codecs
  24. import locale
  25. import wx
  26. from wx.lib.newevent import NewEvent
  27. import grass.script as grass
  28. from grass.script import task as gtask
  29. from grass.pydispatch.signal import Signal
  30. from core import globalvar
  31. from core.gcmd import CommandThread, GError, GException
  32. from core.utils import _
  33. from gui_core.forms import GUI
  34. from core.debug import Debug
  35. from core.settings import UserSettings
  36. wxCmdOutput, EVT_CMD_OUTPUT = NewEvent()
  37. wxCmdProgress, EVT_CMD_PROGRESS = NewEvent()
  38. wxCmdRun, EVT_CMD_RUN = NewEvent()
  39. wxCmdDone, EVT_CMD_DONE = NewEvent()
  40. wxCmdAbort, EVT_CMD_ABORT = NewEvent()
  41. wxCmdPrepare, EVT_CMD_PREPARE = NewEvent()
  42. def GrassCmd(cmd, env=None, stdout=None, stderr=None):
  43. """!Return GRASS command thread"""
  44. return CommandThread(cmd, env=env,
  45. stdout=stdout, stderr=stderr)
  46. class CmdThread(threading.Thread):
  47. """!Thread for GRASS commands"""
  48. requestId = 0
  49. def __init__(self, receiver, requestQ=None, resultQ=None, **kwds):
  50. """!
  51. @param receiver event receiver (used in PostEvent)
  52. """
  53. threading.Thread.__init__(self, **kwds)
  54. if requestQ is None:
  55. self.requestQ = Queue.Queue()
  56. else:
  57. self.requestQ = requestQ
  58. if resultQ is None:
  59. self.resultQ = Queue.Queue()
  60. else:
  61. self.resultQ = resultQ
  62. self.setDaemon(True)
  63. self.requestCmd = None
  64. self.receiver = receiver
  65. self._want_abort_all = False
  66. self.start()
  67. def RunCmd(self, *args, **kwds):
  68. """!Run command in queue
  69. @param args unnamed command arguments
  70. @param kwds named command arguments
  71. @return request id in queue
  72. """
  73. CmdThread.requestId += 1
  74. self.requestCmd = None
  75. self.requestQ.put((CmdThread.requestId, args, kwds))
  76. return CmdThread.requestId
  77. def GetId(self):
  78. """!Get id for next command"""
  79. return CmdThread.requestId + 1
  80. def SetId(self, id):
  81. """!Set starting id"""
  82. CmdThread.requestId = id
  83. def run(self):
  84. os.environ['GRASS_MESSAGE_FORMAT'] = 'gui'
  85. while True:
  86. requestId, args, kwds = self.requestQ.get()
  87. for key in ('callable', 'onDone', 'onPrepare', 'userData'):
  88. if key in kwds:
  89. vars()[key] = kwds[key]
  90. del kwds[key]
  91. else:
  92. vars()[key] = None
  93. if not vars()['callable']:
  94. vars()['callable'] = GrassCmd
  95. requestTime = time.time()
  96. # prepare
  97. if self.receiver:
  98. event = wxCmdPrepare(cmd=args[0],
  99. time=requestTime,
  100. pid=requestId,
  101. onPrepare=vars()['onPrepare'],
  102. userData=vars()['userData'])
  103. wx.PostEvent(self.receiver, event)
  104. # run command
  105. event = wxCmdRun(cmd=args[0],
  106. pid=requestId)
  107. wx.PostEvent(self.receiver, event)
  108. time.sleep(.1)
  109. self.requestCmd = vars()['callable'](*args, **kwds)
  110. if self._want_abort_all and self.requestCmd is not None:
  111. self.requestCmd.abort()
  112. if self.requestQ.empty():
  113. self._want_abort_all = False
  114. self.resultQ.put((requestId, self.requestCmd.run()))
  115. try:
  116. returncode = self.requestCmd.module.returncode
  117. except AttributeError:
  118. returncode = 0 # being optimistic
  119. try:
  120. aborted = self.requestCmd.aborted
  121. except AttributeError:
  122. aborted = False
  123. time.sleep(.1)
  124. # set default color table for raster data
  125. if UserSettings.Get(group='rasterLayer',
  126. key='colorTable', subkey='enabled') and \
  127. args[0][0][:2] == 'r.':
  128. colorTable = UserSettings.Get(group='rasterLayer',
  129. key='colorTable',
  130. subkey='selection')
  131. mapName = None
  132. if args[0][0] == 'r.mapcalc':
  133. try:
  134. mapName = args[0][1].split('=', 1)[0].strip()
  135. except KeyError:
  136. pass
  137. else:
  138. moduleInterface = GUI(show=None).ParseCommand(args[0])
  139. outputParam = moduleInterface.get_param(value='output',
  140. raiseError=False)
  141. if outputParam and outputParam['prompt'] == 'raster':
  142. mapName = outputParam['value']
  143. if mapName:
  144. argsColor = list(args)
  145. argsColor[0] = ['r.colors',
  146. 'map=%s' % mapName,
  147. 'color=%s' % colorTable]
  148. self.requestCmdColor = vars()['callable'](*argsColor, **kwds)
  149. self.resultQ.put((requestId, self.requestCmdColor.run()))
  150. if self.receiver:
  151. event = wxCmdDone(cmd=args[0],
  152. aborted=aborted,
  153. returncode=returncode,
  154. time=requestTime,
  155. pid=requestId,
  156. onDone=vars()['onDone'],
  157. userData=vars()['userData'])
  158. # send event
  159. wx.PostEvent(self.receiver, event)
  160. def abort(self, abortall=True):
  161. """!Abort command(s)"""
  162. if abortall:
  163. self._want_abort_all = True
  164. if self.requestCmd is not None:
  165. self.requestCmd.abort()
  166. if self.requestQ.empty():
  167. self._want_abort_all = False
  168. class GStdout:
  169. """!GConsole standard output
  170. Based on FrameOutErr.py
  171. Name: FrameOutErr.py
  172. Purpose: Redirecting stdout / stderr
  173. Author: Jean-Michel Fauth, Switzerland
  174. Copyright: (c) 2005-2007 Jean-Michel Fauth
  175. Licence: GPL
  176. """
  177. def __init__(self, receiver):
  178. """!
  179. @param receiver event receiver (used in PostEvent)
  180. """
  181. self.receiver = receiver
  182. def flush(self):
  183. pass
  184. def write(self, s):
  185. if len(s) == 0 or s == '\n':
  186. return
  187. for line in s.splitlines():
  188. if len(line) == 0:
  189. continue
  190. evt = wxCmdOutput(text=line + '\n',
  191. type='')
  192. wx.PostEvent(self.receiver, evt)
  193. class GStderr:
  194. """!GConsole standard error output
  195. Based on FrameOutErr.py
  196. Name: FrameOutErr.py
  197. Purpose: Redirecting stdout / stderr
  198. Author: Jean-Michel Fauth, Switzerland
  199. Copyright: (c) 2005-2007 Jean-Michel Fauth
  200. Licence: GPL
  201. """
  202. def __init__(self, receiver):
  203. """!
  204. @param receiver event receiver (used in PostEvent)
  205. """
  206. self.receiver = receiver
  207. self.type = ''
  208. self.message = ''
  209. self.printMessage = False
  210. def flush(self):
  211. pass
  212. def write(self, s):
  213. if "GtkPizza" in s:
  214. return
  215. # remove/replace escape sequences '\b' or '\r' from stream
  216. progressValue = -1
  217. for line in s.splitlines():
  218. if len(line) == 0:
  219. continue
  220. if 'GRASS_INFO_PERCENT' in line:
  221. value = int(line.rsplit(':', 1)[1].strip())
  222. if value >= 0 and value < 100:
  223. progressValue = value
  224. else:
  225. progressValue = 0
  226. elif 'GRASS_INFO_MESSAGE' in line:
  227. self.type = 'message'
  228. self.message += line.split(':', 1)[1].strip() + '\n'
  229. elif 'GRASS_INFO_WARNING' in line:
  230. self.type = 'warning'
  231. self.message += line.split(':', 1)[1].strip() + '\n'
  232. elif 'GRASS_INFO_ERROR' in line:
  233. self.type = 'error'
  234. self.message += line.split(':', 1)[1].strip() + '\n'
  235. elif 'GRASS_INFO_END' in line:
  236. self.printMessage = True
  237. elif self.type == '':
  238. if len(line) == 0:
  239. continue
  240. evt = wxCmdOutput(text=line,
  241. type='')
  242. wx.PostEvent(self.receiver, evt)
  243. elif len(line) > 0:
  244. self.message += line.strip() + '\n'
  245. if self.printMessage and len(self.message) > 0:
  246. evt = wxCmdOutput(text=self.message,
  247. type=self.type)
  248. wx.PostEvent(self.receiver, evt)
  249. self.type = ''
  250. self.message = ''
  251. self.printMessage = False
  252. # update progress message
  253. if progressValue > -1:
  254. # self.gmgauge.SetValue(progressValue)
  255. evt = wxCmdProgress(value=progressValue)
  256. wx.PostEvent(self.receiver, evt)
  257. # events related to messages
  258. # TODO: create separete class for handling messages?
  259. gWriteLog, EVT_WRITE_LOG = NewEvent()
  260. gWriteCmdLog, EVT_WRITE_CMD_LOG = NewEvent()
  261. gWriteWarning, EVT_WRITE_WARNING = NewEvent()
  262. gWriteError, EVT_WRITE_ERROR = NewEvent()
  263. # Occurs when an ignored command is called.
  264. # Attribute cmd contains command (as a list).
  265. gIgnoredCmdRun, EVT_IGNORED_CMD_RUN = NewEvent()
  266. # Occurs when important command is called.
  267. # Determined by switchPage and priority parameters of GConsole.RunCmd()
  268. # currently used only for Layer Manager
  269. # because others (forms and gmodeler) just wants to see all commands
  270. # (because commands are the main part of their work)
  271. gImportantCmdRun, EVT_IMPORTANT_CMD_RUN = NewEvent()
  272. class GConsole(wx.EvtHandler):
  273. """!
  274. """
  275. def __init__(self, guiparent=None, giface=None, ignoredCmdPattern=None):
  276. """!
  277. @param guiparent parent window for created GUI objects
  278. @param lmgr layer manager window (TODO: replace by giface)
  279. @param ignoredCmdPattern regular expression specifying commads
  280. to be ignored (e.g. @c '^d\..*' for display commands)
  281. """
  282. wx.EvtHandler.__init__(self)
  283. # Signal when some map is created or updated by a module.
  284. # attributes: name: map name, ltype: map type,
  285. self.mapCreated = Signal('GConsole.mapCreated')
  286. self._guiparent = guiparent
  287. self._giface = giface
  288. self._ignoredCmdPattern = ignoredCmdPattern
  289. # create queues
  290. self.requestQ = Queue.Queue()
  291. self.resultQ = Queue.Queue()
  292. self.cmdOutputTimer = wx.Timer(self)
  293. self.Bind(wx.EVT_TIMER, self.OnProcessPendingOutputWindowEvents)
  294. self.Bind(EVT_CMD_RUN, self.OnCmdRun)
  295. self.Bind(EVT_CMD_DONE, self.OnCmdDone)
  296. self.Bind(EVT_CMD_ABORT, self.OnCmdAbort)
  297. # stream redirection
  298. self.cmdStdOut = GStdout(receiver=self)
  299. self.cmdStdErr = GStderr(receiver=self)
  300. # thread
  301. self.cmdThread = CmdThread(self, self.requestQ, self.resultQ)
  302. def Redirect(self):
  303. """!Redirect stdout/stderr
  304. """
  305. if Debug.GetLevel() == 0 and int(grass.gisenv().get('DEBUG', 0)) == 0:
  306. # don't redirect when debugging is enabled
  307. sys.stdout = self.cmdStdOut
  308. sys.stderr = self.cmdStdErr
  309. else:
  310. enc = locale.getdefaultlocale()[1]
  311. if enc:
  312. sys.stdout = codecs.getwriter(enc)(sys.__stdout__)
  313. sys.stderr = codecs.getwriter(enc)(sys.__stderr__)
  314. else:
  315. sys.stdout = sys.__stdout__
  316. sys.stderr = sys.__stderr__
  317. def WriteLog(self, text, style=None, wrap=None,
  318. switchPage=False, priority=1):
  319. """!Generic method for writing log message in
  320. given style
  321. @param line text line
  322. @param switchPage for backward compatibility
  323. (replace by priority: False=1, True=2)
  324. @param priority priority of this message
  325. (0=no priority, 1=normal, 2=medium, 3=high)
  326. """
  327. event = gWriteLog(text=text, wrap=wrap,
  328. switchPage=switchPage, priority=priority)
  329. wx.PostEvent(self, event)
  330. def WriteCmdLog(self, line, pid=None, switchPage=True):
  331. """!Write message in selected style
  332. @param line message to be printed
  333. @param pid process pid or None
  334. @param switchPage True to switch page
  335. """
  336. event = gWriteCmdLog(line=line, pid=pid,
  337. switchPage=switchPage)
  338. wx.PostEvent(self, event)
  339. def WriteWarning(self, line):
  340. """!Write message in warning style"""
  341. event = gWriteWarning(line=line)
  342. wx.PostEvent(self, event)
  343. def WriteError(self, line):
  344. """!Write message in error style"""
  345. event = gWriteError(line=line)
  346. wx.PostEvent(self, event)
  347. def RunCmd(self, command, compReg=True, switchPage=False, skipInterface=False,
  348. onDone=None, onPrepare=None, userData=None, priority=1):
  349. """!Run command typed into console command prompt (GPrompt).
  350. @todo Document the other event.
  351. @todo Solve problem with the other event
  352. (now uses gOutputText event but there is no text,
  353. use onPrepare handler instead?)
  354. Posts event EVT_IGNORED_CMD_RUN when command which should be ignored
  355. (according to ignoredCmdPattern) is run.
  356. For example, see layer manager which handles d.* on its own.
  357. @todo replace swichPage and priority by parameter 'silent' or 'important'
  358. also possible solution is RunCmdSilently and RunCmdWithoutNotifyingAUser
  359. @param command command given as a list (produced e.g. by utils.split())
  360. @param compReg True use computation region
  361. @param switchPage switch to output page
  362. @param priority command importance - possible replacement for switchPage
  363. @param skipInterface True to do not launch GRASS interface
  364. parser when command has no arguments given
  365. @param onDone function to be called when command is finished
  366. @param onPrepare function to be called before command is launched
  367. @param userData data defined for the command
  368. """
  369. if len(command) == 0:
  370. Debug.msg(2, "GPrompt:RunCmd(): empty command")
  371. return
  372. # update history file
  373. env = grass.gisenv()
  374. try:
  375. filePath = os.path.join(env['GISDBASE'],
  376. env['LOCATION_NAME'],
  377. env['MAPSET'],
  378. '.bash_history')
  379. fileHistory = codecs.open(filePath, encoding='utf-8', mode='a')
  380. except IOError, e:
  381. GError(_("Unable to write file '%(filePath)s'.\n\nDetails: %(error)s") %
  382. {'filePath': filePath, 'error': e},
  383. parent=self._guiparent)
  384. fileHistory = None
  385. if fileHistory:
  386. try:
  387. fileHistory.write(' '.join(command) + os.linesep)
  388. finally:
  389. fileHistory.close()
  390. if command[0] in globalvar.grassCmd:
  391. # send GRASS command without arguments to GUI command interface
  392. # except ignored commands (event is emitted)
  393. if self._ignoredCmdPattern and \
  394. re.compile(self._ignoredCmdPattern).search(' '.join(command)) and \
  395. '--help' not in command and '--ui' not in command:
  396. event = gIgnoredCmdRun(cmd=command)
  397. wx.PostEvent(self, event)
  398. return
  399. else:
  400. # other GRASS commands (r|v|g|...)
  401. try:
  402. task = GUI(show=None).ParseCommand(command)
  403. except GException, e:
  404. GError(parent=self._guiparent,
  405. message=unicode(e),
  406. showTraceback=False)
  407. return
  408. hasParams = False
  409. if task:
  410. options = task.get_options()
  411. hasParams = options['params'] and options['flags']
  412. # check for <input>=-
  413. for p in options['params']:
  414. if p.get('prompt', '') == 'input' and \
  415. p.get('element', '') == 'file' and \
  416. p.get('age', 'new') == 'old' and \
  417. p.get('value', '') == '-':
  418. GError(parent=self._guiparent,
  419. message=_("Unable to run command:\n%(cmd)s\n\n"
  420. "Option <%(opt)s>: read from standard input is not "
  421. "supported by wxGUI") % {'cmd': ' '.join(command),
  422. 'opt': p.get('name', '')})
  423. return
  424. if len(command) == 1 and hasParams and \
  425. command[0] != 'v.krige':
  426. # no arguments given
  427. try:
  428. GUI(parent=self._guiparent, giface=self._giface).ParseCommand(command)
  429. except GException, e:
  430. print >> sys.stderr, e
  431. return
  432. # documenting old behavior/implementation:
  433. # switch and focus if required
  434. # important commad
  435. # TODO: add also user data, cmd, ... to the event?
  436. importantEvent = gImportantCmdRun()
  437. wx.PostEvent(self, importantEvent)
  438. # activate computational region (set with g.region)
  439. # for all non-display commands.
  440. if compReg:
  441. tmpreg = os.getenv("GRASS_REGION")
  442. if "GRASS_REGION" in os.environ:
  443. del os.environ["GRASS_REGION"]
  444. # process GRASS command with argument
  445. self.cmdThread.RunCmd(command,
  446. stdout=self.cmdStdOut,
  447. stderr=self.cmdStdErr,
  448. onDone=onDone, onPrepare=onPrepare,
  449. userData=userData,
  450. env=os.environ.copy())
  451. self.cmdOutputTimer.Start(50)
  452. # deactivate computational region and return to display settings
  453. if compReg and tmpreg:
  454. os.environ["GRASS_REGION"] = tmpreg
  455. else:
  456. # Send any other command to the shell. Send output to
  457. # console output window
  458. if len(command) == 1 and not skipInterface:
  459. try:
  460. task = gtask.parse_interface(command[0])
  461. except:
  462. task = None
  463. else:
  464. task = None
  465. if task:
  466. # process GRASS command without argument
  467. GUI(parent=self._guiparent, giface=self._giface).ParseCommand(command)
  468. else:
  469. self.cmdThread.RunCmd(command,
  470. stdout=self.cmdStdOut,
  471. stderr=self.cmdStdErr,
  472. onDone=onDone, onPrepare=onPrepare,
  473. userData=userData)
  474. self.cmdOutputTimer.Start(50)
  475. def GetLog(self, err=False):
  476. """!Get widget used for logging
  477. @todo what's this?
  478. @param err True to get stderr widget
  479. """
  480. if err:
  481. return self.cmdStdErr
  482. return self.cmdStdOut
  483. def GetCmd(self):
  484. """!Get running command or None"""
  485. return self.requestQ.get()
  486. def OnCmdAbort(self, event):
  487. """!Abort running command"""
  488. self.cmdThread.abort()
  489. event.Skip()
  490. def OnCmdRun(self, event):
  491. """!Run command"""
  492. self.WriteCmdLog('(%s)\n%s' % (str(time.ctime()), ' '.join(event.cmd)))
  493. event.Skip()
  494. def OnCmdDone(self, event):
  495. """!Command done (or aborted)
  496. Sends signal mapCreated if map is recognized in output parameters.
  497. """
  498. # Process results here
  499. try:
  500. ctime = time.time() - event.time
  501. if ctime < 60:
  502. stime = _("%d sec") % int(ctime)
  503. else:
  504. mtime = int(ctime / 60)
  505. stime = _("%(min)d min %(sec)d sec") % {'min': mtime,
  506. 'sec': int(ctime - (mtime * 60))}
  507. except KeyError:
  508. # stopped deamon
  509. stime = _("unknown")
  510. if event.aborted:
  511. # Thread aborted (using our convention of None return)
  512. self.WriteWarning(_('Please note that the data are left in'
  513. ' inconsistent state and may be corrupted'))
  514. msg = _('Command aborted')
  515. else:
  516. msg = _('Command finished')
  517. self.WriteCmdLog('(%s) %s (%s)' % (str(time.ctime()), msg, stime))
  518. if event.onDone:
  519. event.onDone(cmd=event.cmd, returncode=event.returncode)
  520. self.cmdOutputTimer.Stop()
  521. if event.cmd[0] == 'g.gisenv':
  522. Debug.SetLevel()
  523. self.Redirect()
  524. # do nothing when no map added
  525. if event.returncode != 0 or event.aborted:
  526. event.Skip()
  527. return
  528. if event.cmd[0] not in globalvar.grassCmd:
  529. return
  530. # find which maps were created
  531. try:
  532. task = GUI(show=None).ParseCommand(event.cmd)
  533. except GException, e:
  534. print >> sys.stderr, e
  535. task = None
  536. return
  537. for p in task.get_options()['params']:
  538. prompt = p.get('prompt', '')
  539. if prompt in ('raster', 'vector', '3d-raster') and \
  540. p.get('age', 'old') == 'new' and \
  541. p.get('value', None):
  542. name = p.get('value')
  543. if '@' not in name:
  544. name = name + '@' + grass.gisenv()['MAPSET']
  545. self.mapCreated.emit(name=name, ltype=prompt)
  546. event.Skip()
  547. def OnProcessPendingOutputWindowEvents(self, event):
  548. wx.GetApp().ProcessPendingEvents()