From 1222a7f9277f4eb050ac4915d12c63e7051ad7d9 Mon Sep 17 00:00:00 2001
From: "James E. Blair" <jeblair@linux.vnet.ibm.com>
Date: Thu, 14 Jan 2016 13:51:06 -0800
Subject: [PATCH] Add support for external commands

If you run "gertty --open <URL>" it will instruct a running
Gertty to open the change at that URL.

Change-Id: Ie82aa53f497717e7355646d6d6fd12473ececad0
---
 examples/reference-gertty.yaml |  3 ++
 gertty/app.py                  | 66 +++++++++++++++++++++++++++++++++-
 gertty/config.py               |  3 ++
 3 files changed, 71 insertions(+), 1 deletion(-)

diff --git a/examples/reference-gertty.yaml b/examples/reference-gertty.yaml
index 5a33fdd..e621f52 100644
--- a/examples/reference-gertty.yaml
+++ b/examples/reference-gertty.yaml
@@ -47,6 +47,9 @@ servers:
 # time it starts (so that it does not grow without bound).  If you
 # would like to log to a different location, you may specify it here.
 #    log-file: ~/.gertty.log
+# Gertty listens on a unix domain socket for remote commands at
+# ~/.gertty.sock.  You may change the path here:
+#    socket: ~/.gertty.sock
 
 # Gertty comes with two palettes defined internally.  The default
 # palette is suitable for use on a terminal with a dark background.
diff --git a/gertty/app.py b/gertty/app.py
index b72f006..c47a31f 100644
--- a/gertty/app.py
+++ b/gertty/app.py
@@ -19,6 +19,7 @@ import dateutil
 import logging
 import os
 import re
+import socket
 import subprocess
 import sys
 import textwrap
@@ -256,6 +257,8 @@ class App(object):
         self.error_queue = queue.Queue()
         self.error_pipe = self.loop.watch_pipe(self._errorPipeInput)
         self.logged_warnings = set()
+        self.command_pipe = self.loop.watch_pipe(self._commandPipeInput)
+        self.command_queue = queue.Queue()
 
         warnings.showwarning = self._showWarning
 
@@ -268,6 +271,9 @@ class App(object):
 
         self.loop.screen.tty_signal_keys(start='undefined', stop='undefined')
         #self.loop.screen.set_terminal_properties(colors=88)
+
+        self.startSocketListener()
+
         if not disable_sync:
             self.sync_thread = threading.Thread(target=self.sync.run, args=(self.sync_pipe,))
             self.sync_thread.daemon = True
@@ -294,6 +300,35 @@ class App(object):
 
         self.popup(dialog)
 
+    def startSocketListener(self):
+        if os.path.exists(self.config.socket_path):
+            os.unlink(self.config.socket_path)
+        self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        self.socket.bind(self.config.socket_path)
+        self.socket.listen(1)
+        self.socket_thread = threading.Thread(target=self._socketListener)
+        self.socket_thread.daemon = True
+        self.socket_thread.start()
+
+    def _socketListener(self):
+        while True:
+            try:
+                s, addr = self.socket.accept()
+                self.log.debug("Accepted socket connection %s" % (s,))
+                buf = ''
+                while True:
+                    buf += s.recv(1)
+                    if buf[-1] == '\n':
+                        break
+                buf = buf.strip()
+                self.log.debug("Received %s from socket" % (buf,))
+                s.close()
+                parts = buf.split()
+                self.command_queue.put((parts[0], parts[1:]))
+                os.write(self.command_pipe, six.b('command\n'))
+            except Exception:
+                self.log.exception("Exception in socket handler")
+
     def clearInputBuffer(self):
         if self.input_buffer:
             self.input_buffer = []
@@ -598,7 +633,18 @@ class App(object):
             if category == requestsexceptions.InsecureRequestWarning:
                 return
         self.error_queue.put(('Warning', m))
-        os.write(self.error_pipe, 'error\n')
+        os.write(self.error_pipe, six.b('error\n'))
+
+    def _commandPipeInput(self, data=None):
+        (command, data) = self.command_queue.get()
+        if command == 'open':
+            url = data[0]
+            self.log.debug("Opening URL %s" % (url,))
+            result = self.parseInternalURL(url)
+            if result is not None:
+                self.openInternalURL(result)
+        else:
+            self.log.error("Unable to parse command %s with data %s" % (command, data))
 
     def toggleHeldChange(self, change_key):
         with self.db.getSession() as session:
@@ -709,6 +755,21 @@ class PrintPaletteAction(argparse.Action):
             print(attr)
         sys.exit(0)
 
+class OpenChangeAction(argparse.Action):
+    def __call__(self, parser, namespace, values, option_string=None):
+        cf = config.Config(namespace.server, namespace.palette,
+                           namespace.keymap, namespace.path)
+        url = values[0]
+        result = urlparse.urlparse(values[0])
+        if not url.startswith(cf.url):
+            print('Supplied URL must start with %s' % (cf.url,))
+            sys.exit(1)
+
+        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        s.connect(cf.socket_path)
+        s.sendall('open %s\n' % url)
+        sys.exit(0)
+
 def main():
     parser = argparse.ArgumentParser(
         description='Console client for Gerrit Code Review.')
@@ -728,6 +789,9 @@ def main():
                         help='print the keymap command names to stdout')
     parser.add_argument('--print-palette', nargs=0, action=PrintPaletteAction,
                         help='print the palette attribute names to stdout')
+    parser.add_argument('--open', nargs=1, action=OpenChangeAction,
+                        metavar='URL',
+                        help='open the given URL in a running Gertty')
     parser.add_argument('--version', dest='version', action='version',
                         version=version(),
                         help='show Gertty\'s version')
diff --git a/gertty/config.py b/gertty/config.py
index 51296ad..42089b8 100644
--- a/gertty/config.py
+++ b/gertty/config.py
@@ -47,6 +47,7 @@ class ConfigSchema(object):
               'dburi': str,
               v.Required('git-root'): str,
               'log-file': str,
+              'socket': str,
               'auth-type': str,
               }
 
@@ -173,6 +174,8 @@ class Config(object):
         self.git_root = os.path.expanduser(server['git-root'])
         self.dburi = server.get('dburi',
                                 'sqlite:///' + os.path.expanduser('~/.gertty.db'))
+        socket_path = server.get('socket', '~/.gertty.sock')
+        self.socket_path = os.path.expanduser(socket_path)
         log_file = server.get('log-file', '~/.gertty.log')
         self.log_file = os.path.expanduser(log_file)