gcmd.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. """
  2. @package gcmd
  3. @brief GRASS command interface
  4. Classes:
  5. - GException
  6. - GStdError
  7. - CmdError
  8. - SettingsError
  9. - DigitError
  10. - DBMError
  11. - NvizError
  12. - Popen (from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440554)
  13. - Command
  14. - CommandThread
  15. (C) 2007-2008 by the GRASS Development Team
  16. This program is free software under the GNU General Public
  17. License (>=v2). Read the file COPYING that comes with GRASS
  18. for details.
  19. @author Jachym Cepicky
  20. Martin Landa <landa.martin gmail.com>
  21. @date 2007-2008
  22. """
  23. import os
  24. import sys
  25. import time
  26. import errno
  27. import signal
  28. import wx
  29. try:
  30. import subprocess
  31. except:
  32. compatPath = os.path.join(globalvar.ETCWXDIR, "compat")
  33. sys.path.append(compatPath)
  34. import subprocess
  35. if subprocess.mswindows:
  36. from win32file import ReadFile, WriteFile
  37. from win32pipe import PeekNamedPipe
  38. import msvcrt
  39. else:
  40. import select
  41. import fcntl
  42. from threading import Thread
  43. import globalvar
  44. grassPath = os.path.join(globalvar.ETCDIR, "python")
  45. sys.path.append(grassPath)
  46. import grass
  47. import utils
  48. from debug import Debug as Debug
  49. class GException(Exception):
  50. """Generic exception"""
  51. def __init__(self, message, title=_("Error"), parent=None):
  52. self.message = message
  53. self.parent = parent
  54. self.title = title
  55. def __str__(self):
  56. dlg = wx.MessageDialog(parent=self.parent,
  57. caption=self.title,
  58. message=self.message,
  59. style=wx.ICON_ERROR | wx.CENTRE)
  60. dlg.SetIcon(wx.Icon(os.path.join(globalvar.ETCICONDIR, 'grass_error.ico'), wx.BITMAP_TYPE_ICO))
  61. if self.parent:
  62. dlg.CentreOnParent()
  63. else:
  64. dlg.CentreOnScreen()
  65. dlg.ShowModal()
  66. return ''
  67. class GStdError(GException):
  68. """Generic exception"""
  69. def __init__(self, message, title=_("Error"), parent=None):
  70. GException.__init__(self, message, title=title, parent=parent)
  71. class CmdError(GException):
  72. """Exception used for GRASS commands.
  73. See Command class (command exits with EXIT_FAILURE,
  74. G_fatal_error() is called)."""
  75. def __init__(self, cmd, message, parent=None):
  76. self.cmd = cmd
  77. GException.__init__(self, message,
  78. title=_("Error in command execution %s" % self.cmd[0]),
  79. parent=parent)
  80. class SettingsError(GException):
  81. """Exception used for GRASS settings, see
  82. gui_modules/preferences.py."""
  83. def __init__(self, message, parent=None):
  84. GException.__init__(self, message,
  85. title=_("Preferences error"),
  86. parent=parent)
  87. class DigitError(GException):
  88. """Exception raised during digitization session"""
  89. def __init__(self, message, parent=None):
  90. GException.__init__(self, message,
  91. title=_("Vector digitizer error"),
  92. parent=parent)
  93. class DBMError(GException):
  94. """Attribute Table Manager exception class"""
  95. def __init__(self, message, parent=None):
  96. GException.__init__(self, message,
  97. title=_("Attribute table manager error"),
  98. parent=parent)
  99. class NvizError(GException):
  100. """Nviz exception class"""
  101. def __init__(self, message, parent=None):
  102. GException.__init__(self, message,
  103. title=_("Nviz error"),
  104. parent=parent)
  105. class Popen(subprocess.Popen):
  106. """Subclass subprocess.Popen"""
  107. def recv(self, maxsize=None):
  108. return self._recv('stdout', maxsize)
  109. def recv_err(self, maxsize=None):
  110. return self._recv('stderr', maxsize)
  111. def send_recv(self, input='', maxsize=None):
  112. return self.send(input), self.recv(maxsize), self.recv_err(maxsize)
  113. def get_conn_maxsize(self, which, maxsize):
  114. if maxsize is None:
  115. maxsize = 1024
  116. elif maxsize < 1:
  117. maxsize = 1
  118. return getattr(self, which), maxsize
  119. def _close(self, which):
  120. getattr(self, which).close()
  121. setattr(self, which, None)
  122. def kill(self):
  123. """Try to kill running process"""
  124. if subprocess.mswindows:
  125. import win32api
  126. handle = win32api.OpenProcess(1, 0, self.pid)
  127. return (0 != win32api.TerminateProcess(handle, 0))
  128. else:
  129. try:
  130. os.kill(-self.pid, signal.SIGTERM) # kill whole group
  131. except OSError:
  132. pass
  133. if subprocess.mswindows:
  134. def send(self, input):
  135. if not self.stdin:
  136. return None
  137. try:
  138. x = msvcrt.get_osfhandle(self.stdin.fileno())
  139. (errCode, written) = WriteFile(x, input)
  140. except ValueError:
  141. return self._close('stdin')
  142. except (subprocess.pywintypes.error, Exception), why:
  143. if why[0] in (109, errno.ESHUTDOWN):
  144. return self._close('stdin')
  145. raise
  146. return written
  147. def _recv(self, which, maxsize):
  148. conn, maxsize = self.get_conn_maxsize(which, maxsize)
  149. if conn is None:
  150. return None
  151. try:
  152. x = msvcrt.get_osfhandle(conn.fileno())
  153. (read, nAvail, nMessage) = PeekNamedPipe(x, 0)
  154. if maxsize < nAvail:
  155. nAvail = maxsize
  156. if nAvail > 0:
  157. (errCode, read) = ReadFile(x, nAvail, None)
  158. except ValueError:
  159. return self._close(which)
  160. except (subprocess.pywintypes.error, Exception), why:
  161. if why[0] in (109, errno.ESHUTDOWN):
  162. return self._close(which)
  163. raise
  164. if self.universal_newlines:
  165. read = self._translate_newlines(read)
  166. return read
  167. else:
  168. def send(self, input):
  169. if not self.stdin:
  170. return None
  171. if not select.select([], [self.stdin], [], 0)[1]:
  172. return 0
  173. try:
  174. written = os.write(self.stdin.fileno(), input)
  175. except OSError, why:
  176. if why[0] == errno.EPIPE: #broken pipe
  177. return self._close('stdin')
  178. raise
  179. return written
  180. def _recv(self, which, maxsize):
  181. conn, maxsize = self.get_conn_maxsize(which, maxsize)
  182. if conn is None:
  183. return None
  184. flags = fcntl.fcntl(conn, fcntl.F_GETFL)
  185. if not conn.closed:
  186. fcntl.fcntl(conn, fcntl.F_SETFL, flags| os.O_NONBLOCK)
  187. try:
  188. if not select.select([conn], [], [], 0)[0]:
  189. return ''
  190. r = conn.read(maxsize)
  191. if not r:
  192. return self._close(which)
  193. if self.universal_newlines:
  194. r = self._translate_newlines(r)
  195. return r
  196. finally:
  197. if not conn.closed:
  198. fcntl.fcntl(conn, fcntl.F_SETFL, flags)
  199. message = "Other end disconnected!"
  200. def recv_some(p, t=.1, e=1, tr=5, stderr=0):
  201. if tr < 1:
  202. tr = 1
  203. x = time.time()+t
  204. y = []
  205. r = ''
  206. pr = p.recv
  207. if stderr:
  208. pr = p.recv_err
  209. while time.time() < x or r:
  210. r = pr()
  211. if r is None:
  212. if e:
  213. raise Exception(message)
  214. else:
  215. break
  216. elif r:
  217. y.append(r)
  218. else:
  219. time.sleep(max((x-time.time())/tr, 0))
  220. return ''.join(y)
  221. def send_all(p, data):
  222. while len(data):
  223. sent = p.send(data)
  224. if sent is None:
  225. raise Exception(message)
  226. data = buffer(data, sent)
  227. class Command:
  228. """Run command in separate thread. Used for commands launched
  229. on the background.
  230. If stdout/err is redirected, write() method is required for the
  231. given classes.
  232. @code
  233. cmd = Command(cmd=['d.rast', 'elevation.dem'], verbose=3, wait=True)
  234. if cmd.returncode == None:
  235. print 'RUNNING?'
  236. elif cmd.returncode == 0:
  237. print 'SUCCESS'
  238. else:
  239. print 'FAILURE (%d)' % cmd.returncode
  240. @endcode
  241. """
  242. def __init__ (self, cmd, stdin=None,
  243. verbose=None, wait=True, rerr=False,
  244. stdout=None, stderr=None):
  245. """
  246. @param cmd command given as list
  247. @param stdin standard input stream
  248. @param verbose verbose level [0, 3] (--q, --v)
  249. @param wait wait for child execution terminated
  250. @param rerr error handling (when CmdError raised).
  251. True for redirection to stderr, False for GUI dialog,
  252. None for no operation (quiet mode)
  253. @param stdout redirect standard output or None
  254. @param stderr redirect standard error output or None
  255. """
  256. self.cmd = cmd
  257. self.stderr = stderr
  258. # hack around platform-specific extension for binaries
  259. if self.cmd[0] in globalvar.grassCmd['script']:
  260. self.cmd[0] = self.cmd[0] + globalvar.EXT_SCT
  261. else:
  262. self.cmd[0] = self.cmd[0] + globalvar.EXT_BIN
  263. #
  264. # set verbosity level
  265. #
  266. verbose_orig = None
  267. if ('--q' not in self.cmd and '--quiet' not in self.cmd) and \
  268. ('--v' not in self.cmd and '--verbose' not in self.cmd):
  269. if verbose is not None:
  270. if verbose == 0:
  271. self.cmd.append('--quiet')
  272. elif verbose == 3:
  273. self.cmd.append('--verbose')
  274. else:
  275. verbose_orig = os.getenv("GRASS_VERBOSE")
  276. os.environ["GRASS_VERBOSE"] = str(verbose)
  277. #
  278. # create command thread
  279. #
  280. self.cmdThread = CommandThread(cmd, stdin,
  281. stdout, stderr)
  282. self.cmdThread.start()
  283. if wait:
  284. self.cmdThread.join()
  285. if self.cmdThread.module:
  286. self.cmdThread.module.wait()
  287. self.returncode = self.cmdThread.module.returncode
  288. else:
  289. self.returncode = 1
  290. else:
  291. self.cmdThread.join(0.5)
  292. self.returncode = None
  293. if self.returncode is not None:
  294. Debug.msg (3, "Command(): cmd='%s', wait=%s, returncode=%d, alive=%s" % \
  295. (' '.join(cmd), wait, self.returncode, self.cmdThread.isAlive()))
  296. if rerr is not None and self.returncode != 0:
  297. if rerr is False: # GUI dialog
  298. raise CmdError(cmd=self.cmd,
  299. message="%s '%s'%s%s%s %s%s" %
  300. (_("Execution failed:"),
  301. ' '.join(self.cmd),
  302. os.linesep, os.linesep,
  303. _("Details:"),
  304. os.linesep,
  305. _("Error: ") + self.GetError()))
  306. elif rerr == sys.stderr: # redirect message to sys
  307. stderr.write("Execution failed: '%s'" % (' '.join(self.cmd)))
  308. stderr.write("%sDetails:%s%s" % (os.linesep,
  309. _("Error: ") + self.GetError(),
  310. os.linesep))
  311. else:
  312. pass # nop
  313. else:
  314. Debug.msg (3, "Command(): cmd='%s', wait=%s, returncode=?, alive=%s" % \
  315. (' '.join(cmd), wait, self.cmdThread.isAlive()))
  316. if verbose_orig:
  317. os.environ["GRASS_VERBOSE"] = verbose_orig
  318. elif os.environ.has_key("GRASS_VERBOSE"):
  319. del os.environ["GRASS_VERBOSE"]
  320. def __ReadOutput(self, stream):
  321. """Read stream and return list of lines
  322. @param stream stream to be read
  323. """
  324. lineList = []
  325. if stream is None:
  326. return lineList
  327. while True:
  328. line = stream.readline()
  329. if not line:
  330. break
  331. line = line.replace('%s' % os.linesep, '').strip()
  332. lineList.append(line)
  333. return lineList
  334. def ReadStdOutput(self):
  335. """Read standard output and return list of lines"""
  336. if self.cmdThread.stdout:
  337. stream = self.cmdThread.stdout # use redirected stream instead
  338. stream.seek(0)
  339. else:
  340. stream = self.cmdThread.module.stdout
  341. return self.__ReadOutput(stream)
  342. def ReadErrOutput(self):
  343. """Read standard error output and return list of lines"""
  344. return self.__ReadOutput(self.cmdThread.module.stderr)
  345. def __ProcessStdErr(self):
  346. """
  347. Read messages/warnings/errors from stderr
  348. @return list of (type, message)
  349. """
  350. if self.stderr is None:
  351. lines = self.ReadErrOutput()
  352. else:
  353. lines = self.cmdThread.error.strip('%s' % os.linesep). \
  354. split('%s' % os.linesep)
  355. msg = []
  356. type = None
  357. content = ""
  358. for line in lines:
  359. if len(line) == 0:
  360. continue
  361. if 'GRASS_' in line: # error or warning
  362. if 'GRASS_INFO_WARNING' in line: # warning
  363. type = "WARNING"
  364. elif 'GRASS_INFO_ERROR' in line: # error
  365. type = "ERROR"
  366. elif 'GRASS_INFO_END': # end of message
  367. msg.append((type, content))
  368. type = None
  369. content = ""
  370. if type:
  371. content += line.split(':', 1)[1].strip()
  372. else: # stderr
  373. msg.append((None, line.strip()))
  374. return msg
  375. def GetError(self):
  376. """Get error message or ''"""
  377. if not self.cmdThread.module:
  378. return _("Unable to exectute command: '%s'") % ' '.join(self.cmd)
  379. for type, msg in self.__ProcessStdErr():
  380. if type == 'ERROR':
  381. return unicode(msg, "utf-8")
  382. return ''
  383. def PrintModuleOutput(self, error=True, warning=False, message=False):
  384. """Print module errors, warnings, messages to output
  385. @param error print errors
  386. @param warning print warnings
  387. @param message print messages
  388. @return string
  389. """
  390. msgString = ""
  391. for type, msg in self.__ProcessStdErr():
  392. if type:
  393. if (type == 'ERROR' and error) or \
  394. (type == 'WARNING' and warning) or \
  395. (type == 'MESSAGE' and message):
  396. msgString += " " + type + ": " + msg + "%s" % os.linesep
  397. else:
  398. msgString += " " + msg + "%s" % os.linesep
  399. return msgString
  400. class CommandThread(Thread):
  401. """Create separate thread for command. Used for commands launched
  402. on the background."""
  403. def __init__ (self, cmd, stdin=None,
  404. stdout=sys.stdout, stderr=sys.stderr):
  405. """
  406. @param cmd command (given as list)
  407. @param stdin standard input stream
  408. @param stdout redirect standard output or None
  409. @param stderr redirect standard error output or None
  410. """
  411. Thread.__init__(self)
  412. self.cmd = cmd
  413. self.stdin = stdin
  414. self.stdout = stdout
  415. self.stderr = stderr
  416. self.module = None
  417. self.error = ''
  418. self._want_abort = False
  419. self.aborted = False
  420. self.setDaemon(True)
  421. # set message formatting
  422. self.message_format = os.getenv("GRASS_MESSAGE_FORMAT")
  423. os.environ["GRASS_MESSAGE_FORMAT"] = "gui"
  424. def __del__(self):
  425. if self.message_format:
  426. os.environ["GRASS_MESSAGE_FORMAT"] = self.message_format
  427. else:
  428. del os.environ["GRASS_MESSAGE_FORMAT"]
  429. def run(self):
  430. """Run command"""
  431. if len(self.cmd) == 0:
  432. return
  433. self.startTime = time.time()
  434. try:
  435. self.module = Popen(self.cmd,
  436. stdin=subprocess.PIPE,
  437. stdout=subprocess.PIPE,
  438. stderr=subprocess.PIPE)
  439. except OSError, e:
  440. self.error = str(e)
  441. return 1
  442. if self.stdin: # read stdin if requested ...
  443. self.module.stdin.write(self.stdin)
  444. self.module.stdin.close()
  445. # redirect standard outputs...
  446. if self.stdout or self.stderr:
  447. self.__redirect_stream()
  448. def __redirect_stream(self):
  449. """Redirect stream"""
  450. if self.stdout:
  451. # make module stdout/stderr non-blocking
  452. out_fileno = self.module.stdout.fileno()
  453. if not subprocess.mswindows:
  454. flags = fcntl.fcntl(out_fileno, fcntl.F_GETFL)
  455. fcntl.fcntl(out_fileno, fcntl.F_SETFL, flags| os.O_NONBLOCK)
  456. if self.stderr:
  457. # make module stdout/stderr non-blocking
  458. out_fileno = self.module.stderr.fileno()
  459. if not subprocess.mswindows:
  460. flags = fcntl.fcntl(out_fileno, fcntl.F_GETFL)
  461. fcntl.fcntl(out_fileno, fcntl.F_SETFL, flags| os.O_NONBLOCK)
  462. # wait for the process to end, sucking in stuff until it does end
  463. while self.module.poll() is None:
  464. if self._want_abort: # abort running process
  465. self.module.kill()
  466. self.aborted = True
  467. return
  468. if self.stdout:
  469. line = recv_some(self.module, e=0, stderr=0)
  470. self.stdout.write(line)
  471. if self.stderr:
  472. line = recv_some(self.module, e=0, stderr=1)
  473. self.stderr.write(line)
  474. if len(line) > 0:
  475. self.error = line
  476. # get the last output
  477. if self.stdout:
  478. line = recv_some(self.module, e=0, stderr=0)
  479. self.stdout.write(line)
  480. if self.stderr:
  481. line = recv_some(self.module, e=0, stderr=1)
  482. self.stderr.write(line)
  483. if len(line) > 0:
  484. self.error = line
  485. def abort(self):
  486. """Abort running process, used by main thread to signal an abort"""
  487. self._want_abort = True