chiark / gitweb /
fishdescriptor: Fix a tuple bug in fd permutation
[chiark-utils.git] / fishdescriptor / fishdescriptor
index 7150df2b31fc587f711b3826a7a90a296558e0de..d583533c210faf64d351f1536af350fceafe887c 100755 (executable)
 #!/usr/bin/python3
 
+# This file is part of chiark-utils, a collection of useful programs
+# used on chiark.greenend.org.uk.
+#
+# This file is:
+#  Copyright 2018 Citrix Systems Ltd
+#
+# This is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# This is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, consult the Free Software Foundation's
+# website at www.fsf.org, or the GNU Project website at www.gnu.org.
+
 import sys
 import fishdescriptor.fish
+import optparse
+import re
+import subprocess
+import socket
+import os
+
+donor = None
+
+usage = '''fishdescriptor [-p|--pid] <pid> <action>... [-p|--pid <pid> <action>...]
+
+<action>s
+  [<here-fd>=]<there-fd>
+          fish the openfile referenced by descriptor <there-fd> in
+          (the most recent) <pid> and keep a descriptor onto it;
+          and, optionally, give it the number <here-fd> for exec
+  exec <program> [<arg>...]
+          execute a process with each specified <here>
+          as an actual fd
+  sockinfo
+          calls getsockname/getpeername on the most recent
+          <there-fd>
+
+  -p|-pid <pid>
+          now attach to <pid>, detaching from previous pid
+'''
+
+pending = []
+# list of (nominal, there) where nominal might be None
+
+fdmap = { }
+# fdmap[nominal] = (actual, Donor, there)
+
+def implement_pending():
+    try: actuals = donor.fish([pend[1] for pend in pending])
+    except fishdescriptor.fish.Error as e:
+        print('fishdescriptor error: %s' % e, file=sys.stderr)
+        sys.exit(127)
+    assert(len(actuals) == len(pending))
+    for (nominal, there), actual in zip(pending, actuals):
+        overwriting_info = fdmap.get(nominal)
+        if overwriting_info is not None: os.close(overwriting_info[0])
+        fdmap[nominal] = [actual, donor, there]
+
+def implement_sockinfo(nominal):
+    (actual, tdonor, there) = fdmap[nominal]
+    # socket.fromfd requires the AF.  But of course we don't know the AF.
+    # There isn't a sane way to get it in Python:
+    #  https://utcc.utoronto.ca/~cks/space/blog/python/SocketFromFdMistake
+    # Rejected options:
+    #  https://github.com/tiran/socketfromfd
+    #   adds a dependency, not portable due to reliance on SO_DOMAIN
+    #  call getsockname using ctypes
+    #   no sane way to discover how to unpack sa_family_t
+    perl_script = '''
+        use strict;
+        use Socket;
+        use POSIX;
+        my $sa = getsockname STDIN;
+        exit 0 if !defined $sa and $!==ENOTSOCK;
+        my $family = sockaddr_family $sa;
+        print $family, "\n" or die $!;
+    '''
+    famp = subprocess.Popen(
+        stdin = actual,
+        stdout = subprocess.PIPE,
+        args = ['perl','-we',perl_script]
+    )
+    (output, dummy) = famp.communicate()
+    family = int(output)
+
+    sock = socket.fromfd(actual, family, 0)
+
+    print("[%s] %d sockinfo" % (tdonor.pid, there), end='')
+    for f in (lambda: socket.AddressFamily(family).name,
+              lambda: repr(sock.getsockname()),
+              lambda: repr(sock.getpeername())):
+        try: info = f()
+        except Exception as e: info = repr(e)
+        print("\t", info, sep='', end='')
+    print("")
+
+    sock.close()
+
+def permute_fds_for_exec():
+    actual2intended = { info[0]: nominal for nominal, info in fdmap.items() }
+    # invariant at the start of each loop iteration:
+    #     for each intended (aka `nominal') we have processed:
+    #         relevant open-file is only held in fd intended
+    #         (unless `nominal' is None in which case it is closed)
+    #     for each intended (aka `nominal') we have NOT processed:
+    #         relevant open-file is only held in actual
+    #         where  actual = fdmap[nominal][0]
+    #         and where  actual2intended[actual] = nominal
+    # we can rely on processing each intended only once,
+    #  since they're hash keys
+    # the post-condition is not really a valid state (fdmap
+    #  is nonsense) but we call this function just before exec
+    for intended, (actual, tdonor, there) in fdmap.items():
+        if intended == actual:
+            continue
+        if intended is not None:
+            inway_intended = actual2intended.get(intended)
+            if inway_intended is not None:
+                inway_moved = os.dup(intended)
+                actual2intended[inway_moved] = inway_intended
+                fdmap[inway_intended][0] = inway_moved
+            os.dup2(actual, intended)
+        os.close(actual)
+
+def implement_exec(argl):
+    if donor is not None: donor.detach()
+    sys.stdout.flush()
+    permute_fds_for_exec()
+    os.execvp(argl[0], argl)
+
+def set_donor(pid):
+    global donor
+    if donor is not None: donor.detach()
+    donor = fishdescriptor.fish.Donor(pid, debug=ov.debug)
+
+def ocb_set_donor(option, opt, value, parser):
+    set_donor(value)
+
+ov = optparse.Values()
+
+def process_args():
+    global ov
+
+    m = None
+    
+    def arg_matches(regexp):
+        nonlocal m
+        m = re.search(regexp, arg)
+        return m
+
+    op = optparse.OptionParser(usage=usage)
+
+    op.disable_interspersed_args()
+    op.add_option('-p','--pid', type='int', action='callback',
+                  callback=ocb_set_donor)
+    op.add_option('-D','--debug', action='store_const',
+                  dest='debug', const=sys.stderr)
+    ov.debug = None
+
+    args = sys.argv[1:]
+    last_nominal = None # None or (nominal,) ie None or (None,) or (int,)
+
+    while True:
+        (ov, args) = op.parse_args(args=args, values=ov)
+        if not len(args): break
+
+        arg = args.pop(0)
 
-pid = int(sys.argv[1])
-fds = [int(x) for x in sys.argv[2:]]
+        if donor is None:
+            set_donor(int(arg))
+        elif arg_matches(r'^(?:(\d+)=)?(\d+)?$'):
+            (nominal, there) = m.groups()
+            nominal = None if nominal is None else int(nominal)
+            there = int(there)
+            pending.append((nominal,there))
+            last_nominal = (nominal,)
+        elif arg == 'exec':
+            if not len(args):
+                op.error("exec needs command to run")
+            implement_pending()
+            implement_exec(args)
+        elif arg == 'sockinfo':
+            if last_nominal is None:
+                op.error('sockinfo needs a prior fd spec')
+            implement_pending()
+            implement_sockinfo(last_nominal[0])
+        else:
+            op.error("unknown argument/option `%s'" % arg)
 
-d = fishdescriptor.fish.Donor(pid)
-r = d.fish(fds)
-print(repr(r))
+process_args()