gconsole.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  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 ignored command is called.
  254. # Attribute cmd contains command (as a list).
  255. gIgnoredCmdRun, EVT_IGNORED_CMD_RUN = NewEvent()
  256. class GConsole(wx.EvtHandler):
  257. """!
  258. """
  259. def __init__(self, guiparent=None, lmgr=None, ignoredCmdPattern=None):
  260. """!
  261. @param guiparent parent window for created GUI objects
  262. @param lmgr layer manager window (TODO: replace by giface)
  263. @param ignoredCmdPattern regular expression specifying commads
  264. to be ignored (e.g. @c '^d\..*' for display commands)
  265. """
  266. wx.EvtHandler.__init__(self)
  267. self._guiparent = guiparent
  268. self._lmgr = lmgr
  269. self._ignoredCmdPattern = ignoredCmdPattern
  270. # create queues
  271. self.requestQ = Queue.Queue()
  272. self.resultQ = Queue.Queue()
  273. self.cmdOutputTimer = wx.Timer(self)
  274. self.Bind(wx.EVT_TIMER, self.OnProcessPendingOutputWindowEvents)
  275. self.Bind(EVT_CMD_RUN, self.OnCmdRun)
  276. self.Bind(EVT_CMD_DONE, self.OnCmdDone)
  277. self.Bind(EVT_CMD_ABORT, self.OnCmdAbort)
  278. # stream redirection
  279. self.cmdStdOut = GStdout(receiver=self)
  280. self.cmdStdErr = GStderr(receiver=self)
  281. # thread
  282. self.cmdThread = CmdThread(self, self.requestQ, self.resultQ)
  283. def Redirect(self):
  284. """!Redirect stdout/stderr
  285. """
  286. if Debug.GetLevel() == 0 and int(grass.gisenv().get('DEBUG', 0)) == 0:
  287. # don't redirect when debugging is enabled
  288. sys.stdout = self.cmdStdOut
  289. sys.stderr = self.cmdStdErr
  290. else:
  291. enc = locale.getdefaultlocale()[1]
  292. if enc:
  293. sys.stdout = codecs.getwriter(enc)(sys.__stdout__)
  294. sys.stderr = codecs.getwriter(enc)(sys.__stderr__)
  295. else:
  296. sys.stdout = sys.__stdout__
  297. sys.stderr = sys.__stderr__
  298. def WriteLog(self, text, style=None, wrap=None,
  299. switchPage=False, priority=1):
  300. """!Generic method for writing log message in
  301. given style
  302. @param line text line
  303. @param switchPage for backward compatibility
  304. (replace by priority: False=1, True=2)
  305. @param priority priority of this message
  306. (0=no priority, 1=normal, 2=medium, 3=high)
  307. """
  308. event = gWriteLog(text=text, wrap=wrap,
  309. switchPage=switchPage, priority=priority)
  310. wx.PostEvent(self, event)
  311. def WriteCmdLog(self, line, pid=None, switchPage=True):
  312. """!Write message in selected style
  313. @param line message to be printed
  314. @param pid process pid or None
  315. @param switchPage True to switch page
  316. """
  317. event = gWriteCmdLog(line=line, pid=pid,
  318. switchPage=switchPage)
  319. wx.PostEvent(self, event)
  320. def WriteWarning(self, line):
  321. """!Write message in warning style"""
  322. event = gWriteWarning(line=line)
  323. wx.PostEvent(self, event)
  324. def WriteError(self, line):
  325. """!Write message in error style"""
  326. event = gWriteError(line=line)
  327. wx.PostEvent(self, event)
  328. def RunCmd(self, command, compReg=True, switchPage=False, skipInterface=False,
  329. onDone=None, onPrepare=None, userData=None):
  330. """!Run command typed into console command prompt (GPrompt).
  331. @todo Document the other event.
  332. @todo Solve problem with the other event
  333. (now uses gOutputText event but there is no text,
  334. use onPrepare handler instead?)
  335. Posts event EVT_IGNORED_CMD_RUN when command which should be ignored
  336. (according to ignoredCmdPattern) is run.
  337. For example, see layer manager which handles d.* on its own.
  338. @todo all switchPage and priority params are currently broken in this class)
  339. @param command command given as a list (produced e.g. by utils.split())
  340. @param compReg True use computation region
  341. @param switchPage switch to output page
  342. @param skipInterface True to do not launch GRASS interface
  343. parser when command has no arguments given
  344. @param onDone function to be called when command is finished
  345. @param onPrepare function to be called before command is launched
  346. @param userData data defined for the command
  347. """
  348. if len(command) == 0:
  349. Debug.msg(2, "GPrompt:RunCmd(): empty command")
  350. return
  351. # update history file
  352. env = grass.gisenv()
  353. try:
  354. filePath = os.path.join(env['GISDBASE'],
  355. env['LOCATION_NAME'],
  356. env['MAPSET'],
  357. '.bash_history')
  358. fileHistory = codecs.open(filePath, encoding='utf-8', mode='a')
  359. except IOError, e:
  360. GError(_("Unable to write file '%(filePath)s'.\n\nDetails: %(error)s") %
  361. {'filePath': filePath, 'error': e},
  362. parent=self._guiparent)
  363. fileHistory = None
  364. if fileHistory:
  365. try:
  366. fileHistory.write(' '.join(command) + os.linesep)
  367. finally:
  368. fileHistory.close()
  369. if command[0] in globalvar.grassCmd:
  370. # send GRASS command without arguments to GUI command interface
  371. # except ignored commands (event is emitted)
  372. if self._ignoredCmdPattern and \
  373. re.compile(self._ignoredCmdPattern).search(' '.join(command)) and \
  374. '--help' not in command:
  375. event = gIgnoredCmdRun(cmd=command)
  376. wx.PostEvent(self, event)
  377. return
  378. else:
  379. # other GRASS commands (r|v|g|...)
  380. try:
  381. task = GUI(show=None).ParseCommand(command)
  382. except GException, e:
  383. GError(parent=self._guiparent,
  384. message=unicode(e),
  385. showTraceback=False)
  386. return
  387. hasParams = False
  388. if task:
  389. options = task.get_options()
  390. hasParams = options['params'] and options['flags']
  391. # check for <input>=-
  392. for p in options['params']:
  393. if p.get('prompt', '') == 'input' and \
  394. p.get('element', '') == 'file' and \
  395. p.get('age', 'new') == 'old' and \
  396. p.get('value', '') == '-':
  397. GError(parent=self._guiparent,
  398. message=_("Unable to run command:\n%(cmd)s\n\n"
  399. "Option <%(opt)s>: read from standard input is not "
  400. "supported by wxGUI") % {'cmd': ' '.join(command),
  401. 'opt': p.get('name', '')})
  402. return
  403. if len(command) == 1 and hasParams and \
  404. command[0] != 'v.krige':
  405. # no arguments given
  406. try:
  407. GUI(parent = self._guiparent, lmgr = self._lmgr).ParseCommand(command)
  408. except GException, e:
  409. print >> sys.stderr, e
  410. return
  411. # documenting old behavior/implementation:
  412. # switch and focus if required
  413. # TODO: this probably should be run command event
  414. # TODO: this should be solved by the user using userData and onDone
  415. # activate computational region (set with g.region)
  416. # for all non-display commands.
  417. if compReg:
  418. tmpreg = os.getenv("GRASS_REGION")
  419. if "GRASS_REGION" in os.environ:
  420. del os.environ["GRASS_REGION"]
  421. # process GRASS command with argument
  422. self.cmdThread.RunCmd(command,
  423. stdout=self.cmdStdOut,
  424. stderr=self.cmdStdErr,
  425. onDone=onDone, onPrepare=onPrepare,
  426. userData=userData,
  427. env=os.environ.copy())
  428. self.cmdOutputTimer.Start(50)
  429. # deactivate computational region and return to display settings
  430. if compReg and tmpreg:
  431. os.environ["GRASS_REGION"] = tmpreg
  432. else:
  433. # Send any other command to the shell. Send output to
  434. # console output window
  435. if len(command) == 1 and not skipInterface:
  436. try:
  437. task = gtask.parse_interface(command[0])
  438. except:
  439. task = None
  440. else:
  441. task = None
  442. if task:
  443. # process GRASS command without argument
  444. GUI(parent=self._guiparent, lmgr=self._lmgr).ParseCommand(command)
  445. else:
  446. self.cmdThread.RunCmd(command,
  447. stdout=self.cmdStdOut,
  448. stderr=self.cmdStdErr,
  449. onDone=onDone, onPrepare=onPrepare,
  450. userData=userData)
  451. self.cmdOutputTimer.Start(50)
  452. def GetLog(self, err=False):
  453. """!Get widget used for logging
  454. @todo what's this?
  455. @param err True to get stderr widget
  456. """
  457. if err:
  458. return self.cmdStdErr
  459. return self.cmdStdOut
  460. def GetCmd(self):
  461. """!Get running command or None"""
  462. return self.requestQ.get()
  463. def OnCmdAbort(self, event):
  464. """!Abort running command"""
  465. self.cmdThread.abort()
  466. event.Skip()
  467. def OnCmdRun(self, event):
  468. """!Run command"""
  469. self.WriteCmdLog('(%s)\n%s' % (str(time.ctime()), ' '.join(event.cmd)))
  470. event.Skip()
  471. def OnCmdDone(self, event):
  472. """!Command done (or aborted)
  473. Posts event EVT_MAP_CREATED.
  474. """
  475. # Process results here
  476. try:
  477. ctime = time.time() - event.time
  478. if ctime < 60:
  479. stime = _("%d sec") % int(ctime)
  480. else:
  481. mtime = int(ctime / 60)
  482. stime = _("%(min)d min %(sec)d sec") % {'min': mtime,
  483. 'sec': int(ctime - (mtime * 60))}
  484. except KeyError:
  485. # stopped deamon
  486. stime = _("unknown")
  487. if event.aborted:
  488. # Thread aborted (using our convention of None return)
  489. self.WriteWarning(_('Please note that the data are left in'
  490. ' inconsistent state and may be corrupted'))
  491. msg = _('Command aborted')
  492. else:
  493. msg = _('Command finished')
  494. self.WriteCmdLog('(%s) %s (%s)' % (str(time.ctime()), msg, stime))
  495. if event.onDone:
  496. event.onDone(cmd=event.cmd, returncode=event.returncode)
  497. self.cmdOutputTimer.Stop()
  498. if event.cmd[0] == 'g.gisenv':
  499. Debug.SetLevel()
  500. self.Redirect()
  501. # do nothing when no map added
  502. if event.returncode != 0 or event.aborted:
  503. event.Skip()
  504. return
  505. # find which maps were created
  506. try:
  507. task = GUI(show=None).ParseCommand(event.cmd)
  508. except GException, e:
  509. print >> sys.stderr, e
  510. task = None
  511. return
  512. for p in task.get_options()['params']:
  513. prompt = p.get('prompt', '')
  514. if prompt in ('raster', 'vector', '3d-raster') and \
  515. p.get('age', 'old') == 'new' and \
  516. p.get('value', None):
  517. name = p.get('value')
  518. if '@' not in name:
  519. name = name + '@' + grass.gisenv()['MAPSET']
  520. mapEvent = gMapCreated(wx.ID_ANY,
  521. name=name, ltype=prompt, add=None)
  522. wx.PostEvent(self, mapEvent)
  523. event.Skip()
  524. def OnProcessPendingOutputWindowEvents(self, event):
  525. wx.GetApp().ProcessPendingEvents()