3 # This file is part of chiark-utils, a collection of useful programs
4 # used on chiark.greenend.org.uk.
7 # Copyright 2018 Citrix Systems Ltd
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.
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
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.
23 import fishdescriptor.fish
32 usage = '''fishdescriptor [-p|--pid] <pid> <action>... [-p|--pid <pid> <action>...]
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>
43 calls getsockname/getpeername on the most recent
47 now attach to <pid>, detaching from previous pid
51 # list of (nominal, there) where nominal might be None
54 # fdmap[nominal] = (actual, Donor, there)
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)
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)
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
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
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 $!;
86 famp = subprocess.Popen(
88 stdout = subprocess.PIPE,
89 args = ['perl','-we',perl_script]
91 (output, dummy) = famp.communicate()
94 sock = socket.fromfd(actual, family, 0)
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())):
101 except Exception as e: info = repr(e)
102 print("\t", info, sep='', end='')
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:
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_intented][0] = inway_moved
130 os.dup2(actual, intended)
133 def implement_exec(argl):
134 if donor is not None: donor.detach()
136 permute_fds_for_exec()
137 os.execvp(argl[0], argl)
141 if donor is not None: donor.detach()
142 donor = fishdescriptor.fish.Donor(pid, debug=ov.debug)
144 def ocb_set_donor(option, opt, value, parser):
147 ov = optparse.Values()
154 def arg_matches(regexp):
156 m = re.search(regexp, arg)
159 op = optparse.OptionParser(usage=usage)
161 op.disable_interspersed_args()
162 op.add_option('-p','--pid', type='int', action='callback',
163 callback=ocb_set_donor)
164 op.add_option('-D','--debug', action='store_const',
165 dest='debug', const=sys.stderr)
169 last_nominal = None # None or (nominal,) ie None or (None,) or (int,)
172 (ov, args) = op.parse_args(args=args, values=ov)
173 if not len(args): break
179 elif arg_matches(r'^(?:(\d+)=)?(\d+)?$'):
180 (nominal, there) = m.groups()
181 nominal = None if nominal is None else int(nominal)
183 pending.append((nominal,there))
184 last_nominal = (nominal,)
187 op.error("exec needs command to run")
190 elif arg == 'sockinfo':
191 if last_nominal is None:
192 op.error('sockinfo needs a prior fd spec')
194 implement_sockinfo(last_nominal[0])
196 op.error("unknown argument/option `%s'" % arg)