Mercurial > hg > sh-issue-141
changeset 0:7d0255678d8b
wtf?
author | Jordi Gutiérrez Hermoso <jordigh@octave.org> |
---|---|
date | Mon, 06 Oct 2014 15:18:55 -0400 |
parents | |
children | 0d89b02cb45c |
files | sh.py sh.pyc ss/bin/scansetup ss/lib/perl5/NeuroRx/ScanSetup.pm ss/lib/perl5/NeuroRx/ScanSetup/IdentifyModality.pm ss/lib/perl5/NeuroRx/ScanSetup/RenameMincFiles.pm wtf |
diffstat | 7 files changed, 2102 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/sh.py @@ -0,0 +1,1695 @@ +#=============================================================================== +# Copyright (C) 2011-2012 by Andrew Moffat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +#=============================================================================== + + +__version__ = "1.08" +__project_url__ = "https://github.com/amoffat/sh" + + + +import platform + +if "windows" in platform.system().lower(): + raise ImportError("sh %s is currently only supported on linux and osx. \ +please install pbs 0.110 (http://pypi.python.org/pypi/pbs) for windows \ +support." % __version__) + + + +import sys +IS_PY3 = sys.version_info[0] == 3 + +import traceback +import os +import re +from glob import glob as original_glob +from types import ModuleType +from functools import partial +import inspect +import time as _time + +from locale import getpreferredencoding +DEFAULT_ENCODING = getpreferredencoding() or "utf-8" + + +if IS_PY3: + from io import StringIO + from io import BytesIO as cStringIO + from queue import Queue, Empty +else: + from StringIO import StringIO + from cStringIO import OutputType as cStringIO + from Queue import Queue, Empty + +IS_OSX = platform.system() == "Darwin" +THIS_DIR = os.path.dirname(os.path.realpath(__file__)) + + +import errno +import warnings + +import pty +import termios +import signal +import gc +import select +import atexit +import threading +import tty +import fcntl +import struct +import resource +from collections import deque +import logging +import weakref + + +logging_enabled = False + + +if IS_PY3: + raw_input = input + unicode = str + basestring = str + + + + +class ErrorReturnCode(Exception): + truncate_cap = 750 + + def __init__(self, full_cmd, stdout, stderr): + self.full_cmd = full_cmd + self.stdout = stdout + self.stderr = stderr + + + if self.stdout is None: tstdout = "<redirected>" + else: + tstdout = self.stdout[:self.truncate_cap] + out_delta = len(self.stdout) - len(tstdout) + if out_delta: + tstdout += ("... (%d more, please see e.stdout)" % out_delta).encode() + + if self.stderr is None: tstderr = "<redirected>" + else: + tstderr = self.stderr[:self.truncate_cap] + err_delta = len(self.stderr) - len(tstderr) + if err_delta: + tstderr += ("... (%d more, please see e.stderr)" % err_delta).encode() + + msg = "\n\n RAN: %r\n\n STDOUT:\n%s\n\n STDERR:\n%s" %\ + (full_cmd, tstdout.decode(DEFAULT_ENCODING), tstderr.decode(DEFAULT_ENCODING)) + super(ErrorReturnCode, self).__init__(msg) + + +class SignalException(ErrorReturnCode): pass + +SIGNALS_THAT_SHOULD_THROW_EXCEPTION = ( + signal.SIGKILL, + signal.SIGSEGV, + signal.SIGTERM, + signal.SIGINT, + signal.SIGQUIT +) + + +# we subclass AttributeError because: +# https://github.com/ipython/ipython/issues/2577 +# https://github.com/amoffat/sh/issues/97#issuecomment-10610629 +class CommandNotFound(AttributeError): pass + +rc_exc_regex = re.compile("(ErrorReturnCode|SignalException)_(\d+)") +rc_exc_cache = {} + +def get_rc_exc(rc): + rc = int(rc) + try: return rc_exc_cache[rc] + except KeyError: pass + + if rc > 0: + name = "ErrorReturnCode_%d" % rc + exc = type(name, (ErrorReturnCode,), {}) + else: + name = "SignalException_%d" % abs(rc) + exc = type(name, (SignalException,), {}) + + rc_exc_cache[rc] = exc + return exc + + + + +def which(program): + def is_exe(fpath): + return os.path.exists(fpath) and os.access(fpath, os.X_OK) + + fpath, fname = os.path.split(program) + if fpath: + if is_exe(program): return program + else: + if "PATH" not in os.environ: return None + for path in os.environ["PATH"].split(os.pathsep): + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + + return None + +def resolve_program(program): + path = which(program) + if not path: + # our actual command might have a dash in it, but we can't call + # that from python (we have to use underscores), so we'll check + # if a dash version of our underscore command exists and use that + # if it does + if "_" in program: path = which(program.replace("_", "-")) + if not path: return None + return path + + +# we add this thin wrapper to glob.glob because of a specific edge case where +# glob does not expand to anything. for example, if you try to do +# glob.glob("*.py") and there are no *.py files in the directory, glob.glob +# returns an empty list. this empty list gets passed to the command, and +# then the command fails with a misleading error message. this thin wrapper +# ensures that if there is no expansion, we pass in the original argument, +# so that when the command fails, the error message is clearer +def glob(arg): + return original_glob(arg) or arg + + + +class Logger(object): + def __init__(self, name, context=None): + self.name = name + self.context = "%s" + if context: self.context = "%s: %%s" % context + self.log = logging.getLogger(name) + + def info(self, msg, *args): + if not logging_enabled: return + self.log.info(self.context, msg % args) + + def debug(self, msg, *args): + if not logging_enabled: return + self.log.debug(self.context, msg % args) + + def error(self, msg, *args): + if not logging_enabled: return + self.log.error(self.context, msg % args) + + def exception(self, msg, *args): + if not logging_enabled: return + self.log.exception(self.context, msg % args) + + + +class RunningCommand(object): + def __init__(self, cmd, call_args, stdin, stdout, stderr): + truncate = 20 + if len(cmd) > truncate: + logger_str = "command %r...(%d more) call_args %r" % \ + (cmd[:truncate], len(cmd) - truncate, call_args) + else: + logger_str = "command %r call_args %r" % (cmd, call_args) + + self.log = Logger("command", logger_str) + self.call_args = call_args + self.cmd = cmd + self.ran = " ".join(cmd) + self.process = None + + # this flag is for whether or not we've handled the exit code (like + # by raising an exception). this is necessary because .wait() is called + # from multiple places, and wait() triggers the exit code to be + # processed. but we don't want to raise multiple exceptions, only + # one (if any at all) + self._handled_exit_code = False + + self.should_wait = True + spawn_process = True + + + # with contexts shouldn't run at all yet, they prepend + # to every command in the context + if call_args["with"]: + spawn_process = False + Command._prepend_stack.append(self) + + + if callable(call_args["out"]) or callable(call_args["err"]): + self.should_wait = False + + if call_args["piped"] or call_args["iter"] or call_args["iter_noblock"]: + self.should_wait = False + + # we're running in the background, return self and let us lazily + # evaluate + if call_args["bg"]: self.should_wait = False + + # redirection + if call_args["err_to_out"]: stderr = STDOUT + + + # set up which stream should write to the pipe + # TODO, make pipe None by default and limit the size of the Queue + # in oproc.OProc + pipe = STDOUT + if call_args["iter"] == "out" or call_args["iter"] is True: pipe = STDOUT + elif call_args["iter"] == "err": pipe = STDERR + + if call_args["iter_noblock"] == "out" or call_args["iter_noblock"] is True: pipe = STDOUT + elif call_args["iter_noblock"] == "err": pipe = STDERR + + + if spawn_process: + self.log.debug("starting process") + self.process = OProc(cmd, stdin, stdout, stderr, + self.call_args, pipe=pipe) + + if self.should_wait: + self.wait() + + + def wait(self): + self._handle_exit_code(self.process.wait()) + return self + + # here we determine if we had an exception, or an error code that we weren't + # expecting to see. if we did, we create and raise an exception + def _handle_exit_code(self, code): + if self._handled_exit_code: return + self._handled_exit_code = True + + if code not in self.call_args["ok_code"] and \ + (code > 0 or -code in SIGNALS_THAT_SHOULD_THROW_EXCEPTION): + raise get_rc_exc(code)( + " ".join(self.cmd), + self.process.stdout, + self.process.stderr + ) + + + + @property + def stdout(self): + self.wait() + return self.process.stdout + + @property + def stderr(self): + self.wait() + return self.process.stderr + + @property + def exit_code(self): + self.wait() + return self.process.exit_code + + @property + def pid(self): + return self.process.pid + + def __len__(self): + return len(str(self)) + + def __enter__(self): + # we don't actually do anything here because anything that should + # have been done would have been done in the Command.__call__ call. + # essentially all that has to happen is the comand be pushed on + # the prepend stack. + pass + + def __iter__(self): + return self + + def next(self): + # we do this because if get blocks, we can't catch a KeyboardInterrupt + # so the slight timeout allows for that. + while True: + try: chunk = self.process._pipe_queue.get(False, .001) + except Empty: + if self.call_args["iter_noblock"]: return errno.EWOULDBLOCK + else: + if chunk is None: + self.wait() + raise StopIteration() + try: return chunk.decode(self.call_args["encoding"], + self.call_args["decode_errors"]) + except UnicodeDecodeError: return chunk + + # python 3 + __next__ = next + + def __exit__(self, typ, value, traceback): + if self.call_args["with"] and Command._prepend_stack: + Command._prepend_stack.pop() + + def __str__(self): + if IS_PY3: return self.__unicode__() + else: return unicode(self).encode(self.call_args["encoding"]) + + def __unicode__(self): + if self.process and self.stdout: + return self.stdout.decode(self.call_args["encoding"], + self.call_args["decode_errors"]) + return "" + + def __eq__(self, other): + return unicode(self) == unicode(other) + + def __contains__(self, item): + return item in str(self) + + def __getattr__(self, p): + # let these three attributes pass through to the OProc object + if p in ("signal", "terminate", "kill"): + if self.process: return getattr(self.process, p) + else: raise AttributeError + return getattr(unicode(self), p) + + def __repr__(self): + try: return str(self) + except UnicodeDecodeError: + if self.process: + if self.stdout: return repr(self.stdout) + return repr("") + + def __long__(self): + return long(str(self).strip()) + + def __float__(self): + return float(str(self).strip()) + + def __int__(self): + return int(str(self).strip()) + + + + + +class Command(object): + _prepend_stack = [] + + _call_args = { + # currently unsupported + #"fg": False, # run command in foreground + + "bg": False, # run command in background + "with": False, # prepend the command to every command after it + "in": None, + "out": None, # redirect STDOUT + "err": None, # redirect STDERR + "err_to_out": None, # redirect STDERR to STDOUT + + # stdin buffer size + # 1 for line, 0 for unbuffered, any other number for that amount + "in_bufsize": 0, + # stdout buffer size, same values as above + "out_bufsize": 1, + "err_bufsize": 1, + + # this is how big the output buffers will be for stdout and stderr. + # this is essentially how much output they will store from the process. + # we use a deque, so if it overflows past this amount, the first items + # get pushed off as each new item gets added. + # + # NOTICE + # this is not a *BYTE* size, this is a *CHUNK* size...meaning, that if + # you're buffering out/err at 1024 bytes, the internal buffer size will + # be "internal_bufsize" CHUNKS of 1024 bytes + "internal_bufsize": 3 * 1024**2, + + "env": None, + "piped": None, + "iter": None, + "iter_noblock": None, + "ok_code": 0, + "cwd": None, + "long_sep": "=", + + # this is for programs that expect their input to be from a terminal. + # ssh is one of those programs + "tty_in": False, + "tty_out": True, + + "encoding": DEFAULT_ENCODING, + "decode_errors": "strict", + + # how long the process should run before it is auto-killed + "timeout": 0, + + # these control whether or not stdout/err will get aggregated together + # as the process runs. this has memory usage implications, so sometimes + # with long-running processes with a lot of data, it makes sense to + # set these to true + "no_out": False, + "no_err": False, + "no_pipe": False, + + # if any redirection is used for stdout or stderr, internal buffering + # of that data is not stored. this forces it to be stored, as if + # the output is being T'd to both the redirected destination and our + # internal buffers + "tee": None, + } + + # these are arguments that cannot be called together, because they wouldn't + # make any sense + _incompatible_call_args = ( + #("fg", "bg", "Command can't be run in the foreground and background"), + ("err", "err_to_out", "Stderr is already being redirected"), + ("piped", "iter", "You cannot iterate when this command is being piped"), + ) + + + # this method exists because of the need to have some way of letting + # manual object instantiation not perform the underscore-to-dash command + # conversion that resolve_program uses. + # + # there are 2 ways to create a Command object. using sh.Command(<program>) + # or by using sh.<program>. the method fed into sh.Command must be taken + # literally, and so no underscore-dash conversion is performed. the one + # for sh.<program> must do the underscore-dash converesion, because we + # can't type dashes in method names + @classmethod + def _create(cls, program, **default_kwargs): + path = resolve_program(program) + if not path: raise CommandNotFound(program) + + cmd = cls(path) + if default_kwargs: cmd = cmd.bake(**default_kwargs) + + return cmd + + + def __init__(self, path): + path = which(path) + if not path: raise CommandNotFound(path) + self._path = path + + self._partial = False + self._partial_baked_args = [] + self._partial_call_args = {} + + # bugfix for functools.wraps. issue #121 + self.__name__ = repr(self) + + + def __getattribute__(self, name): + # convenience + getattr = partial(object.__getattribute__, self) + + if name.startswith("_"): return getattr(name) + if name == "bake": return getattr("bake") + if name.endswith("_"): name = name[:-1] + + return getattr("bake")(name) + + + @staticmethod + def _extract_call_args(kwargs, to_override={}): + kwargs = kwargs.copy() + call_args = {} + for parg, default in Command._call_args.items(): + key = "_" + parg + + if key in kwargs: + call_args[parg] = kwargs[key] + del kwargs[key] + elif parg in to_override: + call_args[parg] = to_override[parg] + + # test for incompatible call args + s1 = set(call_args.keys()) + for args in Command._incompatible_call_args: + args = list(args) + error = args.pop() + + if s1.issuperset(args): + raise TypeError("Invalid special arguments %r: %s" % (args, error)) + + return call_args, kwargs + + + # this helper method is for normalizing an argument into a string in the + # system's default encoding. we can feed it a number or a string or + # whatever + def _format_arg(self, arg): + if IS_PY3: arg = str(arg) + else: + # if the argument is already unicode, or a number or whatever, + # this first call will fail. + try: arg = unicode(arg, DEFAULT_ENCODING).encode(DEFAULT_ENCODING) + except TypeError: arg = unicode(arg).encode(DEFAULT_ENCODING) + return arg + + + def _aggregate_keywords(self, keywords, sep, raw=False): + processed = [] + for k, v in keywords.items(): + # we're passing a short arg as a kwarg, example: + # cut(d="\t") + if len(k) == 1: + if v is not False: + processed.append("-" + k) + if v is not True: + processed.append(self._format_arg(v)) + + # we're doing a long arg + else: + if not raw: k = k.replace("_", "-") + + if v is True: + processed.append("--" + k) + elif v is False: + pass + else: + processed.append("--%s%s%s" % (k, sep, self._format_arg(v))) + return processed + + + def _compile_args(self, args, kwargs, sep): + processed_args = [] + + # aggregate positional args + for arg in args: + if isinstance(arg, (list, tuple)): + if not arg: + warnings.warn("Empty list passed as an argument to %r. \ +If you're using glob.glob(), please use sh.glob() instead." % self.path, stacklevel=3) + for sub_arg in arg: processed_args.append(self._format_arg(sub_arg)) + elif isinstance(arg, dict): + processed_args += self._aggregate_keywords(arg, sep, raw=True) + else: + processed_args.append(self._format_arg(arg)) + + # aggregate the keyword arguments + processed_args += self._aggregate_keywords(kwargs, sep) + + return processed_args + + + # TODO needs documentation + def bake(self, *args, **kwargs): + fn = Command(self._path) + fn._partial = True + + call_args, kwargs = self._extract_call_args(kwargs) + + pruned_call_args = call_args + for k,v in Command._call_args.items(): + try: + if pruned_call_args[k] == v: + del pruned_call_args[k] + except KeyError: continue + + fn._partial_call_args.update(self._partial_call_args) + fn._partial_call_args.update(pruned_call_args) + fn._partial_baked_args.extend(self._partial_baked_args) + sep = pruned_call_args.get("long_sep", self._call_args["long_sep"]) + fn._partial_baked_args.extend(self._compile_args(args, kwargs, sep)) + return fn + + def __str__(self): + if IS_PY3: return self.__unicode__() + else: return unicode(self).encode(DEFAULT_ENCODING) + + def __eq__(self, other): + try: return str(self) == str(other) + except: return False + + def __repr__(self): + return "<Command %r>" % str(self) + + def __unicode__(self): + baked_args = " ".join(self._partial_baked_args) + if baked_args: baked_args = " " + baked_args + return self._path + baked_args + + def __enter__(self): + self(_with=True) + + def __exit__(self, typ, value, traceback): + Command._prepend_stack.pop() + + + def __call__(self, *args, **kwargs): + kwargs = kwargs.copy() + args = list(args) + + cmd = [] + + # aggregate any 'with' contexts + call_args = Command._call_args.copy() + for prepend in self._prepend_stack: + # don't pass the 'with' call arg + pcall_args = prepend.call_args.copy() + try: del pcall_args["with"] + except: pass + + call_args.update(pcall_args) + cmd.extend(prepend.cmd) + + cmd.append(self._path) + + # here we extract the special kwargs and override any + # special kwargs from the possibly baked command + tmp_call_args, kwargs = self._extract_call_args(kwargs, self._partial_call_args) + call_args.update(tmp_call_args) + + if not isinstance(call_args["ok_code"], (tuple, list)): + call_args["ok_code"] = [call_args["ok_code"]] + + + # check if we're piping via composition + stdin = call_args["in"] + if args: + first_arg = args.pop(0) + if isinstance(first_arg, RunningCommand): + # it makes sense that if the input pipe of a command is running + # in the background, then this command should run in the + # background as well + if first_arg.call_args["bg"]: call_args["bg"] = True + stdin = first_arg.process._pipe_queue + + else: + args.insert(0, first_arg) + + processed_args = self._compile_args(args, kwargs, call_args["long_sep"]) + + # makes sure our arguments are broken up correctly + split_args = self._partial_baked_args + processed_args + + final_args = split_args + + cmd.extend(final_args) + + + # stdout redirection + stdout = call_args["out"] + if stdout \ + and not callable(stdout) \ + and not hasattr(stdout, "write") \ + and not isinstance(stdout, (cStringIO, StringIO)): + + stdout = open(str(stdout), "wb") + + + # stderr redirection + stderr = call_args["err"] + if stderr and not callable(stderr) and not hasattr(stderr, "write") \ + and not isinstance(stderr, (cStringIO, StringIO)): + stderr = open(str(stderr), "wb") + + + return RunningCommand(cmd, call_args, stdin, stdout, stderr) + + + + +# used in redirecting +STDOUT = -1 +STDERR = -2 + + + +# Process open = Popen +# Open Process = OProc +class OProc(object): + _procs_to_cleanup = set() + _registered_cleanup = False + _default_window_size = (24, 80) + + def __init__(self, cmd, stdin, stdout, stderr, call_args, + persist=False, pipe=STDOUT): + + self.call_args = call_args + + self._single_tty = self.call_args["tty_in"] and self.call_args["tty_out"] + + # this logic is a little convoluted, but basically this top-level + # if/else is for consolidating input and output TTYs into a single + # TTY. this is the only way some secure programs like ssh will + # output correctly (is if stdout and stdin are both the same TTY) + if self._single_tty: + self._stdin_fd, self._slave_stdin_fd = pty.openpty() + + self._stdout_fd = self._stdin_fd + self._slave_stdout_fd = self._slave_stdin_fd + + self._stderr_fd = self._stdin_fd + self._slave_stderr_fd = self._slave_stdin_fd + + # do not consolidate stdin and stdout + else: + if self.call_args["tty_in"]: + self._slave_stdin_fd, self._stdin_fd = pty.openpty() + else: + self._slave_stdin_fd, self._stdin_fd = os.pipe() + + # tty_out is usually the default + if self.call_args["tty_out"]: + self._stdout_fd, self._slave_stdout_fd = pty.openpty() + else: + self._stdout_fd, self._slave_stdout_fd = os.pipe() + + # unless STDERR is going to STDOUT, it ALWAYS needs to be a pipe, + # and never a PTY. the reason for this is not totally clear to me, + # but it has to do with the fact that if STDERR isn't set as the + # CTTY (because STDOUT is), the STDERR buffer won't always flush + # by the time the process exits, and the data will be lost. + # i've only seen this on OSX. + if stderr is not STDOUT: + self._stderr_fd, self._slave_stderr_fd = os.pipe() + + gc_enabled = gc.isenabled() + if gc_enabled: gc.disable() + self.pid = os.fork() + + + # child + if self.pid == 0: + # this piece of ugliness is due to a bug where we can lose output + # if we do os.close(self._slave_stdout_fd) in the parent after + # the child starts writing. + # see http://bugs.python.org/issue15898 + if IS_OSX and IS_PY3: _time.sleep(0.01) + + os.setsid() + + if self.call_args["tty_out"]: + # set raw mode, so there isn't any weird translation of newlines + # to \r\n and other oddities. we're not outputting to a terminal + # anyways + # + # we HAVE to do this here, and not in the parent thread, because + # we have to guarantee that this is set before the child process + # is run, and we can't do it twice. + tty.setraw(self._stdout_fd) + + + os.close(self._stdin_fd) + if not self._single_tty: + os.close(self._stdout_fd) + if stderr is not STDOUT: os.close(self._stderr_fd) + + + if self.call_args["cwd"]: os.chdir(self.call_args["cwd"]) + os.dup2(self._slave_stdin_fd, 0) + os.dup2(self._slave_stdout_fd, 1) + + # we're not directing stderr to stdout? then set self._slave_stderr_fd to + # fd 2, the common stderr fd + if stderr is STDOUT: os.dup2(self._slave_stdout_fd, 2) + else: os.dup2(self._slave_stderr_fd, 2) + + # don't inherit file descriptors + max_fd = resource.getrlimit(resource.RLIMIT_NOFILE)[0] + os.closerange(3, max_fd) + + + # set our controlling terminal + if self.call_args["tty_out"]: + tmp_fd = os.open(os.ttyname(1), os.O_RDWR) + os.close(tmp_fd) + + + if self.call_args["tty_out"]: + self.setwinsize(1) + + # actually execute the process + if self.call_args["env"] is None: os.execv(cmd[0], cmd) + else: os.execve(cmd[0], cmd, self.call_args["env"]) + + os._exit(255) + + # parent + else: + if gc_enabled: gc.enable() + + if not OProc._registered_cleanup: + atexit.register(OProc._cleanup_procs) + OProc._registered_cleanup = True + + + self.started = _time.time() + self.cmd = cmd + self.exit_code = None + + self.stdin = stdin or Queue() + self._pipe_queue = Queue() + + # this is used to prevent a race condition when we're waiting for + # a process to end, and the OProc's internal threads are also checking + # for the processes's end + self._wait_lock = threading.Lock() + + # these are for aggregating the stdout and stderr. we use a deque + # because we don't want to overflow + self._stdout = deque(maxlen=self.call_args["internal_bufsize"]) + self._stderr = deque(maxlen=self.call_args["internal_bufsize"]) + + if self.call_args["tty_in"]: self.setwinsize(self._stdin_fd) + + + self.log = Logger("process", repr(self)) + + os.close(self._slave_stdin_fd) + if not self._single_tty: + os.close(self._slave_stdout_fd) + if stderr is not STDOUT: os.close(self._slave_stderr_fd) + + self.log.debug("started process") + if not persist: OProc._procs_to_cleanup.add(self) + + + if self.call_args["tty_in"]: + attr = termios.tcgetattr(self._stdin_fd) + attr[3] &= ~termios.ECHO + termios.tcsetattr(self._stdin_fd, termios.TCSANOW, attr) + + # this represents the connection from a Queue object (or whatever + # we're using to feed STDIN) to the process's STDIN fd + self._stdin_stream = StreamWriter("stdin", self, self._stdin_fd, + self.stdin, self.call_args["in_bufsize"]) + + + stdout_pipe = None + if pipe is STDOUT and not self.call_args["no_pipe"]: + stdout_pipe = self._pipe_queue + + # this represents the connection from a process's STDOUT fd to + # wherever it has to go, sometimes a pipe Queue (that we will use + # to pipe data to other processes), and also an internal deque + # that we use to aggregate all the output + save_stdout = not self.call_args["no_out"] and \ + (self.call_args["tee"] in (True, "out") or stdout is None) + self._stdout_stream = StreamReader("stdout", self, self._stdout_fd, stdout, + self._stdout, self.call_args["out_bufsize"], stdout_pipe, + save_data=save_stdout) + + + if stderr is STDOUT or self._single_tty: self._stderr_stream = None + else: + stderr_pipe = None + if pipe is STDERR and not self.call_args["no_pipe"]: + stderr_pipe = self._pipe_queue + + save_stderr = not self.call_args["no_err"] and \ + (self.call_args["tee"] in ("err",) or stderr is None) + self._stderr_stream = StreamReader("stderr", self, self._stderr_fd, stderr, + self._stderr, self.call_args["err_bufsize"], stderr_pipe, + save_data=save_stderr) + + # start the main io threads + self._input_thread = self._start_thread(self.input_thread, self._stdin_stream) + self._output_thread = self._start_thread(self.output_thread, self._stdout_stream, self._stderr_stream) + + + def __repr__(self): + return "<Process %d %r>" % (self.pid, self.cmd[:500]) + + + # also borrowed from pexpect.py + @staticmethod + def setwinsize(fd): + rows, cols = OProc._default_window_size + TIOCSWINSZ = getattr(termios, 'TIOCSWINSZ', -2146929561) + if TIOCSWINSZ == 2148037735: # L is not required in Python >= 2.2. + TIOCSWINSZ = -2146929561 # Same bits, but with sign. + + s = struct.pack('HHHH', rows, cols, 0, 0) + fcntl.ioctl(fd, TIOCSWINSZ, s) + + + @staticmethod + def _start_thread(fn, *args): + thrd = threading.Thread(target=fn, args=args) + thrd.daemon = True + thrd.start() + return thrd + + def in_bufsize(self, buf): + self._stdin_stream.stream_bufferer.change_buffering(buf) + + def out_bufsize(self, buf): + self._stdout_stream.stream_bufferer.change_buffering(buf) + + def err_bufsize(self, buf): + if self._stderr_stream: + self._stderr_stream.stream_bufferer.change_buffering(buf) + + + def input_thread(self, stdin): + done = False + while not done and self.alive: + self.log.debug("%r ready for more input", stdin) + done = stdin.write() + + stdin.close() + + + def output_thread(self, stdout, stderr): + readers = [] + errors = [] + + if stdout is not None: + readers.append(stdout) + errors.append(stdout) + if stderr is not None: + readers.append(stderr) + errors.append(stderr) + + while readers: + outputs, inputs, err = select.select(readers, [], errors, 0.1) + + # stdout and stderr + for stream in outputs: + self.log.debug("%r ready to be read from", stream) + done = stream.read() + if done: readers.remove(stream) + + for stream in err: + pass + + # test if the process has been running too long + if self.call_args["timeout"]: + now = _time.time() + if now - self.started > self.call_args["timeout"]: + self.log.debug("we've been running too long") + self.kill() + + + # this is here because stdout may be the controlling TTY, and + # we can't close it until the process has ended, otherwise the + # child will get SIGHUP. typically, if we've broken out of + # the above loop, and we're here, the process is just about to + # end, so it's probably ok to aggressively poll self.alive + # + # the other option to this would be to do the CTTY close from + # the method that does the actual os.waitpid() call, but the + # problem with that is that the above loop might still be + # running, and closing the fd will cause some operation to + # fail. this is less complex than wrapping all the ops + # in the above loop with out-of-band fd-close exceptions + while self.alive: _time.sleep(0.001) + if stdout: stdout.close() + if stderr: stderr.close() + + + @property + def stdout(self): + return "".encode(self.call_args["encoding"]).join(self._stdout) + + @property + def stderr(self): + return "".encode(self.call_args["encoding"]).join(self._stderr) + + + def signal(self, sig): + self.log.debug("sending signal %d", sig) + try: os.kill(self.pid, sig) + except OSError: pass + + def kill(self): + self.log.debug("killing") + self.signal(signal.SIGKILL) + + def terminate(self): + self.log.debug("terminating") + self.signal(signal.SIGTERM) + + @staticmethod + def _cleanup_procs(): + for proc in OProc._procs_to_cleanup: + proc.kill() + + + def _handle_exit_code(self, exit_code): + # if we exited from a signal, let our exit code reflect that + if os.WIFSIGNALED(exit_code): return -os.WTERMSIG(exit_code) + # otherwise just give us a normal exit code + elif os.WIFEXITED(exit_code): return os.WEXITSTATUS(exit_code) + else: raise RuntimeError("Unknown child exit status!") + + @property + def alive(self): + if self.exit_code is not None: return False + + # what we're doing here essentially is making sure that the main thread + # (or another thread), isn't calling .wait() on the process. because + # .wait() calls os.waitpid(self.pid, 0), we can't do an os.waitpid + # here...because if we did, and the process exited while in this + # thread, the main thread's os.waitpid(self.pid, 0) would raise OSError + # (because the process ended in another thread). + # + # so essentially what we're doing is, using this lock, checking if + # we're calling .wait(), and if we are, let .wait() get the exit code + # and handle the status, otherwise let us do it. + acquired = self._wait_lock.acquire(False) + if not acquired: + if self.exit_code is not None: return False + return True + + try: + # WNOHANG is just that...we're calling waitpid without hanging... + # essentially polling the process + pid, exit_code = os.waitpid(self.pid, os.WNOHANG) + if pid == self.pid: + self.exit_code = self._handle_exit_code(exit_code) + return False + + # no child process + except OSError: return False + else: return True + finally: self._wait_lock.release() + + + def wait(self): + self.log.debug("acquiring wait lock to wait for completion") + with self._wait_lock: + self.log.debug("got wait lock") + + if self.exit_code is None: + self.log.debug("exit code not set, waiting on pid") + pid, exit_code = os.waitpid(self.pid, 0) + self.exit_code = self._handle_exit_code(exit_code) + else: + self.log.debug("exit code already set (%d), no need to wait", self.exit_code) + + self._input_thread.join() + self._output_thread.join() + + OProc._procs_to_cleanup.discard(self) + + return self.exit_code + + + + +class DoneReadingStdin(Exception): pass +class NoStdinData(Exception): pass + + + +# this guy is for reading from some input (the stream) and writing to our +# opened process's stdin fd. the stream can be a Queue, a callable, something +# with the "read" method, a string, or an iterable +class StreamWriter(object): + def __init__(self, name, process, stream, stdin, bufsize): + self.name = name + self.process = weakref.ref(process) + self.stream = stream + self.stdin = stdin + + self.log = Logger("streamwriter", repr(self)) + + + self.stream_bufferer = StreamBufferer(self.process().call_args["encoding"], + bufsize) + + # determine buffering for reading from the input we set for stdin + if bufsize == 1: self.bufsize = 1024 + elif bufsize == 0: self.bufsize = 1 + else: self.bufsize = bufsize + + + if isinstance(stdin, Queue): + log_msg = "queue" + self.get_chunk = self.get_queue_chunk + + elif callable(stdin): + log_msg = "callable" + self.get_chunk = self.get_callable_chunk + + # also handles stringio + elif hasattr(stdin, "read"): + log_msg = "file descriptor" + self.get_chunk = self.get_file_chunk + + elif isinstance(stdin, basestring): + log_msg = "string" + + if bufsize == 1: + # TODO, make the split() be a generator + self.stdin = iter((c+"\n" for c in stdin.split("\n"))) + else: + self.stdin = iter(stdin[i:i+self.bufsize] for i in range(0, len(stdin), self.bufsize)) + self.get_chunk = self.get_iter_chunk + + else: + log_msg = "general iterable" + self.stdin = iter(stdin) + self.get_chunk = self.get_iter_chunk + + self.log.debug("parsed stdin as a %s", log_msg) + + + def __repr__(self): + return "<StreamWriter %s for %r>" % (self.name, self.process()) + + def fileno(self): + return self.stream + + def get_queue_chunk(self): + try: chunk = self.stdin.get(True, 0.01) + except Empty: raise NoStdinData + if chunk is None: raise DoneReadingStdin + return chunk + + def get_callable_chunk(self): + try: return self.stdin() + except: raise DoneReadingStdin + + def get_iter_chunk(self): + try: + if IS_PY3: return self.stdin.__next__() + else: return self.stdin.next() + except StopIteration: raise DoneReadingStdin + + def get_file_chunk(self): + if self.stream_bufferer.type == 1: chunk = self.stdin.readline() + else: chunk = self.stdin.read(self.bufsize) + if not chunk: raise DoneReadingStdin + else: return chunk + + + # the return value answers the questions "are we done writing forever?" + def write(self): + # get_chunk may sometimes return bytes, and sometimes returns trings + # because of the nature of the different types of STDIN objects we + # support + try: chunk = self.get_chunk() + except DoneReadingStdin: + self.log.debug("done reading") + + if self.process().call_args["tty_in"]: + # EOF time + try: char = termios.tcgetattr(self.stream)[6][termios.VEOF] + except: char = chr(4).encode() + os.write(self.stream, char) + + return True + + except NoStdinData: + self.log.debug("received no data") + return False + + # if we're not bytes, make us bytes + if IS_PY3 and hasattr(chunk, "encode"): + chunk = chunk.encode(self.process().call_args["encoding"]) + + for chunk in self.stream_bufferer.process(chunk): + self.log.debug("got chunk size %d: %r", len(chunk), chunk[:30]) + + self.log.debug("writing chunk to process") + try: + os.write(self.stream, chunk) + except OSError: + self.log.debug("OSError writing stdin chunk") + return True + + + def close(self): + self.log.debug("closing, but flushing first") + chunk = self.stream_bufferer.flush() + self.log.debug("got chunk size %d to flush: %r", len(chunk), chunk[:30]) + try: + if chunk: os.write(self.stream, chunk) + if not self.process().call_args["tty_in"]: + self.log.debug("we used a TTY, so closing the stream") + os.close(self.stream) + except OSError: pass + + + +class StreamReader(object): + def __init__(self, name, process, stream, handler, buffer, bufsize, + pipe_queue=None, save_data=True): + self.name = name + self.process = weakref.ref(process) + self.stream = stream + self.buffer = buffer + self.save_data = save_data + self.encoding = process.call_args["encoding"] + self.decode_errors = process.call_args["decode_errors"] + + self.pipe_queue = None + if pipe_queue: self.pipe_queue = weakref.ref(pipe_queue) + + self.log = Logger("streamreader", repr(self)) + + self.stream_bufferer = StreamBufferer(self.encoding, bufsize, + self.decode_errors) + + # determine buffering + if bufsize == 1: self.bufsize = 1024 + elif bufsize == 0: self.bufsize = 1 + else: self.bufsize = bufsize + + + # here we're determining the handler type by doing some basic checks + # on the handler object + self.handler = handler + if callable(handler): self.handler_type = "fn" + elif isinstance(handler, StringIO): self.handler_type = "stringio" + elif isinstance(handler, cStringIO): + self.handler_type = "cstringio" + elif hasattr(handler, "write"): self.handler_type = "fd" + else: self.handler_type = None + + + self.should_quit = False + + # here we choose how to call the callback, depending on how many + # arguments it takes. the reason for this is to make it as easy as + # possible for people to use, without limiting them. a new user will + # assume the callback takes 1 argument (the data). as they get more + # advanced, they may want to terminate the process, or pass some stdin + # back, and will realize that they can pass a callback of more args + if self.handler_type == "fn": + implied_arg = 0 + if inspect.ismethod(handler): + implied_arg = 1 + num_args = len(inspect.getargspec(handler).args) + + else: + if inspect.isfunction(handler): + num_args = len(inspect.getargspec(handler).args) + + # is an object instance with __call__ method + else: + implied_arg = 1 + num_args = len(inspect.getargspec(handler.__call__).args) + + + self.handler_args = () + if num_args == implied_arg + 2: + self.handler_args = (self.process().stdin,) + elif num_args == implied_arg + 3: + self.handler_args = (self.process().stdin, self.process) + + + def fileno(self): + return self.stream + + def __repr__(self): + return "<StreamReader %s for %r>" % (self.name, self.process()) + + def close(self): + chunk = self.stream_bufferer.flush() + self.log.debug("got chunk size %d to flush: %r", + len(chunk), chunk[:30]) + if chunk: self.write_chunk(chunk) + + if self.handler_type == "fd" and hasattr(self.handler, "close"): + self.handler.flush() + + if self.pipe_queue and self.save_data: self.pipe_queue().put(None) + try: os.close(self.stream) + except OSError: pass + + + def write_chunk(self, chunk): + # in PY3, the chunk coming in will be bytes, so keep that in mind + + if self.handler_type == "fn" and not self.should_quit: + # try to use the encoding first, if that doesn't work, send + # the bytes, because it might be binary + try: to_handler = chunk.decode(self.encoding, self.decode_errors) + except UnicodeDecodeError: to_handler = chunk + + # this is really ugly, but we can't store self.process as one of + # the handler args in self.handler_args, the reason being is that + # it would create cyclic references, and prevent objects from + # being garbage collected. so we're determining if this handler + # even requires self.process (by the argument count), and if it + # does, resolving the weakref to a hard reference and passing + # that into the handler + handler_args = self.handler_args + if len(self.handler_args) == 2: + handler_args = (self.handler_args[0], self.process()) + self.should_quit = self.handler(to_handler, *handler_args) + + elif self.handler_type == "stringio": + self.handler.write(chunk.decode(self.encoding, self.decode_errors)) + + elif self.handler_type in ("cstringio", "fd"): + self.handler.write(chunk) + + + if self.save_data: + self.buffer.append(chunk) + + if self.pipe_queue: + self.log.debug("putting chunk onto pipe: %r", chunk[:30]) + self.pipe_queue().put(chunk) + + + def read(self): + # if we're PY3, we're reading bytes, otherwise we're reading + # str + try: chunk = os.read(self.stream, self.bufsize) + except OSError as e: + self.log.debug("got errno %d, done reading", e.errno) + return True + if not chunk: + self.log.debug("got no chunk, done reading") + return True + + self.log.debug("got chunk size %d: %r", len(chunk), chunk[:30]) + for chunk in self.stream_bufferer.process(chunk): + self.write_chunk(chunk) + + + + +# this is used for feeding in chunks of stdout/stderr, and breaking it up into +# chunks that will actually be put into the internal buffers. for example, if +# you have two processes, one being piped to the other, and you want that, +# first process to feed lines of data (instead of the chunks however they +# come in), OProc will use an instance of this class to chop up the data and +# feed it as lines to be sent down the pipe +class StreamBufferer(object): + def __init__(self, encoding=DEFAULT_ENCODING, buffer_type=1, + decode_errors="strict"): + # 0 for unbuffered, 1 for line, everything else for that amount + self.type = buffer_type + self.buffer = [] + self.n_buffer_count = 0 + self.encoding = encoding + self.decode_errors = decode_errors + + # this is for if we change buffering types. if we change from line + # buffered to unbuffered, its very possible that our self.buffer list + # has data that was being saved up (while we searched for a newline). + # we need to use that up, so we don't lose it + self._use_up_buffer_first = False + + # the buffering lock is used because we might chance the buffering + # types from a different thread. for example, if we have a stdout + # callback, we might use it to change the way stdin buffers. so we + # lock + self._buffering_lock = threading.RLock() + self.log = Logger("stream_bufferer") + + + def change_buffering(self, new_type): + # TODO, when we stop supporting 2.6, make this a with context + self.log.debug("acquiring buffering lock for changing buffering") + self._buffering_lock.acquire() + self.log.debug("got buffering lock for changing buffering") + try: + if new_type == 0: self._use_up_buffer_first = True + + self.type = new_type + finally: + self._buffering_lock.release() + self.log.debug("released buffering lock for changing buffering") + + + def process(self, chunk): + # MAKE SURE THAT THE INPUT IS PY3 BYTES + # THE OUTPUT IS ALWAYS PY3 BYTES + + # TODO, when we stop supporting 2.6, make this a with context + self.log.debug("acquiring buffering lock to process chunk (buffering: %d)", self.type) + self._buffering_lock.acquire() + self.log.debug("got buffering lock to process chunk (buffering: %d)", self.type) + try: + # we've encountered binary, permanently switch to N size buffering + # since matching on newline doesn't make sense anymore + if self.type == 1: + try: chunk.decode(self.encoding, self.decode_errors) + except: + self.log.debug("detected binary data, changing buffering") + self.change_buffering(1024) + + # unbuffered + if self.type == 0: + if self._use_up_buffer_first: + self._use_up_buffer_first = False + to_write = self.buffer + self.buffer = [] + to_write.append(chunk) + return to_write + + return [chunk] + + # line buffered + elif self.type == 1: + total_to_write = [] + chunk = chunk.decode(self.encoding, self.decode_errors) + while True: + newline = chunk.find("\n") + if newline == -1: break + + chunk_to_write = chunk[:newline+1] + if self.buffer: + # this is ugly, but it's designed to take the existing + # bytes buffer, join it together, tack on our latest + # chunk, then convert the whole thing to a string. + # it's necessary, i'm sure. read the whole block to + # see why. + chunk_to_write = "".encode(self.encoding).join(self.buffer) \ + + chunk_to_write.encode(self.encoding) + chunk_to_write = chunk_to_write.decode(self.encoding) + + self.buffer = [] + self.n_buffer_count = 0 + + chunk = chunk[newline+1:] + total_to_write.append(chunk_to_write.encode(self.encoding)) + + if chunk: + self.buffer.append(chunk.encode(self.encoding)) + self.n_buffer_count += len(chunk) + return total_to_write + + # N size buffered + else: + total_to_write = [] + while True: + overage = self.n_buffer_count + len(chunk) - self.type + if overage >= 0: + ret = "".encode(self.encoding).join(self.buffer) + chunk + chunk_to_write = ret[:self.type] + chunk = ret[self.type:] + total_to_write.append(chunk_to_write) + self.buffer = [] + self.n_buffer_count = 0 + else: + self.buffer.append(chunk) + self.n_buffer_count += len(chunk) + break + return total_to_write + finally: + self._buffering_lock.release() + self.log.debug("released buffering lock for processing chunk (buffering: %d)", self.type) + + + def flush(self): + self.log.debug("acquiring buffering lock for flushing buffer") + self._buffering_lock.acquire() + self.log.debug("got buffering lock for flushing buffer") + try: + ret = "".encode(self.encoding).join(self.buffer) + self.buffer = [] + return ret + finally: + self._buffering_lock.release() + self.log.debug("released buffering lock for flushing buffer") + + + + + +# this allows lookups to names that aren't found in the global scope to be +# searched for as a program name. for example, if "ls" isn't found in this +# module's scope, we consider it a system program and try to find it. +# +# we use a dict instead of just a regular object as the base class because +# the exec() statement used in this file requires the "globals" argument to +# be a dictionary +class Environment(dict): + def __init__(self, globs, baked_args={}): + self.globs = globs + self.baked_args = baked_args + + def __setitem__(self, k, v): + self.globs[k] = v + + def __getitem__(self, k): + try: return self.globs[k] + except KeyError: pass + + # the only way we'd get to here is if we've tried to + # import * from a repl. so, raise an exception, since + # that's really the only sensible thing to do + if k == "__all__": + raise ImportError("Cannot import * from sh. \ +Please import sh or import programs individually.") + + # if we end with "_" just go ahead and skip searching + # our namespace for python stuff. this was mainly for the + # command "id", which is a popular program for finding + # if a user exists, but also a python function for getting + # the address of an object. so can call the python + # version by "id" and the program version with "id_" + if not k.endswith("_"): + # check if we're naming a dynamically generated ReturnCode exception + try: return rc_exc_cache[k] + except KeyError: + m = rc_exc_regex.match(k) + if m: + exit_code = int(m.group(2)) + if m.group(1) == "SignalException": exit_code = -exit_code + return get_rc_exc(exit_code) + + # is it a builtin? + try: return getattr(self["__builtins__"], k) + except AttributeError: pass + elif not k.startswith("_"): k = k.rstrip("_") + + + # https://github.com/ipython/ipython/issues/2577 + # https://github.com/amoffat/sh/issues/97#issuecomment-10610629 + if k.startswith("__") and k.endswith("__"): + raise AttributeError + + # how about an environment variable? + try: return os.environ[k] + except KeyError: pass + + # is it a custom builtin? + builtin = getattr(self, "b_"+k, None) + if builtin: return builtin + + # it must be a command then + # we use _create instead of instantiating the class directly because + # _create uses resolve_program, which will automatically do underscore- + # to-dash conversions. instantiating directly does not use that + return Command._create(k, **self.baked_args) + + + # methods that begin with "b_" are custom builtins and will override any + # program that exists in our path. this is useful for things like + # common shell builtins that people are used to, but which aren't actually + # full-fledged system binaries + + def b_cd(self, path): + os.chdir(path) + + def b_which(self, program): + return which(program) + + + + +def run_repl(env): + banner = "\n>> sh v{version}\n>> https://github.com/amoffat/sh\n" + + print(banner.format(version=__version__)) + while True: + try: line = raw_input("sh> ") + except (ValueError, EOFError): break + + try: exec(compile(line, "<dummy>", "single"), env, env) + except SystemExit: break + except: print(traceback.format_exc()) + + # cleans up our last line + print("") + + + + +# this is a thin wrapper around THIS module (we patch sys.modules[__name__]). +# this is in the case that the user does a "from sh import whatever" +# in other words, they only want to import certain programs, not the whole +# system PATH worth of commands. in this case, we just proxy the +# import lookup to our Environment class +class SelfWrapper(ModuleType): + def __init__(self, self_module, baked_args={}): + # this is super ugly to have to copy attributes like this, + # but it seems to be the only way to make reload() behave + # nicely. if i make these attributes dynamic lookups in + # __getattr__, reload sometimes chokes in weird ways... + for attr in ["__builtins__", "__doc__", "__name__", "__package__"]: + setattr(self, attr, getattr(self_module, attr, None)) + + # python 3.2 (2.7 and 3.3 work fine) breaks on osx (not ubuntu) + # if we set this to None. and 3.3 needs a value for __path__ + self.__path__ = [] + self.self_module = self_module + self.env = Environment(globals(), baked_args) + + def __setattr__(self, name, value): + if hasattr(self, "env"): self.env[name] = value + ModuleType.__setattr__(self, name, value) + + def __getattr__(self, name): + if name == "env": raise AttributeError + return self.env[name] + + # accept special keywords argument to define defaults for all operations + # that will be processed with given by return SelfWrapper + def __call__(self, **kwargs): + return SelfWrapper(self.self_module, kwargs) + + + + +# we're being run as a stand-alone script +if __name__ == "__main__": + try: arg = sys.argv.pop(1) + except: arg = None + + if arg == "test": + import subprocess + + def run_test(version): + py_version = "python%s" % version + py_bin = which(py_version) + + if py_bin: + print("Testing %s" % py_version.capitalize()) + + p = subprocess.Popen([py_bin, os.path.join(THIS_DIR, "test.py")] + + sys.argv[1:]) + p.wait() + else: + print("Couldn't find %s, skipping" % py_version.capitalize()) + + versions = ("2.6", "2.7", "3.1", "3.2", "3.3") + for version in versions: run_test(version) + + else: + env = Environment(globals()) + run_repl(env) + +# we're being imported from somewhere +else: + self = sys.modules[__name__] + sys.modules[__name__] = SelfWrapper(self)
new file mode 100644 index 0000000000000000000000000000000000000000..6fe9bcf19332ac1d2afa17541dd8edeb30960653 GIT binary patch literal 41826 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%0D6NpK9 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
new file mode 100755 --- /dev/null +++ b/ss/bin/scansetup @@ -0,0 +1,22 @@ +#!/usr/bin/perl + +eval 'exec /usr/bin/perl -S $0 ${1+"$@"}' + if 0; # not running under some shell + +use FindBin; +use lib "$FindBin::Bin/../lib/perl5"; +use NeuroRx::ScanSetup::IdentifyModality; +use NeuroRx::ScanSetup::RenameMincFiles; + +$SIG{__DIE__} = sub{ + if ($logger) { + } +}; +END{ + chdir($origdir); +} +sub SetupDTI +sub DecompressDicom { +sub OrganizeMinc {} +sub ConvertToMinc { +}
new file mode 100644 --- /dev/null +++ b/ss/lib/perl5/NeuroRx/ScanSetup.pm @@ -0,0 +1,358 @@ +package NeuroRx::ScanSetup; +use strict; + +BEGIN { + use Exporter (); + use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); + $VERSION = '1.00'; + @ISA = qw(Exporter); + #Give a hoot don't pollute, do not export more than needed by default + @EXPORT = qw(GetSystemOutput RunAndLog GetSystemOutputArray ParseHeader + GetDimensions GetScanDescription ParseDicom GetFileroot + GetUser GetPath unlink_glob); + @EXPORT_OK = qw(get_start_time logfilename get_pm $logger $trial + $email_target $scanpath); + %EXPORT_TAGS = (); +} + + +#################### subroutine header begin #################### + +=head2 sample_function + + Usage : How to use this function/method + Purpose : What it does + Returns : What it returns + Argument : What it wants to know + Throws : Exceptions and other anomolies + Comment : This is a sample subroutine header. + : It is polite to include more pod and fewer comments. + +See Also : + +=cut + +#################### subroutine header end #################### + +our $trial; +our $logger; +our $email_target; +our $scanpath; + +sub new{ + my ($class, %parameters) = @_; + my $self = bless ({}, ref ($class) || $class); + + return $self; +} + +my $starttime; +sub get_start_time{ + if(!$starttime){ + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); + $starttime = sprintf("%04d-%02d-%02d_%02d:%02d:%02d", + $year+1900, $mon+1, $mday, $hour, $min, $sec); + } + return $starttime; +} + +sub logfilename{ + return "/var/log/scansetup/scansetup_".get_start_time().".log"; +} + +sub get_pm { + my ($which_trial) = (@_); + $which_trial = $trial unless $which_trial; + + if ($email_target){ + return [$email_target]; + } + + my $default; + + open TRIALLIST, "/trials/quarantine/common/lists/email.list" + or return $default; + + my %trial_pms; + while (<TRIALLIST>) { + next if /^\s*(#.*)?$/; + chomp; + my @line = split /,/; + next if @line < 2; + + $trial_pms{$line[0]} = [@line[1..$#line]]; + } + close TRIALLIST; + + open SUBLIST, "/trials/quarantine/common/lists/user_substitutions.list" + or return $default; + + while (<SUBLIST>) { + next if /^\s*(#.*)?$/; + chomp; + my @line = split /\./; + + my $from = $line[3]; + my $to = $line[5]; + my $until = $line[7]; + + my ($sec,$min,$hour,$day,$mon,$year) = localtime(time); + my $today = sprintf("%04d-%02d-%02d",$year+1900,$mon+1,$day); + + if ($today lt $until) { + foreach my $pms(keys %trial_pms){ + ## Replace all usages of absent PM with substituting PM + map {s/$from/$to/;} @{$trial_pms{$pms}}; + } + } + } + close SUBLIST; + + if (defined $trial_pms{$which_trial}){ + my @pm_emails = @{$trial_pms{$which_trial}}; + + foreach my $pm_email(@pm_emails){ + $pm_email .= '@neurorx.com' unless $pm_email =~ /@/; + } + + return \@pm_emails; + } + + return $default; +} + +use Cwd qw(getcwd); + +sub GetSystemOutput{ + my ($input, $exit_codes) = @_; + + my @args = split(" ", $input); + my $prog = which($args[0]); + $logger -> logdie("$args[0]: program not found") unless $prog; + + my ($output, $err, $success, $code) = capture_exec($input); + + if($exit_codes){ + if(! grep(/$code/, @$exit_codes)){ + $logger -> error($err); + $logger -> logdie("command `$input' failed with exit code $code"); + } + } + + chomp $output; + + return wantarray ? ($output, $err) : $output; +} + +sub RunAndLog{ + my @args = @_; + my $prog = which($args[0]); + my ($pkg, $filename, $line) = caller; + $logger -> logdie("$args[0]: program not found") unless $prog; + + my $cmd = join(" ", @args); + $logger -> debug($cmd); + $logger -> debug("Called from: $filename:$line"); + my ($msg, $err) = capture_exec(@args); + $logger -> debug($msg) if $msg; + if($err + ## This warning is from mincconcat and too frequent to be a warning + && !($err =~ m/Don't use an image dimension as a loop dimension/)){ + $logger -> warn("Problem running $cmd:"); + $logger -> warn($err); + } +} + +sub GetSystemOutputArray { + + my ($InputString,@SysOut); + + $InputString = $_[0]; + + open (SYSOUT, "$InputString |"); + @SysOut = <SYSOUT>; + close(SYSOUT); + chomp(@SysOut); + + return(@SysOut); +} + +my %mincheader_cache; +sub ParseHeader { + my ($file, $header) = @_; + + my $line; + if (! $mincheader_cache{$file}) { + $logger -> debug ("Cache miss! Running mincheader on $file"); + $line = GetSystemOutput("mincheader $file"); + $mincheader_cache{$file} = $line; + } + else{ + $line = $mincheader_cache{$file}; + } + my @out = ($line =~ m/$header = "?([^\n;"]*)"?/); + $out[0] =~ s/\s*$// if $out[0]; + my $out = $out[0] || "NotPresent"; + + $out =~ s/\s*$//; + + return $out; + +} + +sub GetDimensions { + my ($file, $dimension) = @_; + + my $line = GetSystemOutput("mincheader $file"); + + my @out = ($line =~ m/$dimension = "?([^\n;"]*)"?/); + my $out = $out[0] || ""; + + $out =~ s/\s*//; + + return $out; +} + +sub GetScanDescription { + my ($file) = @_; + + my ($lines, $err, $success, $exit_code) = capture_exec("mincheader", $file); + if($err){ + $logger -> error ($err); + } + if(! $success){ + $logger -> logdie ("`mincheader' failed with exit code $exit_code"); + } + + my @lines = split(/\n/, $lines); + @lines = grep(/dicom_0x0008:el_0x103e/, @lines); + + return "NotInHeader" unless @lines; + + $lines[0] =~ m/\s+(.+);/ or return "NotInHeader"; + my $out = $1; + $out =~ s,[/"\s],,g; + + return ($out || "NotInHeader"); +} + +sub ParseDicom { + use strict; + my ($file, $field) = @_; + + $logger -> debug("Looking for field $field in file $file"); + my ($val, $err) = capture_exec("dicomhdr", $field, $file); + + if($err){ + $val = "InvalidValue"; + $logger -> warn("Failed parsing DICOM file:"); + my @err = split(/\n/, $err); + foreach $err(@err){ + $logger -> warn("\t$err"); + } + } + + $logger -> debug ("Returning value: $val"); + return($val); +} + +sub GetFileroot { + my ($pwd) = @_; + my @pwd = split(/\//,$pwd); + $logger -> logdie("Not a trial directory: $pwd") if @pwd < 6; + + my $fileroot = $pwd[2]."_".$pwd[3]."_".$pwd[4]."_".$pwd[5]; + return($fileroot); +} + +sub GetUser { + my @outargs = getpwuid($<); + my $user = $outargs[6]; + $user =~ s/\s/_/g; + return $user; +} + +sub GetPath { + my ($fname) = @_; + + my @f = split(/\//,$fname); + $fname = $f[$#f]; + + my @fname = split(/_/,$fname); + + my $filepath = "/trials/".$fname[0]."/".$fname[1]."/".$fname[2]."_".$fname[3]."/".$fname[4]; + return($filepath); +} + + +sub unlink_glob { + my $glob = $_[0]; + my $cwd = getcwd(); + + $logger -> debug("Removing $glob from $cwd..."); + + foreach my $file( glob($glob)){ + unlink($file) or die "Couldn't unlink ${file}: $!"; + $logger -> debug("Deleted $file"); + } +} + +#################### main pod documentation begin ################### +## Below is the stub of documentation for your module. +## You better edit it! + + +=head1 NAME + +NeuroRx::ScanSetup - Converts incoming DICOM images to MINC + +=head1 SYNOPSIS + + use NeuroRx::ScanSetup; + blah blah blah + + +=head1 DESCRIPTION + + + +=head1 USAGE + + + +=head1 BUGS + + + +=head1 SUPPORT + + + +=head1 AUTHOR + + Samson Antel + CPAN ID: MODAUTHOR + NeuroRx + samson@neurorx.com + +=head1 COPYRIGHT + +This program is free software; you can redistribute +it and/or modify it under the same terms as Perl itself. + +The full text of the license can be found in the +LICENSE file included with this module. + + +=head1 SEE ALSO + +perl(1). + +=cut + +#################### main pod documentation end ################### + + +1; + +
new file mode 100644 --- /dev/null +++ b/ss/lib/perl5/NeuroRx/ScanSetup/IdentifyModality.pm @@ -0,0 +1,8 @@ +use strict; +use warnings; + + +sub IdentifyModality{ +} + +1;