etisserant@79: #!/usr/bin/env python
etisserant@79: # -*- coding: utf-8 -*-
etisserant@79: 
andrej@1571: # This file is part of Beremiz, a Integrated Development Environment for
andrej@1571: # programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
andrej@1571: #
andrej@1571: # Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
andrej@1571: #
andrej@1571: # See COPYING file for copyrights details.
andrej@1571: #
andrej@1571: # This program is free software; you can redistribute it and/or
andrej@1571: # modify it under the terms of the GNU General Public License
andrej@1571: # as published by the Free Software Foundation; either version 2
andrej@1571: # of the License, or (at your option) any later version.
andrej@1571: #
andrej@1571: # This program is distributed in the hope that it will be useful,
andrej@1571: # but WITHOUT ANY WARRANTY; without even the implied warranty of
andrej@1571: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
andrej@1571: # GNU General Public License for more details.
andrej@1571: #
andrej@1571: # You should have received a copy of the GNU General Public License
andrej@1571: # along with this program; if not, write to the Free Software
andrej@1571: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
etisserant@79: 
andrej@1881: 
andrej@1881: from __future__ import absolute_import
andrej@1732: import os
andrej@1732: import sys
andrej@1732: import subprocess
andrej@1732: import ctypes
edouard@2730: import time
edouard@2730: from threading import Timer, Lock, Thread, Semaphore, Condition
Edouard@1919: import signal
etisserant@79: 
Edouard@2671: _debug = os.path.exists("BEREMIZ_DEBUG")
Edouard@1407: 
Edouard@704: class outputThread(Thread):
etisserant@110:     """
etisserant@110:     Thread is used to print the output of a command to the stdout
etisserant@110:     """
etisserant@110:     def __init__(self, Proc, fd, callback=None, endcallback=None):
Edouard@704:         Thread.__init__(self)
etisserant@110:         self.killed = False
etisserant@110:         self.finished = False
etisserant@110:         self.retval = None
etisserant@110:         self.Proc = Proc
etisserant@110:         self.callback = callback
etisserant@110:         self.endcallback = endcallback
etisserant@110:         self.fd = fd
etisserant@79: 
etisserant@110:     def run(self):
etisserant@162:         outchunk = None
etisserant@149:         self.retval = None
andrej@1739:         while self.retval is None and not self.killed:
andrej@1507:             if self.endcallback:
andrej@1507:                 self.retval = self.Proc.poll()
andrej@1507:             else:
andrej@1507:                 self.retval = self.Proc.returncode
andrej@1730: 
etisserant@110:             outchunk = self.fd.readline()
andrej@1756:             if self.callback:
andrej@1756:                 self.callback(outchunk)
andrej@1739:         while outchunk != '' and not self.killed:
etisserant@162:             outchunk = self.fd.readline()
andrej@1756:             if self.callback:
andrej@1756:                 self.callback(outchunk)
etisserant@110:         if self.endcallback:
greg@130:             try:
greg@232:                 err = self.Proc.wait()
andrej@1780:             except Exception:
etisserant@149:                 err = self.retval
etisserant@110:             self.finished = True
laurent@421:             self.endcallback(self.Proc.pid, err)
Edouard@1407: 
andrej@1736: 
andrej@1831: class ProcessLogger(object):
andrej@1744:     def __init__(self, logger, Command, finish_callback=None,
andrej@1744:                  no_stdout=False, no_stderr=False, no_gui=True,
andrej@1744:                  timeout=None, outlimit=None, errlimit=None,
andrej@1744:                  endlog=None, keyword=None, kill_it=False, cwd=None,
surkovsv93@1883:                  encoding=None, output_encoding=None):
etisserant@110:         self.logger = logger
greg@424:         if not isinstance(Command, list):
greg@424:             self.Command_str = Command
greg@424:             self.Command = []
andrej@1740:             for i, word in enumerate(Command.replace("'", '"').split('"')):
greg@424:                 if i % 2 == 0:
greg@424:                     word = word.strip()
greg@424:                     if len(word) > 0:
greg@424:                         self.Command.extend(word.split())
greg@424:                 else:
greg@424:                     self.Command.append(word)
greg@424:         else:
greg@424:             self.Command = Command
greg@424:             self.Command_str = subprocess.list2cmdline(self.Command)
Edouard@1407: 
Edouard@1415:         fsencoding = sys.getfilesystemencoding()
surkovsv93@1883:         self.output_encoding = output_encoding
Edouard@1415: 
Edouard@1415:         if encoding is None:
Edouard@1415:             encoding = fsencoding
Edouard@1415:         self.Command = [self.Command[0].encode(fsencoding)]+map(
Edouard@1415:             lambda x: x.encode(encoding), self.Command[1:])
Edouard@1407: 
etisserant@110:         self.finish_callback = finish_callback
etisserant@110:         self.no_stdout = no_stdout
etisserant@110:         self.no_stderr = no_stderr
greg@111:         self.startupinfo = None
etisserant@110:         self.errlen = 0
etisserant@110:         self.outlen = 0
Edouard@704:         self.errlimit = errlimit
Edouard@704:         self.outlimit = outlimit
etisserant@110:         self.exitcode = None
Edouard@704:         self.outdata = []
Edouard@704:         self.errdata = []
Edouard@704:         self.keyword = keyword
Edouard@704:         self.kill_it = kill_it
andrej@1730:         self.startsem = Semaphore(0)
Edouard@704:         self.finishsem = Semaphore(0)
Edouard@704:         self.endlock = Lock()
Edouard@1407: 
andrej@1742:         popenargs = {
andrej@1878:             "cwd":    os.getcwd() if cwd is None else cwd,
andrej@1878:             "stdin":  subprocess.PIPE,
andrej@1878:             "stdout": subprocess.PIPE,
andrej@1878:             "stderr": subprocess.PIPE
andrej@1740:         }
Edouard@1407: 
Edouard@1919:         if no_gui and os.name in ("nt", "ce"):
greg@111:             self.startupinfo = subprocess.STARTUPINFO()
greg@111:             self.startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
greg@128:             popenargs["startupinfo"] = self.startupinfo
Edouard@1919:         elif os.name == 'posix':
Edouard@704:             popenargs["shell"] = False
Edouard@1407: 
andrej@1506:         if timeout:
andrej@1740:             self.timeout = Timer(timeout, self.endlog)
andrej@1506:             self.timeout.start()
andrej@1506:         else:
andrej@1506:             self.timeout = None
andrej@1730: 
edouard@3500:         if _debug and self.logger:
edouard@3297:             self.logger.write("(DEBUG) launching:\n" + self.Command_str + "\n")
edouard@3297: 
andrej@1746:         self.Proc = subprocess.Popen(self.Command, **popenargs)
etisserant@112: 
etisserant@110:         self.outt = outputThread(
andrej@1878:             self.Proc,
andrej@1878:             self.Proc.stdout,
andrej@1878:             self.output,
andrej@1878:             self.finish)
etisserant@110:         self.outt.start()
etisserant@79: 
etisserant@112:         self.errt = outputThread(
andrej@1878:             self.Proc,
andrej@1878:             self.Proc.stderr,
andrej@1878:             self.errors)
greg@232:         self.errt.start()
andrej@1519:         self.startsem.release()
etisserant@79: 
edouard@2730:         self.spinwakeuplock = Lock()
edouard@2730:         self.spinwakeupcond = Condition(self.spinwakeuplock)
edouard@2730:         self.spinwakeuptimer = None
edouard@2730: 
andrej@1740:     def output(self, v):
surkovsv93@1883:         if v and self.output_encoding:
surkovsv93@1883:             v = v.decode(self.output_encoding)
Edouard@704:         self.outdata.append(v)
etisserant@110:         self.outlen += 1
Edouard@2712:         if self.logger and not self.no_stdout:
Edouard@686:             self.logger.write(v)
andrej@1742:         if (self.keyword and v.find(self.keyword) != -1) or (self.outlimit and self.outlen > self.outlimit):
Edouard@704:             self.endlog()
Edouard@1407: 
andrej@1740:     def errors(self, v):
surkovsv93@1883:         if v and self.output_encoding:
surkovsv93@1883:             v = v.decode(self.output_encoding)
Edouard@704:         self.errdata.append(v)
etisserant@110:         self.errlen += 1
Edouard@2712:         if self.logger and not self.no_stderr:
Edouard@686:             self.logger.write_warning(v)
Edouard@704:         if self.errlimit and self.errlen > self.errlimit:
Edouard@704:             self.endlog()
etisserant@79: 
andrej@1740:     def log_the_end(self, ecode, pid):
Edouard@2671:         if self.logger is not None:
Edouard@2671:             self.logger.write(self.Command_str + "\n")
Edouard@2671:             self.logger.write_warning(_("exited with status {a1} (pid {a2})\n").format(a1=str(ecode), a2=str(pid)))
greg@232: 
andrej@1740:     def finish(self, pid, ecode):
andrej@1730:         # avoid running function before start is finished
andrej@1519:         self.startsem.acquire()
andrej@1882:         self.startsem.release()
andrej@1476:         if self.timeout:
andrej@1476:             self.timeout.cancel()
etisserant@110:         self.exitcode = ecode
edouard@3297:         if self.exitcode != 0:
andrej@1740:             self.log_the_end(ecode, pid)
etisserant@110:         if self.finish_callback is not None:
andrej@1740:             self.finish_callback(self, ecode, pid)
andrey@1516:         self.errt.join()
Edouard@704:         self.finishsem.release()
edouard@2730:         self.spinwakeup()
etisserant@79: 
andrej@1740:     def kill(self, gently=True):
andrej@1539:         # avoid running kill before start is finished
andrej@1539:         self.startsem.acquire()
andrej@1539:         self.startsem.release()
andrej@1730: 
etisserant@110:         self.outt.killed = True
etisserant@112:         self.errt.killed = True
Edouard@1919:         if os.name in ("nt", "ce"):
etisserant@110:             PROCESS_TERMINATE = 1
etisserant@110:             handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, False, self.Proc.pid)
etisserant@110:             ctypes.windll.kernel32.TerminateProcess(handle, -1)
etisserant@110:             ctypes.windll.kernel32.CloseHandle(handle)
etisserant@110:         else:
etisserant@235:             if gently:
Edouard@1919:                 sig = signal.SIGTERM
etisserant@235:             else:
Edouard@1919:                 sig = signal.SIGKILL
etisserant@154:             try:
etisserant@235:                 os.kill(self.Proc.pid, sig)
andrej@1780:             except Exception:
etisserant@154:                 pass
greg@232:         self.outt.join()
greg@232:         self.errt.join()
etisserant@79: 
Edouard@704:     def endlog(self):
Edouard@704:         if self.endlock.acquire(False):
Edouard@704:             if not self.outt.finished and self.kill_it:
andrej@1757:                 self.kill()
andrey@1516:             self.finishsem.release()
edouard@2730:             self.spinwakeup()
edouard@2730: 
edouard@2730:     def spinwakeup(self):
edouard@2730:         with self.spinwakeuplock:
edouard@2730:             if self.spinwakeuptimer is not None:
edouard@2730:                 self.spinwakeuptimer.cancel()
edouard@2730:             self.spinwakeuptimer = None
edouard@2730:             self.spinwakeupcond.notify()
etisserant@89: 
Edouard@704:     def spin(self):
edouard@2730:         start = time.time()
edouard@2733:         if self.logger:
edouard@2733:             while not self.finishsem.acquire(0):
edouard@2733:                 with self.spinwakeuplock:
edouard@2733:                     self.spinwakeuptimer = Timer(0.1, self.spinwakeup)
edouard@2733:                     self.spinwakeuptimer.start()
edouard@2733:                     self.spinwakeupcond.wait()
edouard@2733:                     self.logger.progress("%.3fs"%(time.time() - start))
edouard@2733:         else:
edouard@2733:             self.finishsem.acquire()
edouard@2730: 
Edouard@704:         return [self.exitcode, "".join(self.outdata), "".join(self.errdata)]