chiark / gitweb /
fishdescriptor: Use Python "errno" module
[chiark-utils.git] / fishdescriptor / py / fishdescriptor / fish.py
1 # fish.py
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 # python 3 only
23
24 import socket
25 import subprocess
26 import os
27 import pwd
28 import struct
29 import tempfile
30 import shutil
31 import sys
32 import errno
33
34 def _shuffle_fd3():
35     os.dup2(1,3)
36     os.dup2(2,1)
37
38 class Error(Exception): pass
39
40 class Donor():
41     def __init__(d, pid, debug=None):
42         d.pid = pid
43         if debug is None:
44             d._stderr = tempfile.TemporaryFile(mode='w+')
45         else:
46             d._stderr = None
47         d._sp = subprocess.Popen(
48             preexec_fn = _shuffle_fd3,
49             stdin = subprocess.PIPE,
50             stdout = subprocess.PIPE,
51             stderr = d._stderr,
52             close_fds = False,
53             args = ['gdb', '-p', str(pid), '-batch', '-ex',
54                     'python import fishdescriptor.indonor as id;'+
55                     ' id.DonorImplementation().eval_loop()'
56                 ]
57         )            
58
59     def _eval_integer(d, expr):
60         try:
61             l = d._sp.stdout.readline()
62             if not len(l): raise Error('gdb process donor python repl quit')
63             if l != b'!\n': raise RuntimeError("indonor said %s" % repr(l))
64             d._sp.stdin.write(expr.encode('utf-8') + b'\n')
65             d._sp.stdin.flush()
66             l = d._sp.stdout.readline().rstrip(b'\n')
67             return int(l)
68         except Exception as e:
69             if d._stderr is not None:
70                 d._stderr.seek(0)
71                 shutil.copyfileobj(d._stderr, sys.stderr)
72                 d._stderr.seek(0)
73                 d._stderr.truncate()
74             raise e
75
76     def _eval_success(d, expr):
77         r = d._eval_integer(expr)
78         if r != 1: raise RuntimeError("eval of %s gave %d" % (expr, r))
79
80     def _geteuid(d):
81         return d._eval_integer('di.geteuid()')
82
83     def _ancilmsg(d, fds):
84         perl_script = '''
85             use strict;
86             use Socket;
87             use Socket::MsgHdr;
88             my $fds = pack "i*", @ARGV;
89             my $m = Socket::MsgHdr::pack_cmsghdr SOL_SOCKET, SCM_RIGHTS, $fds;
90             print join ", ", unpack "C*", $m;
91         '''
92         ap = subprocess.Popen(
93             stdin = subprocess.DEVNULL,
94             stdout = subprocess.PIPE,
95             args = ['perl','-we',perl_script] + [str(x) for x in fds]
96         )
97         (output, dummy) = ap.communicate()
98         return output.decode('utf-8')
99
100     def donate(d, path, fds):
101         ancil = d._ancilmsg(fds)
102         d._eval_success('di.donate(%s, [ %s ])'
103                         % (repr(path), ancil))
104         return len(ancil.split(','))
105
106     def mkdir(d, path):
107         d._eval_integer('di.mkdir(%s)'
108                         % (repr(path)))
109
110     def _exists(d, path):
111         try:
112             os.stat(path)
113             return True
114         except OSError as oe:
115             if oe.errno != errno.ENOENT: raise oe
116             return False
117
118     def _sock_dir(d, target_euid, target_root):
119         run_dir = '/run/user/%d' % target_euid
120         if d._exists(target_root + run_dir):
121             return run_dir + '/fishdescriptor'
122
123         try:
124             pw = pwd.getpwuid(target_euid)
125             return pw.pw_dir + '/.fishdescriptor'
126         except KeyError:
127             pass
128
129         raise RuntimeError(
130  'cannot find good socket path - no /run/user/UID nor pw entry for target process euid %d'
131             % target_euid
132         )
133
134     def fish(d, fds):
135         # -> list of fds in our process
136
137         target_root = '/proc/%d/root' % d.pid
138         if not d._exists(target_root):
139             target_root = ''
140
141         euid = d._geteuid()
142         sockdir = d._sock_dir(euid, target_root)
143         d.mkdir(sockdir)
144
145         sockname = '%s/%s,%d' % (sockdir, os.uname().nodename, d.pid)
146
147         our_sockname = target_root + sockname
148
149         s = None
150         s2 = None
151
152         try:
153             try: os.remove(our_sockname)
154             except FileNotFoundError: pass
155
156             s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
157             s.bind(our_sockname)
158             os.chmod(our_sockname, 666)
159             s.listen(1)
160
161             ancil_len = d.donate(sockname, fds)
162             (s2, dummy) = s.accept()
163             (msg, ancil, flags, sender) = s2.recvmsg(1, ancil_len)
164
165             got_fds = None
166             unpack_fmt = '%di' % len(fds)
167
168             for clvl, ctype, cdata in ancil:
169                 if clvl == socket.SOL_SOCKET and ctype == socket.SCM_RIGHTS:
170                     assert(got_fds is None)
171                     got_fds = struct.unpack_from(unpack_fmt, cdata)
172
173         finally:
174             if s is not None: s.close()
175             if s2 is not None: s2.close()
176
177             try: os.remove(our_sockname)
178             except FileNotFoundError: pass
179
180         return list(got_fds)
181
182     def detach(d):
183         d._sp.stdin.close()