gcmd.py 20 KB

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