gconsole.py 22 KB

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