source: grass/trunk/gui/wxpython/core/gconsole.py

Last change on this file was 74307, checked in by neteler, 5 years ago

i18N: cleanup gettext usage for Python code (fixes #3790) (contributed by pmav99)

  • Property svn:eol-style set to native
  • Property svn:mime-type set to text/x-python
File size: 27.0 KB
Line 
1"""
2@package core.gconsole
3
4@brief Command output widgets
5
6Classes:
7 - goutput::CmdThread
8 - goutput::GStdout
9 - goutput::GStderr
10 - goutput::GConsole
11
12(C) 2007-2015 by the GRASS Development Team
13
14This program is free software under the GNU General Public License
15(>=v2). Read the file COPYING that comes with GRASS for details.
16
17@author Michael Barton (Arizona State University)
18@author Martin Landa <landa.martin gmail.com>
19@author Vaclav Petras <wenzeslaus gmail.com> (refactoring)
20@author Anna Kratochvilova <kratochanna gmail.com> (refactoring)
21"""
22
23from __future__ import print_function
24
25import os
26import sys
27import re
28import time
29import threading
30
31if sys.version_info.major == 2:
32 import Queue
33else:
34 import queue as Queue
35
36import codecs
37import locale
38
39import wx
40from wx.lib.newevent import NewEvent
41
42import grass.script as grass
43from grass.script import task as gtask
44
45from grass.pydispatch.signal import Signal
46
47from core import globalvar
48from core.gcmd import CommandThread, GError, GException
49from gui_core.forms import GUI
50from core.debug import Debug
51from core.settings import UserSettings
52from core.giface import Notification
53from gui_core.widgets import FormNotebook
54
55wxCmdOutput, EVT_CMD_OUTPUT = NewEvent()
56wxCmdProgress, EVT_CMD_PROGRESS = NewEvent()
57wxCmdRun, EVT_CMD_RUN = NewEvent()
58wxCmdDone, EVT_CMD_DONE = NewEvent()
59wxCmdAbort, EVT_CMD_ABORT = NewEvent()
60wxCmdPrepare, EVT_CMD_PREPARE = NewEvent()
61
62
63def GrassCmd(cmd, env=None, stdout=None, stderr=None):
64 """Return GRASS command thread"""
65 return CommandThread(cmd, env=env,
66 stdout=stdout, stderr=stderr)
67
68
69class CmdThread(threading.Thread):
70 """Thread for GRASS commands"""
71 requestId = 0
72
73 def __init__(self, receiver, requestQ=None, resultQ=None, **kwds):
74 """
75 :param receiver: event receiver (used in PostEvent)
76 """
77 threading.Thread.__init__(self, **kwds)
78
79 if requestQ is None:
80 self.requestQ = Queue.Queue()
81 else:
82 self.requestQ = requestQ
83
84 if resultQ is None:
85 self.resultQ = Queue.Queue()
86 else:
87 self.resultQ = resultQ
88
89 self.setDaemon(True)
90
91 self.requestCmd = None
92
93 self.receiver = receiver
94 self._want_abort_all = False
95
96 self.start()
97
98 def RunCmd(self, *args, **kwds):
99 """Run command in queue
100
101 :param args: unnamed command arguments
102 :param kwds: named command arguments
103
104 :return: request id in queue
105 """
106 CmdThread.requestId += 1
107
108 self.requestCmd = None
109 self.requestQ.put((CmdThread.requestId, args, kwds))
110
111 return CmdThread.requestId
112
113 def GetId(self):
114 """Get id for next command"""
115 return CmdThread.requestId + 1
116
117 def SetId(self, id):
118 """Set starting id"""
119 CmdThread.requestId = id
120
121 def run(self):
122 os.environ['GRASS_MESSAGE_FORMAT'] = 'gui'
123 while True:
124 requestId, args, kwds = self.requestQ.get()
125 for key in ('callable', 'onDone', 'onPrepare',
126 'userData', 'addLayer', 'notification'):
127 if key in kwds:
128 vars()[key] = kwds[key]
129 del kwds[key]
130 else:
131 vars()[key] = None
132
133 if not vars()['callable']:
134 vars()['callable'] = GrassCmd
135
136 requestTime = time.time()
137
138 # prepare
139 if self.receiver:
140 event = wxCmdPrepare(cmd=args[0],
141 time=requestTime,
142 pid=requestId,
143 onPrepare=vars()['onPrepare'],
144 userData=vars()['userData'])
145
146 wx.PostEvent(self.receiver, event)
147
148 # run command
149 event = wxCmdRun(cmd=args[0],
150 pid=requestId,
151 notification=vars()['notification'])
152
153 wx.PostEvent(self.receiver, event)
154
155 time.sleep(.1)
156 self.requestCmd = vars()['callable'](*args, **kwds)
157 if self._want_abort_all and self.requestCmd is not None:
158 self.requestCmd.abort()
159 if self.requestQ.empty():
160 self._want_abort_all = False
161
162 self.resultQ.put((requestId, self.requestCmd.run()))
163
164 try:
165 returncode = self.requestCmd.module.returncode
166 except AttributeError:
167 returncode = 0 # being optimistic
168
169 try:
170 aborted = self.requestCmd.aborted
171 except AttributeError:
172 aborted = False
173
174 time.sleep(.1)
175
176 # set default color table for raster data
177 if UserSettings.Get(group='rasterLayer',
178 key='colorTable', subkey='enabled') and \
179 args[0][0][:2] == 'r.':
180 colorTable = UserSettings.Get(group='rasterLayer',
181 key='colorTable',
182 subkey='selection')
183 mapName = None
184 if args[0][0] == 'r.mapcalc':
185 try:
186 mapName = args[0][1].split('=', 1)[0].strip()
187 except KeyError:
188 pass
189 else:
190 moduleInterface = GUI(show=None).ParseCommand(args[0])
191 outputParam = moduleInterface.get_param(value='output',
192 raiseError=False)
193 if outputParam and outputParam['prompt'] == 'raster':
194 mapName = outputParam['value']
195
196 if mapName:
197 argsColor = list(args)
198 argsColor[0] = ['r.colors',
199 'map=%s' % mapName,
200 'color=%s' % colorTable]
201 self.requestCmdColor = vars()['callable'](
202 *argsColor, **kwds)
203 self.resultQ.put((requestId, self.requestCmdColor.run()))
204
205 if self.receiver:
206 event = wxCmdDone(cmd=args[0],
207 aborted=aborted,
208 returncode=returncode,
209 time=requestTime,
210 pid=requestId,
211 onDone=vars()['onDone'],
212 userData=vars()['userData'],
213 addLayer=vars()['addLayer'],
214 notification=vars()['notification'])
215
216 # send event
217 wx.PostEvent(self.receiver, event)
218
219 def abort(self, abortall=True):
220 """Abort command(s)"""
221 if abortall:
222 self._want_abort_all = True
223 if self.requestCmd is not None:
224 self.requestCmd.abort()
225 if self.requestQ.empty():
226 self._want_abort_all = False
227
228
229class GStdout:
230 """GConsole standard output
231
232 Based on FrameOutErr.py
233
234 Name: FrameOutErr.py
235 Purpose: Redirecting stdout / stderr
236 Author: Jean-Michel Fauth, Switzerland
237 Copyright: (c) 2005-2007 Jean-Michel Fauth
238 Licence: GPL
239 """
240
241 def __init__(self, receiver):
242 """
243 :param receiver: event receiver (used in PostEvent)
244 """
245 self.receiver = receiver
246
247 def flush(self):
248 pass
249
250 def write(self, s):
251 if len(s) == 0 or s == '\n':
252 return
253
254 for line in s.splitlines():
255 if len(line) == 0:
256 continue
257
258 evt = wxCmdOutput(text=line + '\n',
259 type='')
260 wx.PostEvent(self.receiver, evt)
261
262
263class GStderr:
264 """GConsole standard error output
265
266 Based on FrameOutErr.py
267
268 Name: FrameOutErr.py
269 Purpose: Redirecting stdout / stderr
270 Author: Jean-Michel Fauth, Switzerland
271 Copyright: (c) 2005-2007 Jean-Michel Fauth
272 Licence: GPL
273 """
274
275 def __init__(self, receiver):
276 """
277 :param receiver: event receiver (used in PostEvent)
278 """
279 self.receiver = receiver
280 self.type = ''
281 self.message = ''
282 self.printMessage = False
283
284 def flush(self):
285 pass
286
287 def write(self, s):
288 if "GtkPizza" in s:
289 return
290
291 # remove/replace escape sequences '\b' or '\r' from stream
292 progressValue = -1
293
294 for line in s.splitlines():
295 if len(line) == 0:
296 continue
297
298 if 'GRASS_INFO_PERCENT' in line:
299 value = int(line.rsplit(':', 1)[1].strip())
300 if value >= 0 and value < 100:
301 progressValue = value
302 else:
303 progressValue = 0
304 elif 'GRASS_INFO_MESSAGE' in line:
305 self.type = 'message'
306 self.message += line.split(':', 1)[1].strip() + '\n'
307 elif 'GRASS_INFO_WARNING' in line:
308 self.type = 'warning'
309 self.message += line.split(':', 1)[1].strip() + '\n'
310 elif 'GRASS_INFO_ERROR' in line:
311 self.type = 'error'
312 self.message += line.split(':', 1)[1].strip() + '\n'
313 elif 'GRASS_INFO_END' in line:
314 self.printMessage = True
315 elif self.type == '':
316 if len(line) == 0:
317 continue
318 evt = wxCmdOutput(text=line,
319 type='')
320 wx.PostEvent(self.receiver, evt)
321 elif len(line) > 0:
322 self.message += line.strip() + '\n'
323
324 if self.printMessage and len(self.message) > 0:
325 evt = wxCmdOutput(text=self.message,
326 type=self.type)
327 wx.PostEvent(self.receiver, evt)
328
329 self.type = ''
330 self.message = ''
331 self.printMessage = False
332
333 # update progress message
334 if progressValue > -1:
335 # self.gmgauge.SetValue(progressValue)
336 evt = wxCmdProgress(value=progressValue)
337 wx.PostEvent(self.receiver, evt)
338
339
340# Occurs when an ignored command is called.
341# Attribute cmd contains command (as a list).
342gIgnoredCmdRun, EVT_IGNORED_CMD_RUN = NewEvent()
343
344
345class GConsole(wx.EvtHandler):
346 """
347 """
348
349 def __init__(self, guiparent=None, giface=None, ignoredCmdPattern=None):
350 """
351 :param guiparent: parent window for created GUI objects
352 :param lmgr: layer manager window (TODO: replace by giface)
353 :param ignoredCmdPattern: regular expression specifying commads
354 to be ignored (e.g. @c '^d\..*' for
355 display commands)
356 """
357 wx.EvtHandler.__init__(self)
358
359 # Signal when some map is created or updated by a module.
360 # attributes: name: map name, ltype: map type,
361 self.mapCreated = Signal('GConsole.mapCreated')
362 # emitted when map display should be re-render
363 self.updateMap = Signal('GConsole.updateMap')
364 # emitted when log message should be written
365 self.writeLog = Signal('GConsole.writeLog')
366 # emitted when command log message should be written
367 self.writeCmdLog = Signal('GConsole.writeCmdLog')
368 # emitted when warning message should be written
369 self.writeWarning = Signal('GConsole.writeWarning')
370 # emitted when error message should be written
371 self.writeError = Signal('GConsole.writeError')
372
373 self._guiparent = guiparent
374 self._giface = giface
375 self._ignoredCmdPattern = ignoredCmdPattern
376
377 # create queues
378 self.requestQ = Queue.Queue()
379 self.resultQ = Queue.Queue()
380
381 self.cmdOutputTimer = wx.Timer(self)
382 self.Bind(wx.EVT_TIMER, self.OnProcessPendingOutputWindowEvents)
383 self.Bind(EVT_CMD_RUN, self.OnCmdRun)
384 self.Bind(EVT_CMD_DONE, self.OnCmdDone)
385 self.Bind(EVT_CMD_ABORT, self.OnCmdAbort)
386
387 # stream redirection
388 self.cmdStdOut = GStdout(receiver=self)
389 self.cmdStdErr = GStderr(receiver=self)
390
391 # thread
392 self.cmdThread = CmdThread(self, self.requestQ, self.resultQ)
393
394 def Redirect(self):
395 """Redirect stdout/stderr
396 """
397 if Debug.GetLevel() == 0 and grass.debug_level(force=True) == 0:
398 # don't redirect when debugging is enabled
399 sys.stdout = self.cmdStdOut
400 sys.stderr = self.cmdStdErr
401 else:
402 enc = locale.getdefaultlocale()[1]
403 if enc:
404 if sys.version_info.major == 2:
405 sys.stdout = codecs.getwriter(enc)(sys.__stdout__)
406 sys.stderr = codecs.getwriter(enc)(sys.__stderr__)
407 else:
408 # https://stackoverflow.com/questions/4374455/how-to-set-sys-stdout-encoding-in-python-3
409 sys.stdout = codecs.getwriter(enc)(sys.__stdout__.detach())
410 sys.stderr = codecs.getwriter(enc)(sys.__stderr__.detach())
411 else:
412 sys.stdout = sys.__stdout__
413 sys.stderr = sys.__stderr__
414
415 def WriteLog(self, text, style=None, wrap=None,
416 notification=Notification.HIGHLIGHT):
417 """Generic method for writing log message in
418 given style
419
420 :param text: text line
421 :param notification: form of notification
422 """
423 self.writeLog.emit(text=text, wrap=wrap,
424 notification=notification)
425
426 def WriteCmdLog(self, text, pid=None,
427 notification=Notification.MAKE_VISIBLE):
428 """Write message in selected style
429
430 :param text: message to be printed
431 :param pid: process pid or None
432 :param notification: form of notification
433 """
434 self.writeCmdLog.emit(text=text, pid=pid,
435 notification=notification)
436
437 def WriteWarning(self, text):
438 """Write message in warning style"""
439 self.writeWarning.emit(text=text)
440
441 def WriteError(self, text):
442 """Write message in error style"""
443 self.writeError.emit(text=text)
444
445 def RunCmd(self, command, compReg=True, env=None, skipInterface=False,
446 onDone=None, onPrepare=None, userData=None, addLayer=None,
447 notification=Notification.MAKE_VISIBLE):
448 """Run command typed into console command prompt (GPrompt).
449
450 .. todo::
451 Document the other event.
452 .. todo::
453 Solve problem with the other event (now uses gOutputText
454 event but there is no text, use onPrepare handler instead?)
455 .. todo::
456 Skip interface is ignored and determined always automatically.
457
458 Posts event EVT_IGNORED_CMD_RUN when command which should be ignored
459 (according to ignoredCmdPattern) is run.
460 For example, see layer manager which handles d.* on its own.
461
462 :param command: command given as a list (produced e.g. by utils.split())
463 :param compReg: True use computation region
464 :param notification: form of notification
465 :param bool skipInterface: True to do not launch GRASS interface
466 parser when command has no arguments
467 given
468 :param onDone: function to be called when command is finished
469 :param onPrepare: function to be called before command is launched
470 :param addLayer: to be passed in the mapCreated signal
471 :param userData: data defined for the command
472 """
473 if len(command) == 0:
474 Debug.msg(2, "GPrompt:RunCmd(): empty command")
475 return
476
477 # update history file
478 self.UpdateHistoryFile(' '.join(command))
479
480 if command[0] in globalvar.grassCmd:
481 # send GRASS command without arguments to GUI command interface
482 # except ignored commands (event is emitted)
483 if self._ignoredCmdPattern and \
484 re.compile(self._ignoredCmdPattern).search(' '.join(command)) and \
485 '--help' not in command and '--ui' not in command:
486 event = gIgnoredCmdRun(cmd=command)
487 wx.PostEvent(self, event)
488 return
489
490 else:
491 # other GRASS commands (r|v|g|...)
492 try:
493 task = GUI(show=None).ParseCommand(command)
494 except GException as e:
495 GError(parent=self._guiparent,
496 message=unicode(e),
497 showTraceback=False)
498 return
499
500 hasParams = False
501 if task:
502 options = task.get_options()
503 hasParams = options['params'] and options['flags']
504 # check for <input>=-
505 for p in options['params']:
506 if p.get('prompt', '') == 'input' and \
507 p.get('element', '') == 'file' and \
508 p.get('age', 'new') == 'old' and \
509 p.get('value', '') == '-':
510 GError(
511 parent=self._guiparent,
512 message=_(
513 "Unable to run command:\n%(cmd)s\n\n"
514 "Option <%(opt)s>: read from standard input is not "
515 "supported by wxGUI") % {
516 'cmd': ' '.join(command),
517 'opt': p.get(
518 'name',
519 '')})
520 return
521
522 if len(command) == 1:
523 if command[0].startswith('g.gui.'):
524 import imp
525 import inspect
526 pyFile = command[0]
527 if sys.platform == 'win32':
528 pyFile += '.py'
529 pyPath = os.path.join(
530 os.environ['GISBASE'], 'scripts', pyFile)
531 if not os.path.exists(pyPath):
532 pyPath = os.path.join(
533 os.environ['GRASS_ADDON_BASE'], 'scripts', pyFile)
534 if not os.path.exists(pyPath):
535 GError(
536 parent=self._guiparent,
537 message=_("Module <%s> not found.") %
538 command[0])
539 pymodule = imp.load_source(
540 command[0].replace('.', '_'), pyPath)
541 pymain = inspect.getargspec(pymodule.main)
542 if pymain and 'giface' in pymain.args:
543 pymodule.main(self._giface)
544 return
545
546 # no arguments given
547 if hasParams and \
548 not isinstance(self._guiparent, FormNotebook):
549 # also parent must be checked, see #3135 for details
550 try:
551 GUI(parent=self._guiparent,
552 giface=self._giface).ParseCommand(command)
553 except GException as e:
554 print(e, file=sys.stderr)
555
556 return
557
558 if env:
559 env = env.copy()
560 else:
561 env = os.environ.copy()
562 # activate computational region (set with g.region)
563 # for all non-display commands.
564 if compReg and "GRASS_REGION" in env:
565 del env["GRASS_REGION"]
566
567 # process GRASS command with argument
568 self.cmdThread.RunCmd(command,
569 stdout=self.cmdStdOut,
570 stderr=self.cmdStdErr,
571 onDone=onDone, onPrepare=onPrepare,
572 userData=userData, addLayer=addLayer,
573 env=env,
574 notification=notification)
575 self.cmdOutputTimer.Start(50)
576
577 # we don't need to change computational region settings
578 # because we work on a copy
579 else:
580 # Send any other command to the shell. Send output to
581 # console output window
582 #
583 # Check if the script has an interface (avoid double-launching
584 # of the script)
585
586 # check if we ignore the command (similar to grass commands part)
587 if self._ignoredCmdPattern and \
588 re.compile(self._ignoredCmdPattern).search(' '.join(command)):
589 event = gIgnoredCmdRun(cmd=command)
590 wx.PostEvent(self, event)
591 return
592
593 skipInterface = True
594 if os.path.splitext(command[0])[1] in ('.py', '.sh'):
595 try:
596 sfile = open(command[0], "r")
597 for line in sfile.readlines():
598 if len(line) < 2:
599 continue
600 if line[0] is '#' and line[1] is '%':
601 skipInterface = False
602 break
603 sfile.close()
604 except IOError:
605 pass
606
607 if len(command) == 1 and not skipInterface:
608 try:
609 task = gtask.parse_interface(command[0])
610 except:
611 task = None
612 else:
613 task = None
614
615 if task:
616 # process GRASS command without argument
617 GUI(parent=self._guiparent,
618 giface=self._giface).ParseCommand(command)
619 else:
620 self.cmdThread.RunCmd(command,
621 stdout=self.cmdStdOut,
622 stderr=self.cmdStdErr,
623 onDone=onDone, onPrepare=onPrepare,
624 userData=userData, addLayer=addLayer,
625 env=env,
626 notification=notification)
627 self.cmdOutputTimer.Start(50)
628
629 def GetLog(self, err=False):
630 """Get widget used for logging
631
632 .. todo::
633 what's this?
634
635 :param bool err: True to get stderr widget
636 """
637 if err:
638 return self.cmdStdErr
639
640 return self.cmdStdOut
641
642 def GetCmd(self):
643 """Get running command or None"""
644 return self.requestQ.get()
645
646 def OnCmdAbort(self, event):
647 """Abort running command"""
648 self.cmdThread.abort()
649 event.Skip()
650
651 def OnCmdRun(self, event):
652 """Run command"""
653 self.WriteCmdLog('(%s)\n%s' % (str(time.ctime()), ' '.join(event.cmd)),
654 notification=event.notification)
655 event.Skip()
656
657 def OnCmdDone(self, event):
658 """Command done (or aborted)
659
660 Sends signal mapCreated if map is recognized in output
661 parameters or for specific modules (as r.colors).
662 """
663 # Process results here
664 try:
665 ctime = time.time() - event.time
666 if ctime < 60:
667 stime = _("%d sec") % int(ctime)
668 else:
669 mtime = int(ctime / 60)
670 stime = _("%(min)d min %(sec)d sec") % {
671 'min': mtime, 'sec': int(ctime - (mtime * 60))}
672 except KeyError:
673 # stopped deamon
674 stime = _("unknown")
675
676 if event.aborted:
677 # Thread aborted (using our convention of None return)
678 self.WriteWarning(_('Please note that the data are left in'
679 ' inconsistent state and may be corrupted'))
680 msg = _('Command aborted')
681 else:
682 msg = _('Command finished')
683
684 self.WriteCmdLog('(%s) %s (%s)' % (str(time.ctime()), msg, stime),
685 notification=event.notification)
686
687 if event.onDone:
688 event.onDone(event)
689
690 self.cmdOutputTimer.Stop()
691
692 if event.cmd[0] == 'g.gisenv':
693 Debug.SetLevel()
694 self.Redirect()
695
696 # do nothing when no map added
697 if event.returncode != 0 or event.aborted:
698 event.Skip()
699 return
700
701 if event.cmd[0] not in globalvar.grassCmd:
702 return
703
704 # find which maps were created
705 try:
706 task = GUI(show=None).ParseCommand(event.cmd)
707 except GException as e:
708 print(e, file=sys.stderr)
709 task = None
710 return
711
712 name = task.get_name()
713 for p in task.get_options()['params']:
714 prompt = p.get('prompt', '')
715 if prompt in (
716 'raster', 'vector', 'raster_3d') and p.get(
717 'value', None):
718 if p.get('age', 'old') == 'new' or name in (
719 'r.colors', 'r3.colors', 'v.colors', 'v.proj', 'r.proj'):
720 # if multiple maps (e.g. r.series.interp), we need add each
721 if p.get('multiple', False):
722 lnames = p.get('value').split(',')
723 # in case multiple input (old) maps in r.colors
724 # we don't want to rerender it multiple times! just
725 # once
726 if p.get('age', 'old') == 'old':
727 lnames = lnames[0:1]
728 else:
729 lnames = [p.get('value')]
730 for lname in lnames:
731 if '@' not in lname:
732 lname += '@' + grass.gisenv()['MAPSET']
733 if grass.find_file(lname, element=p.get('element'))[
734 'fullname']:
735 self.mapCreated.emit(
736 name=lname, ltype=prompt, add=event.addLayer)
737 if name == 'r.mask':
738 self.updateMap.emit()
739
740 event.Skip()
741
742 def OnProcessPendingOutputWindowEvents(self, event):
743 wx.GetApp().ProcessPendingEvents()
744
745 def UpdateHistoryFile(self, command):
746 """Update history file
747
748 :param command: the command given as a string
749 """
750 env = grass.gisenv()
751 try:
752 filePath = os.path.join(env['GISDBASE'],
753 env['LOCATION_NAME'],
754 env['MAPSET'],
755 '.bash_history')
756 fileHistory = codecs.open(filePath, encoding='utf-8', mode='a')
757 except IOError as e:
758 GError(_("Unable to write file '%(filePath)s'.\n\nDetails: %(error)s") %
759 {'filePath': filePath, 'error': e},
760 parent=self._guiparent)
761 return
762
763 try:
764 fileHistory.write(command + os.linesep)
765 finally:
766 fileHistory.close()
767
768 # update wxGUI prompt
769 if self._giface:
770 self._giface.UpdateCmdHistory(command)
Note: See TracBrowser for help on using the repository browser.