summaryrefslogtreecommitdiff
path: root/.i3
diff options
context:
space:
mode:
authorRené 'Necoro' Neumann <necoro@necoro.net>2013-06-06 17:35:45 +0200
committerRené 'Necoro' Neumann <necoro@necoro.net>2013-06-06 17:35:45 +0200
commit93ca7a4258e83996c0075ff5976658a12ffb6e02 (patch)
tree393833395c7c3237cd162fbbbad48c8cf5b5bc49 /.i3
parent346139a617b044329efcc0a17fdbdf11cf31971e (diff)
downloaddotfiles-93ca7a4258e83996c0075ff5976658a12ffb6e02.tar.gz
dotfiles-93ca7a4258e83996c0075ff5976658a12ffb6e02.tar.bz2
dotfiles-93ca7a4258e83996c0075ff5976658a12ffb6e02.zip
i3: unify scripts into one
Diffstat (limited to '.i3')
-rw-r--r--.i3/config4
-rw-r--r--.i3/scripts/libs/i3.py (renamed from .i3/scripts/i3.py)0
-rw-r--r--.i3/scripts/libs/sh.py1695
-rwxr-xr-x.i3/scripts/new_workspace.py23
-rwxr-xr-x.i3/scripts/workspaces.py74
-rwxr-xr-x.i3/scripts/workspaces.sh24
6 files changed, 1771 insertions, 49 deletions
diff --git a/.i3/config b/.i3/config
index f72bdc4..d0cc49a 100644
--- a/.i3/config
+++ b/.i3/config
@@ -149,14 +149,14 @@ bindsym $mod+Tab workspace back_and_forth
bindsym $mod+Shift+Tab move container to workspace back_and_forth
# goto a specific workspace, via i3-input «3
-bindsym $mod+g exec $nsi $script/workspaces.sh
+bindsym $mod+g exec $nsi $script/workspaces.py switch
bindsym $mod+Shift+g exec $nsi $script/workspaces.sh move
# rename «3
bindsym $mod+Shift+n exec $nsi i3-input -F 'rename workspace to "%s"' -P 'Rename workspace: '
# new temp workspace «3
-bindsym $mod+n exec $nsi $script/new_workspace.py
+bindsym $mod+n exec $nsi $script/workspaces.py new
# Resizing «2
#############
diff --git a/.i3/scripts/i3.py b/.i3/scripts/libs/i3.py
index 343c709..343c709 100644
--- a/.i3/scripts/i3.py
+++ b/.i3/scripts/libs/i3.py
diff --git a/.i3/scripts/libs/sh.py b/.i3/scripts/libs/sh.py
new file mode 100644
index 0000000..0e46f14
--- /dev/null
+++ b/.i3/scripts/libs/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_bu