diff --git a/client/powerline b/client/powerline new file mode 100755 index 00000000..fd6de024 Binary files /dev/null and b/client/powerline differ diff --git a/client/powerline.c b/client/powerline.c new file mode 100644 index 00000000..cef562ec --- /dev/null +++ b/client/powerline.c @@ -0,0 +1,115 @@ +/* vim:fileencoding=utf-8:noet */ + +#include +#include +#include +#include +#include +#include +#include + +#define handle_error(msg) \ + do { perror(msg); exit(EXIT_FAILURE); } while (0) + +#ifndef TEMP_FAILURE_RETRY +#define TEMP_FAILURE_RETRY(expression) \ + ( \ + ({ long int __result; \ + do __result = (long int) (expression); \ + while (__result == -1L && errno == EINTR); \ + __result; })) +#endif + +extern char **environ; + +void do_write(int sd, const char *raw, int len) { + int written = 0, n = -1; + + while (written < len) { + n = TEMP_FAILURE_RETRY(write(sd, raw+written, len-written)); + if (n == -1) { + close(sd); + handle_error("write() failed"); + } + written += n; + } +} + +int main(int argc, char *argv[]) { + int sd = -1, i; + struct sockaddr_un server; + char address[50] = {}; + const char eof[2] = "\0\0"; + char buf[4096] = {}; + char *newargv[200] = {}; + char *wd = NULL; + char **envp; + + if (argc < 2) { printf("Must provide at least one argument.\n"); return EXIT_FAILURE; } + +#ifdef __APPLE__ + snprintf(address, 50, "/tmp/powerline-ipc-%d", getuid()); +#else + snprintf(address, 50, "powerline-ipc-%d", getuid()); +#endif + + sd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sd == -1) handle_error("socket() failed"); + + memset(&server, 0, sizeof(struct sockaddr_un)); // Clear + server.sun_family = AF_UNIX; +#ifdef __APPLE__ + strncpy(server.sun_path, address, strlen(address)); +#else + strncpy(server.sun_path+1, address, strlen(address)); +#endif + +#ifdef __APPLE__ + if (connect(sd, (struct sockaddr *) &server, sizeof(server.sun_family) + strlen(address)) < 0) { +#else + if (connect(sd, (struct sockaddr *) &server, sizeof(server.sun_family) + strlen(address)+1) < 0) { +#endif + close(sd); + // We failed to connect to the daemon, execute powerline instead + argc = (argc < 199) ? argc : 199; + for (i=1; i < argc; i++) newargv[i] = argv[i]; + newargv[0] = "powerline-render"; + newargv[argc] = NULL; + execvp("powerline-render", newargv); + } + + for (i = 1; i < argc; i++) { + do_write(sd, argv[i], strlen(argv[i])); + do_write(sd, eof, 1); + } + + for(envp=environ; *envp; envp++) { + do_write(sd, "--env=", 6); + do_write(sd, *envp, strlen(*envp)); + do_write(sd, eof, 1); + } + + wd = getcwd(NULL, 0); + if (wd != NULL) { + do_write(sd, "--cwd=", 6); + do_write(sd, wd, strlen(wd)); + free(wd); wd = NULL; + } + + do_write(sd, eof, 2); + + i = -1; + while (i != 0) { + i = TEMP_FAILURE_RETRY(read(sd, buf, 4096)); + if (i == -1) { + close(sd); + handle_error("read() failed"); + } + if (i > 0) + write(STDOUT_FILENO, buf, i) || 0; + } + + close(sd); + + return 0; +} diff --git a/client/powerline.py b/client/powerline.py new file mode 100755 index 00000000..bbcfffbe --- /dev/null +++ b/client/powerline.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import socket +import os +import errno + +if len(sys.argv) < 2: + print('Must provide at least one argument.', file=sys.stderr) + raise SystemExit(1) + +platform = sys.platform.lower() +use_filesystem = 'darwin' in platform +# use_filesystem = True +del platform + +address = ('/tmp/powerline-ipc-%d' if use_filesystem else '\0powerline-ipc-%d')%os.getuid() + +sock = socket.socket(family=socket.AF_UNIX) + + +def eintr_retry_call(func, *args, **kwargs): + while True: + try: + return func(*args, **kwargs) + except EnvironmentError as e: + if getattr(e, 'errno', None) == errno.EINTR: + continue + raise + +try: + eintr_retry_call(sock.connect, address) +except Exception: + # Run the powerline renderer + args = ['powerline-render'] + sys.argv[1:] + os.execvp('powerline-render', args) + +fenc = sys.getfilesystemencoding() or 'utf-8' +if fenc == 'ascii': + fenc = 'utf-8' + +args = [x.encode(fenc) if isinstance(x, type('')) else x for x in sys.argv[1:]] + +try: + cwd = os.getcwd() +except EnvironmentError: + pass +else: + if isinstance(cwd, type('')): + cwd = cwd.encode(fenc) + args.append(b'--cwd=' + cwd) + + +env = (k + '=' + v for k, v in os.environ.items()) +env = (x if isinstance(x, bytes) else x.encode(fenc, 'replace') for x in env) +args.extend((b'--env=' + x for x in env)) + +EOF = b'\0\0' + +for a in args: + eintr_retry_call(sock.sendall, a + EOF[0]) + +eintr_retry_call(sock.sendall, EOF) + +received = [] +while True: + r = sock.recv(4096) + if not r: + break + received.append(r) + +sock.close() + +print (b''.join(received)) diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 00000000..f2ffc12a --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1 @@ +powerline diff --git a/scripts/powerline-daemon b/scripts/powerline-daemon new file mode 100755 index 00000000..b7627cc4 --- /dev/null +++ b/scripts/powerline-daemon @@ -0,0 +1,395 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import socket +import os +import errno +import sys +from argparse import ArgumentParser +from select import select +from signal import signal, SIGTERM +from time import sleep +from functools import partial +from locale import getpreferredencoding + +from powerline.shell import get_argparser, finish_args, ShellPowerline +from powerline.lib.monotonic import monotonic + +is_daemon = False +platform = sys.platform.lower() +use_filesystem = 'darwin' in platform +# use_filesystem = True +del platform + +if use_filesystem: + address = '/tmp/powerline-ipc-%d' + pidfile = address + '.pid' +else: + # Use the abstract namespace for sockets rather than the filesystem + # (Available only in linux) + address = '\0powerline-ipc-%d' +address = address % os.getuid() + + +class NonInteractiveArgParser(ArgumentParser): + def print_usage(self, file=None): + raise Exception(self.format_usage()) + + def print_help(self, file=None): + raise Exception(self.format_help()) + + def exit(self, status=0, message=None): + pass + + def error(self, message): + raise Exception(self.format_usage()) + + +parser = get_argparser(parser=NonInteractiveArgParser, description='powerline daemon') +parser.add_argument('--cwd', metavar='PATH') +parser.add_argument('--env', action='append') + +EOF = b'EOF\0\0' + +powerlines = {} +logger = None +config_loader = None +home = os.path.expanduser('~') + + +class PowerlineDaemon(ShellPowerline): + def get_log_handler(self): + if not is_daemon: + import logging + return logging.StreamHandler() + return super(PowerlineDaemon, self).get_log_handler() + + +def render(args): + global logger + global config_loader + environ = dict(((k, v) for k, v in (x.partition('=')[0::2] for x in args.env))) + cwd = environ.get('PWD', args.cwd or '/') + segment_info = { + 'getcwd': lambda: cwd, + 'home': environ.get('HOME', home), + 'environ': environ, + 'args': args, + } + key = (args.ext[0], args.renderer_module, + tuple(args.config) if args.config else None, + tuple(args.theme_option) if args.theme_option else None,) + if args.renderer_arg: + segment_info.update(args.renderer_arg) + finish_args(args) + pl = None + try: + pl = powerlines[key] + except KeyError: + try: + pl = powerlines[key] = PowerlineDaemon( + args, + logger=logger, + config_loader=config_loader + ) + except SystemExit: + # Somebody thought raising system exit was a good idea, + return '' + except Exception as e: + if pl: + pl.pl.exception('Failed to render {0}: {1}', str(key), str(e)) + else: + return 'Failed to render {0}: {1}'.format(str(key), str(e)) + if logger is None: + logger = pl.logger + if config_loader is None: + config_loader = pl.config_loader + return pl.render(width=args.width, side=args.side, segment_info=segment_info) + + +def eintr_retry_call(func, *args, **kwargs): + while True: + try: + return func(*args, **kwargs) + except EnvironmentError as e: + if getattr(e, 'errno', None) == errno.EINTR: + continue + raise + + +def do_read(conn, timeout=2.0): + ''' Read data from the client. If the client fails to send data within + timeout seconds, abort. ''' + read = [] + end_time = monotonic() + timeout + while not read or not read[-1].endswith(b'\0\0'): + r, w, e = select((conn,), (), (conn,), timeout) + if e: + return + if monotonic() > end_time: + return + if not r: + continue + x = eintr_retry_call(conn.recv, 4096) + if x: + read.append(x) + else: + break + return b''.join(read) + + +def do_write(conn, result): + try: + eintr_retry_call(conn.sendall, result + b'\0') + except Exception: + pass + + +encoding = getpreferredencoding() or 'UTF-8' +if encoding.lower() == 'ascii': + encoding = 'UTF-8' + + +def safe_bytes(o, encoding=encoding): + '''Return bytes instance without ever throwing an exception.''' + try: + try: + # We are assuming that o is a unicode object + return o.encode(encoding, 'replace') + except Exception: + # Object may have defined __bytes__ (python 3) or __str__ method + # (python 2) + # This also catches problem with non_ascii_bytes.encode('utf-8') + # that first tries to decode to UTF-8 using ascii codec (and fails + # in this case) and then encode to given encoding: errors= argument + # is not used in the first stage. + return bytes(o) + except Exception as e: + return safe_bytes(str(e), encoding) + + +def do_render(req): + try: + args = [x.decode(encoding) for x in req.split(b'\0') if x] + args = parser.parse_args(args) + return safe_bytes(render(args)) + except Exception as e: + return safe_bytes(str(e)) + + +def do_one(sock, read_sockets, write_sockets, result_map): + r, w, e = select( + tuple(read_sockets) + (sock,), + tuple(write_sockets), + tuple(read_sockets) + tuple(write_sockets) + (sock,), + 60.0 + ) + + if sock in e: + # We cannot accept any more connections, so we exit + raise SystemExit(1) + + for s in e: + # Discard all broken connections to clients + s.close() + read_sockets.discard(s) + write_sockets.discard(s) + + for s in r: + if s == sock: + # A client wants to connect + conn, _ = eintr_retry_call(sock.accept) + read_sockets.add(conn) + else: + # A client has sent some data + read_sockets.discard(s) + req = do_read(s) + if req == EOF: + raise SystemExit(0) + elif req: + ans = do_render(req) + result_map[s] = ans + write_sockets.add(s) + else: + s.close() + + for s in w: + # A client is ready to receive the result + write_sockets.discard(s) + result = result_map.pop(s) + try: + do_write(s, result) + finally: + s.close() + + +def main_loop(sock): + sock.listen(1) + sock.setblocking(0) + + read_sockets, write_sockets = set(), set() + result_map = {} + try: + while True: + do_one(sock, read_sockets, write_sockets, result_map) + except KeyboardInterrupt: + raise SystemExit(0) + return 0 + + +def daemonize(stdin=os.devnull, stdout=os.devnull, stderr=os.devnull): + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError as e: + print ("fork #1 failed: %d (%s)" % (e.errno, e.strerror), file=sys.stderr) + sys.exit(1) + + # decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError as e: + print ("fork #2 failed: %d (%s)" % (e.errno, e.strerror), file=sys.stderr) + sys.exit(1) + + # Redirect standard file descriptors. + si = file(stdin, 'r') + so = file(stdout, 'a+') + se = file(stderr, 'a+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + global is_daemon + is_daemon = True + + +def check_existing(): + if use_filesystem: + # We cannot bind if the socket file already exists so remove it, we + # already have a lock on pidfile, so this should be safe. + try: + os.unlink(address) + except EnvironmentError: + pass + + sock = socket.socket(family=socket.AF_UNIX) + try: + sock.bind(address) + except socket.error as e: + if getattr(e, 'errno', None) == errno.EADDRINUSE: + return None + raise + return sock + + +def test_connect(): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + try: + eintr_retry_call(sock.connect, address) + except socket.error: + return False + else: + eintr_retry_call(sock.sendall, EOF) + finally: + sock.close() + return True + + +def cleanup_lockfile(fd, *args): + try: + # Remove the directory entry for the lock file + os.unlink(pidfile) + # Close the file descriptor + os.close(fd) + except EnvironmentError: + pass + if args: + # Called in signal handler + raise SystemExit(1) + + +def lockpidfile(): + import fcntl + import atexit + import stat + fd = os.open(pidfile, os.O_WRONLY|os.O_CREAT, + stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP|stat.S_IROTH) + try: + fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except EnvironmentError: + os.close(fd) + return None + os.lseek(fd, 0, os.SEEK_SET) + os.ftruncate(fd, 0) + os.write(fd, ('%d' % os.getpid()).encode('ascii')) + os.fsync(fd) + cleanup = partial(cleanup_lockfile, fd) + signal(SIGTERM, cleanup) + atexit.register(cleanup) + return fd + + +def main(): + p = ArgumentParser(description= + 'Daemon to improve the performance of powerline') + a = p.add_mutually_exclusive_group().add_argument + a('--kill', '-k', action='store_true', help='Kill an already running instance') + a('--foreground', '-f', action='store_true', help='Run in the foreground (dont daemonize)') + a('--replace', '-r', action='store_true', help='Replace an already running instance') + args = p.parse_args() + + if args.kill: + if test_connect(): + print ('Kill command sent to daemon, if it does not die in a couple of seconds use kill to kill it') + else: + print ('No running daemon found') + return + + if args.replace: + while test_connect(): + print ('Kill command sent to daemon, waiting for daemon to exit, press Ctrl-C to terminate wait and exit') + sleep(2) + + if use_filesystem and not args.foreground: + # We must daemonize before creating the locked pidfile, unfortunately, + # this means further print statements are discarded + daemonize() + + if use_filesystem: + # Create a locked pid file containing the daemon's PID + if lockpidfile() is None: + print ('The daemon is already running. Use %s -k to kill it.' % os.path.basename(sys.argv[0]), + file=sys.stderr) + raise SystemExit(1) + + # Bind to address or bail if we cannot bind + sock = check_existing() + if sock is None: + print ('The daemon is already running. Use %s -k to kill it.' % os.path.basename(sys.argv[0]), + file=sys.stderr) + raise SystemExit(1) + + if args.foreground: + return main_loop(sock) + + if not use_filesystem: + # We daemonize on linux + daemonize() + + main_loop(sock) + + +if __name__ == '__main__': + main() diff --git a/scripts/powerline b/scripts/powerline-render similarity index 100% rename from scripts/powerline rename to scripts/powerline-render diff --git a/setup.py b/setup.py index e341ceec..770a927e 100755 --- a/setup.py +++ b/setup.py @@ -3,16 +3,42 @@ from __future__ import unicode_literals import os import sys +import subprocess from setuptools import setup, find_packages -here = os.path.abspath(os.path.dirname(__file__)) +CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) try: - README = open(os.path.join(here, 'README.rst'), 'rb').read().decode('utf-8') + README = open(os.path.join(CURRENT_DIR, 'README.rst'), 'rb').read().decode('utf-8') except IOError: README = '' -old_python = sys.version_info < (2, 7) +OLD_PYTHON = sys.version_info < (2, 7) + + +def compile_client(): + '''Compile the C powerline-client script.''' + + if hasattr(sys, 'getwindowsversion'): + raise NotImplementedError() + if sys.version_info >= (3, 0): + # FIXME Python 3 doesn't allow compiled C files to be included in the + # scripts list below. This is because Python 3 distutils tries to + # decode the file to ASCII, and fails when powerline-client is + # a binary. + raise NotImplementedError() + else: + from distutils.ccompiler import new_compiler + compiler = new_compiler().compiler + subprocess.check_call(compiler + ['-O3', 'client/powerline.c', '-o', 'scripts/powerline']) + +try: + compile_client() +except Exception: + # FIXME Catch more specific exceptions + import shutil + print('Compiling C version of powerline-client failed, using Python version instead') + shutil.copyfile('client/powerline.py', 'scripts/powerline') setup( name='Powerline', @@ -26,6 +52,8 @@ setup( scripts=[ 'scripts/powerline', 'scripts/powerline-lint', + 'scripts/powerline-daemon', + 'scripts/powerline-render', 'scripts/powerline-config', ], keywords='', @@ -38,5 +66,5 @@ setup( 'Sphinx', ], }, - test_suite='tests' if not old_python else None, + test_suite='tests' if not OLD_PYTHON else None, )