gconsole.py 22 KB

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