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