gconsole.py 23 KB

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