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