chiark / gitweb /
fishdescriptor: fix error handling
[chiark-utils.git] / fishdescriptor / fishdescriptor
1 #!/usr/bin/python3
2
3 import sys
4 import fishdescriptor.fish
5 import optparse
6 import re
7 import subprocess
8 import socket
9 import os
10
11 donor = None
12
13 usage = '''fishdescriptor [-p|--pid] <pid> <action>... [-p|--pid <pid> <action>...]
14
15 <action>s
16   [<here-0fd>=]<there-fd>
17           fish the openfile referenced by descriptor <there-fd> in
18           (the most recent) <pid> and keep a descriptor onto it;
19           and, optionally, give it the number <here-fd> for exec
20   exec <program> [<arg>...]
21           execute a process with each specified <here>
22           as an actual fd
23   sockinfo
24           calls getsockname/getpeername on the most recent
25           <there-fd>
26
27   -p|-pid <pid>
28           now attach to <pid>, detaching from previous pid
29 '''
30
31 pending = []
32 # list of (nominal, there) where nominal might be None
33
34 fdmap = { }
35 # fdmap[nominal] = (actual, Donor, there)
36
37 def implement_pending():
38     try: actuals = donor.fish([pend[1] for pend in pending])
39     except fishdescriptor.fish.Error as e:
40         print('fishdescriptor error: %s' % e, file=sys.stderr)
41         sys.exit(127)
42     assert(len(actuals) == len(pending))
43     for (nominal, there), actual in zip(pending, actuals):
44         overwriting_info = fdmap.get(nominal)
45         if overwriting_info is not None: os.close(overwriting_info[0])
46         fdmap[nominal] = (actual, donor, there)
47
48 def implement_sockinfo(nominal):
49     (actual, tdonor, there) = fdmap[nominal]
50     # socket.fromfd requires the AF.  But of course we don't know the AF.
51     # There isn't a sane way to get it in Python:
52     #  https://utcc.utoronto.ca/~cks/space/blog/python/SocketFromFdMistake
53     # Rejected options:
54     #  https://github.com/tiran/socketfromfd
55     #   adds a dependency, not portable due to reliance on SO_DOMAIN
56     #  call getsockname using ctypes
57     #   no sane way to discover how to unpack sa_family_t
58     perl_script = '''
59         use strict;
60         use Socket;
61         use POSIX;
62         my $sa = getsockname STDIN;
63         exit 0 if !defined $sa and $!==ENOTSOCK;
64         my $family = sockaddr_family $sa;
65         print $family, "\n" or die $!;
66     '''
67     famp = subprocess.Popen(
68         stdin = actual,
69         stdout = subprocess.PIPE,
70         args = ['perl','-we',perl_script]
71     )
72     (output, dummy) = famp.communicate()
73     family = int(output)
74
75     sock = socket.fromfd(actual, family, 0)
76
77     print("[%s] %d sockinfo" % (tdonor.pid, there), end='')
78     for f in (lambda: socket.AddressFamily(family).name,
79               lambda: repr(sock.getsockname()),
80               lambda: repr(sock.getpeername())):
81         try: info = f()
82         except Exception as e: info = repr(e)
83         print("\t", info, sep='', end='')
84     print("")
85
86     sock.close()
87
88 def permute_fds_for_exec():
89     actual2intended = { info[0]: nominal for nominal, info in fdmap.items() }
90     # invariant at the start of each loop iteration:
91     #     for each intended (aka `nominal') we have processed:
92     #         relevant open-file is only held in fd intended
93     #         (unless `nominal' is None in which case it is closed)
94     #     for each intended (aka `nominal') we have NOT processed:
95     #         relevant open-file is only held in actual
96     #         where  actual = fdmap[nominal][0]
97     #         and where  actual2intended[actual] = nominal
98     # we can rely on processing each intended only once,
99     #  since they're hash keys
100     # the post-condition is not really a valid state (fdmap
101     #  is nonsense) but we call this function just before exec
102     for intended, (actual, tdonor, there) in fdmap.items():
103         if intended == actual:
104             continue
105         if intended is not None:
106             inway_intended = actual2intended.get(intended)
107             if inway_intended is not None:
108                 inway_moved = os.dup(intended)
109                 actual2intended[inway_moved] = inway_intended
110                 fdmap[inway_intented][0] = inway_moved
111             os.dup2(actual, intended)
112         os.close(actual)
113
114 def implement_exec(argl):
115     if donor is not None: donor.detach()
116     sys.stdout.flush()
117     permute_fds_for_exec()
118     os.execvp(argl[0], argl)
119
120 def set_donor(pid):
121     global donor
122     if donor is not None: donor.detach()
123     donor = fishdescriptor.fish.Donor(pid, debug=ov.debug)
124
125 def ocb_set_donor(option, opt, value, parser):
126     set_donor(value)
127
128 ov = optparse.Values()
129
130 def process_args():
131     global ov
132
133     m = None
134     
135     def arg_matches(regexp):
136         nonlocal m
137         m = re.search(regexp, arg)
138         return m
139
140     op = optparse.OptionParser(usage=usage)
141
142     op.disable_interspersed_args()
143     op.add_option('-p','--pid', type='int', action='callback',
144                   callback=ocb_set_donor)
145     op.add_option('-D','--debug', action='store_const',
146                   dest='debug', const=sys.stderr)
147     ov.debug = None
148
149     args = sys.argv[1:]
150     last_nominal = None # None or (nominal,) ie None or (None,) or (int,)
151
152     while True:
153         (ov, args) = op.parse_args(args=args, values=ov)
154         if not len(args): break
155
156         arg = args.pop(0)
157
158         if donor is None:
159             set_donor(int(arg))
160         elif arg_matches(r'^(?:(\d+)=)?(\d+)?$'):
161             (nominal, there) = m.groups()
162             nominal = None if nominal is None else int(nominal)
163             there = int(there)
164             pending.append((nominal,there))
165             last_nominal = (nominal,)
166         elif arg == 'exec':
167             if not len(args):
168                 op.error("exec needs command to run")
169             implement_pending()
170             implement_exec(args)
171         elif arg == 'sockinfo':
172             if last_nominal is None:
173                 op.error('sockinfo needs a prior fd spec')
174             implement_pending()
175             implement_sockinfo(last_nominal[0])
176         else:
177             op.error("unknown argument/option `%s'" % arg)
178
179 process_args()