xref: /freebsd/contrib/lib9p/pytest/p9conn.py (revision 134e17798c9af53632b372348ab828e75e65bf46)
1*134e1779SJakub Wojciech Klama#! /usr/bin/env python
2*134e1779SJakub Wojciech Klama
3*134e1779SJakub Wojciech Klama"""
4*134e1779SJakub Wojciech Klamahandle plan9 server <-> client connections
5*134e1779SJakub Wojciech Klama
6*134e1779SJakub Wojciech Klama(We can act as either server or client.)
7*134e1779SJakub Wojciech Klama
8*134e1779SJakub Wojciech KlamaThis code needs some doctests or other unit tests...
9*134e1779SJakub Wojciech Klama"""
10*134e1779SJakub Wojciech Klama
11*134e1779SJakub Wojciech Klamaimport collections
12*134e1779SJakub Wojciech Klamaimport errno
13*134e1779SJakub Wojciech Klamaimport logging
14*134e1779SJakub Wojciech Klamaimport math
15*134e1779SJakub Wojciech Klamaimport os
16*134e1779SJakub Wojciech Klamaimport socket
17*134e1779SJakub Wojciech Klamaimport stat
18*134e1779SJakub Wojciech Klamaimport struct
19*134e1779SJakub Wojciech Klamaimport sys
20*134e1779SJakub Wojciech Klamaimport threading
21*134e1779SJakub Wojciech Klamaimport time
22*134e1779SJakub Wojciech Klama
23*134e1779SJakub Wojciech Klamaimport lerrno
24*134e1779SJakub Wojciech Klamaimport numalloc
25*134e1779SJakub Wojciech Klamaimport p9err
26*134e1779SJakub Wojciech Klamaimport pfod
27*134e1779SJakub Wojciech Klamaimport protocol
28*134e1779SJakub Wojciech Klama
29*134e1779SJakub Wojciech Klama# Timespec based timestamps, if present, have
30*134e1779SJakub Wojciech Klama# both seconds and nanoseconds.
31*134e1779SJakub Wojciech KlamaTimespec = collections.namedtuple('Timespec', 'sec nsec')
32*134e1779SJakub Wojciech Klama
33*134e1779SJakub Wojciech Klama# File attributes from Tgetattr, or given to Tsetattr.
34*134e1779SJakub Wojciech Klama# (move to protocol.py?)  We use pfod here instead of
35*134e1779SJakub Wojciech Klama# namedtuple so that we can create instances with all-None
36*134e1779SJakub Wojciech Klama# fields easily.
37*134e1779SJakub Wojciech KlamaFileattrs = pfod.pfod('Fileattrs',
38*134e1779SJakub Wojciech Klama    'ino mode uid gid nlink rdev size blksize blocks '
39*134e1779SJakub Wojciech Klama    'atime mtime ctime btime gen data_version')
40*134e1779SJakub Wojciech Klama
41*134e1779SJakub Wojciech Klamaqt2n = protocol.qid_type2name
42*134e1779SJakub Wojciech Klama
43*134e1779SJakub Wojciech KlamaSTD_P9_PORT=564
44*134e1779SJakub Wojciech Klama
45*134e1779SJakub Wojciech Klamaclass P9Error(Exception):
46*134e1779SJakub Wojciech Klama    pass
47*134e1779SJakub Wojciech Klama
48*134e1779SJakub Wojciech Klamaclass RemoteError(P9Error):
49*134e1779SJakub Wojciech Klama    """
50*134e1779SJakub Wojciech Klama    Used when the remote returns an error.  We track the client
51*134e1779SJakub Wojciech Klama    (connection instance), the operation being attempted, the
52*134e1779SJakub Wojciech Klama    message, and an error number and type.  The message may be
53*134e1779SJakub Wojciech Klama    from the Rerror reply, or from converting the errno in a dot-L
54*134e1779SJakub Wojciech Klama    or dot-u Rerror reply.  The error number may be None if the
55*134e1779SJakub Wojciech Klama    type is 'Rerror' rather than 'Rlerror'.  The message may be
56*134e1779SJakub Wojciech Klama    None or empty string if a non-None errno supplies the error
57*134e1779SJakub Wojciech Klama    instead.
58*134e1779SJakub Wojciech Klama    """
59*134e1779SJakub Wojciech Klama    def __init__(self, client, op, msg, etype, errno):
60*134e1779SJakub Wojciech Klama        self.client = str(client)
61*134e1779SJakub Wojciech Klama        self.op = op
62*134e1779SJakub Wojciech Klama        self.msg = msg
63*134e1779SJakub Wojciech Klama        self.etype = etype # 'Rerror' or 'Rlerror'
64*134e1779SJakub Wojciech Klama        self.errno = errno # may be None
65*134e1779SJakub Wojciech Klama        self.message = self._get_message()
66*134e1779SJakub Wojciech Klama        super(RemoteError, self).__init__(self, self.message)
67*134e1779SJakub Wojciech Klama
68*134e1779SJakub Wojciech Klama    def __repr__(self):
69*134e1779SJakub Wojciech Klama        return ('{0!r}({1}, {2}, {3}, {4}, '
70*134e1779SJakub Wojciech Klama                '{5})'.format(self.__class__.__name__, self.client, self.op,
71*134e1779SJakub Wojciech Klama                              self.msg, self.errno, self.etype))
72*134e1779SJakub Wojciech Klama    def __str__(self):
73*134e1779SJakub Wojciech Klama        prefix = '{0}: {1}: '.format(self.client, self.op)
74*134e1779SJakub Wojciech Klama        if self.errno: # check for "is not None", or just non-false-y?
75*134e1779SJakub Wojciech Klama            name = {'Rerror': '.u', 'Rlerror': 'Linux'}[self.etype]
76*134e1779SJakub Wojciech Klama            middle = '[{0} error {1}] '.format(name, self.errno)
77*134e1779SJakub Wojciech Klama        else:
78*134e1779SJakub Wojciech Klama            middle = ''
79*134e1779SJakub Wojciech Klama        return '{0}{1}{2}'.format(prefix, middle, self.message)
80*134e1779SJakub Wojciech Klama
81*134e1779SJakub Wojciech Klama    def is_ENOTSUP(self):
82*134e1779SJakub Wojciech Klama        if self.etype == 'Rlerror':
83*134e1779SJakub Wojciech Klama            return self.errno == lerrno.EOPNOTSUPP
84*134e1779SJakub Wojciech Klama        return self.errno == errno.EOPNOTSUPP
85*134e1779SJakub Wojciech Klama
86*134e1779SJakub Wojciech Klama    def _get_message(self):
87*134e1779SJakub Wojciech Klama        "get message based on self.msg or self.errno"
88*134e1779SJakub Wojciech Klama        if self.errno is not None:
89*134e1779SJakub Wojciech Klama            return {
90*134e1779SJakub Wojciech Klama                'Rlerror': p9err.dotl_strerror,
91*134e1779SJakub Wojciech Klama                'Rerror' : p9err.dotu_strerror,
92*134e1779SJakub Wojciech Klama            }[self.etype](self.errno)
93*134e1779SJakub Wojciech Klama        return self.msg
94*134e1779SJakub Wojciech Klama
95*134e1779SJakub Wojciech Klamaclass LocalError(P9Error):
96*134e1779SJakub Wojciech Klama    pass
97*134e1779SJakub Wojciech Klama
98*134e1779SJakub Wojciech Klamaclass TEError(LocalError):
99*134e1779SJakub Wojciech Klama    pass
100*134e1779SJakub Wojciech Klama
101*134e1779SJakub Wojciech Klamaclass P9SockIO(object):
102*134e1779SJakub Wojciech Klama    """
103*134e1779SJakub Wojciech Klama    Common base for server and client, handle send and
104*134e1779SJakub Wojciech Klama    receive to communications channel.  Note that this
105*134e1779SJakub Wojciech Klama    need not set up the channel initially, only the logger.
106*134e1779SJakub Wojciech Klama    The channel is typically connected later.  However, you
107*134e1779SJakub Wojciech Klama    can provide one initially.
108*134e1779SJakub Wojciech Klama    """
109*134e1779SJakub Wojciech Klama    def __init__(self, logger, name=None, server=None, port=STD_P9_PORT):
110*134e1779SJakub Wojciech Klama        self.logger = logger
111*134e1779SJakub Wojciech Klama        self.channel = None
112*134e1779SJakub Wojciech Klama        self.name = name
113*134e1779SJakub Wojciech Klama        self.maxio = None
114*134e1779SJakub Wojciech Klama        self.size_coder = struct.Struct('<I')
115*134e1779SJakub Wojciech Klama        if server is not None:
116*134e1779SJakub Wojciech Klama            self.connect(server, port)
117*134e1779SJakub Wojciech Klama        self.max_payload = 2**32 - self.size_coder.size
118*134e1779SJakub Wojciech Klama
119*134e1779SJakub Wojciech Klama    def __str__(self):
120*134e1779SJakub Wojciech Klama        if self.name:
121*134e1779SJakub Wojciech Klama            return self.name
122*134e1779SJakub Wojciech Klama        return repr(self)
123*134e1779SJakub Wojciech Klama
124*134e1779SJakub Wojciech Klama    def get_recommended_maxio(self):
125*134e1779SJakub Wojciech Klama        "suggest a max I/O size, for when self.maxio is 0 / unset"
126*134e1779SJakub Wojciech Klama        return 16 * 4096
127*134e1779SJakub Wojciech Klama
128*134e1779SJakub Wojciech Klama    def min_maxio(self):
129*134e1779SJakub Wojciech Klama        "return a minimum size below which we refuse to work"
130*134e1779SJakub Wojciech Klama        return self.size_coder.size + 100
131*134e1779SJakub Wojciech Klama
132*134e1779SJakub Wojciech Klama    def connect(self, server, port=STD_P9_PORT):
133*134e1779SJakub Wojciech Klama        """
134*134e1779SJakub Wojciech Klama        Connect to given server name / IP address.
135*134e1779SJakub Wojciech Klama
136*134e1779SJakub Wojciech Klama        If self.name was none, sets self.name to ip:port on success.
137*134e1779SJakub Wojciech Klama        """
138*134e1779SJakub Wojciech Klama        if self.is_connected():
139*134e1779SJakub Wojciech Klama            raise LocalError('already connected')
140*134e1779SJakub Wojciech Klama        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
141*134e1779SJakub Wojciech Klama        sock.connect((server, port))
142*134e1779SJakub Wojciech Klama        if self.name is None:
143*134e1779SJakub Wojciech Klama            if port == STD_P9_PORT:
144*134e1779SJakub Wojciech Klama                name = server
145*134e1779SJakub Wojciech Klama            else:
146*134e1779SJakub Wojciech Klama                name = '{0}:{1}'.format(server, port)
147*134e1779SJakub Wojciech Klama        else:
148*134e1779SJakub Wojciech Klama            name = None
149*134e1779SJakub Wojciech Klama        self.declare_connected(sock, name, None)
150*134e1779SJakub Wojciech Klama
151*134e1779SJakub Wojciech Klama    def is_connected(self):
152*134e1779SJakub Wojciech Klama        "predicate: are we connected?"
153*134e1779SJakub Wojciech Klama        return self.channel != None
154*134e1779SJakub Wojciech Klama
155*134e1779SJakub Wojciech Klama    def declare_connected(self, chan, name, maxio):
156*134e1779SJakub Wojciech Klama        """
157*134e1779SJakub Wojciech Klama        Now available for normal protocol (size-prefixed) I/O.
158*134e1779SJakub Wojciech Klama
159*134e1779SJakub Wojciech Klama        Replaces chan and name and adjusts maxio, if those
160*134e1779SJakub Wojciech Klama        parameters are not None.
161*134e1779SJakub Wojciech Klama        """
162*134e1779SJakub Wojciech Klama        if maxio:
163*134e1779SJakub Wojciech Klama            minio = self.min_maxio()
164*134e1779SJakub Wojciech Klama            if maxio < minio:
165*134e1779SJakub Wojciech Klama                raise LocalError('maxio={0} < minimum {1}'.format(maxio, minio))
166*134e1779SJakub Wojciech Klama        if chan is not None:
167*134e1779SJakub Wojciech Klama            self.channel = chan
168*134e1779SJakub Wojciech Klama        if name is not None:
169*134e1779SJakub Wojciech Klama            self.name = name
170*134e1779SJakub Wojciech Klama        if maxio is not None:
171*134e1779SJakub Wojciech Klama            self.maxio = maxio
172*134e1779SJakub Wojciech Klama            self.max_payload = maxio - self.size_coder.size
173*134e1779SJakub Wojciech Klama
174*134e1779SJakub Wojciech Klama    def reduce_maxio(self, maxio):
175*134e1779SJakub Wojciech Klama        "Reduce maximum I/O size per other-side request"
176*134e1779SJakub Wojciech Klama        minio = self.min_maxio()
177*134e1779SJakub Wojciech Klama        if maxio < minio:
178*134e1779SJakub Wojciech Klama            raise LocalError('new maxio={0} < minimum {1}'.format(maxio, minio))
179*134e1779SJakub Wojciech Klama        if maxio > self.maxio:
180*134e1779SJakub Wojciech Klama            raise LocalError('new maxio={0} > current {1}'.format(maxio,
181*134e1779SJakub Wojciech Klama                                                                  self.maxio))
182*134e1779SJakub Wojciech Klama        self.maxio = maxio
183*134e1779SJakub Wojciech Klama        self.max_payload = maxio - self.size_coder.size
184*134e1779SJakub Wojciech Klama
185*134e1779SJakub Wojciech Klama    def declare_disconnected(self):
186*134e1779SJakub Wojciech Klama        "Declare comm channel dead (note: leaves self.name set!)"
187*134e1779SJakub Wojciech Klama        self.channel = None
188*134e1779SJakub Wojciech Klama        self.maxio = None
189*134e1779SJakub Wojciech Klama
190*134e1779SJakub Wojciech Klama    def shutwrite(self):
191*134e1779SJakub Wojciech Klama        "Do a SHUT_WR on the outbound channel - can't send more"
192*134e1779SJakub Wojciech Klama        chan = self.channel
193*134e1779SJakub Wojciech Klama        # we're racing other threads here
194*134e1779SJakub Wojciech Klama        try:
195*134e1779SJakub Wojciech Klama            chan.shutdown(socket.SHUT_WR)
196*134e1779SJakub Wojciech Klama        except (OSError, AttributeError):
197*134e1779SJakub Wojciech Klama            pass
198*134e1779SJakub Wojciech Klama
199*134e1779SJakub Wojciech Klama    def shutdown(self):
200*134e1779SJakub Wojciech Klama        "Shut down comm channel"
201*134e1779SJakub Wojciech Klama        if self.channel:
202*134e1779SJakub Wojciech Klama            try:
203*134e1779SJakub Wojciech Klama                self.channel.shutdown(socket.SHUT_RDWR)
204*134e1779SJakub Wojciech Klama            except socket.error:
205*134e1779SJakub Wojciech Klama                pass
206*134e1779SJakub Wojciech Klama            self.channel.close()
207*134e1779SJakub Wojciech Klama            self.declare_disconnected()
208*134e1779SJakub Wojciech Klama
209*134e1779SJakub Wojciech Klama    def read(self):
210*134e1779SJakub Wojciech Klama        """
211*134e1779SJakub Wojciech Klama        Try to read a complete packet.
212*134e1779SJakub Wojciech Klama
213*134e1779SJakub Wojciech Klama        Returns '' for EOF, as read() usually does.
214*134e1779SJakub Wojciech Klama
215*134e1779SJakub Wojciech Klama        If we can't even get the size, this still returns ''.
216*134e1779SJakub Wojciech Klama        If we get a sensible size but are missing some data,
217*134e1779SJakub Wojciech Klama        we can return a short packet.  Since we know if we did
218*134e1779SJakub Wojciech Klama        this, we also return a boolean: True means "really got a
219*134e1779SJakub Wojciech Klama        complete packet."
220*134e1779SJakub Wojciech Klama
221*134e1779SJakub Wojciech Klama        Note that '' EOF always returns False: EOF is never a
222*134e1779SJakub Wojciech Klama        complete packet.
223*134e1779SJakub Wojciech Klama        """
224*134e1779SJakub Wojciech Klama        if self.channel is None:
225*134e1779SJakub Wojciech Klama            return b'', False
226*134e1779SJakub Wojciech Klama        size_field = self.xread(self.size_coder.size)
227*134e1779SJakub Wojciech Klama        if len(size_field) < self.size_coder.size:
228*134e1779SJakub Wojciech Klama            if len(size_field) == 0:
229*134e1779SJakub Wojciech Klama                self.logger.log(logging.INFO, '%s: normal EOF', self)
230*134e1779SJakub Wojciech Klama            else:
231*134e1779SJakub Wojciech Klama                self.logger.log(logging.ERROR,
232*134e1779SJakub Wojciech Klama                               '%s: EOF while reading size (got %d bytes)',
233*134e1779SJakub Wojciech Klama                               self, len(size_field))
234*134e1779SJakub Wojciech Klama                # should we raise an error here?
235*134e1779SJakub Wojciech Klama            return b'', False
236*134e1779SJakub Wojciech Klama
237*134e1779SJakub Wojciech Klama        size = self.size_coder.unpack(size_field)[0] - self.size_coder.size
238*134e1779SJakub Wojciech Klama        if size <= 0 or size > self.max_payload:
239*134e1779SJakub Wojciech Klama            self.logger.log(logging.ERROR,
240*134e1779SJakub Wojciech Klama                            '%s: incoming size %d is insane '
241*134e1779SJakub Wojciech Klama                            '(max payload is %d)',
242*134e1779SJakub Wojciech Klama                            self, size, self.max_payload)
243*134e1779SJakub Wojciech Klama            # indicate EOF - should we raise an error instead, here?
244*134e1779SJakub Wojciech Klama            return b'', False
245*134e1779SJakub Wojciech Klama        data = self.xread(size)
246*134e1779SJakub Wojciech Klama        return data, len(data) == size
247*134e1779SJakub Wojciech Klama
248*134e1779SJakub Wojciech Klama    def xread(self, nbytes):
249*134e1779SJakub Wojciech Klama        """
250*134e1779SJakub Wojciech Klama        Read nbytes bytes, looping if necessary.  Return '' for
251*134e1779SJakub Wojciech Klama        EOF; may return a short count if we get some data, then
252*134e1779SJakub Wojciech Klama        EOF.
253*134e1779SJakub Wojciech Klama        """
254*134e1779SJakub Wojciech Klama        assert nbytes > 0
255*134e1779SJakub Wojciech Klama        # Try to get everything at once (should usually succeed).
256*134e1779SJakub Wojciech Klama        # Return immediately for EOF or got-all-data.
257*134e1779SJakub Wojciech Klama        data = self.channel.recv(nbytes)
258*134e1779SJakub Wojciech Klama        if data == b'' or len(data) == nbytes:
259*134e1779SJakub Wojciech Klama            return data
260*134e1779SJakub Wojciech Klama
261*134e1779SJakub Wojciech Klama        # Gather data fragments into an array, then join it all at
262*134e1779SJakub Wojciech Klama        # the end.
263*134e1779SJakub Wojciech Klama        count = len(data)
264*134e1779SJakub Wojciech Klama        data = [data]
265*134e1779SJakub Wojciech Klama        while count < nbytes:
266*134e1779SJakub Wojciech Klama            more = self.channel.recv(nbytes - count)
267*134e1779SJakub Wojciech Klama            if more == b'':
268*134e1779SJakub Wojciech Klama                break
269*134e1779SJakub Wojciech Klama            count += len(more)
270*134e1779SJakub Wojciech Klama            data.append(more)
271*134e1779SJakub Wojciech Klama        return b''.join(data)
272*134e1779SJakub Wojciech Klama
273*134e1779SJakub Wojciech Klama    def write(self, data):
274*134e1779SJakub Wojciech Klama        """
275*134e1779SJakub Wojciech Klama        Write all the data, in the usual encoding.  Note that
276*134e1779SJakub Wojciech Klama        the length of the data, including the length of the length
277*134e1779SJakub Wojciech Klama        itself, is already encoded in the first 4 bytes of the
278*134e1779SJakub Wojciech Klama        data.
279*134e1779SJakub Wojciech Klama
280*134e1779SJakub Wojciech Klama        Raises IOError if we can't write everything.
281*134e1779SJakub Wojciech Klama
282*134e1779SJakub Wojciech Klama        Raises LocalError if len(data) exceeds max_payload.
283*134e1779SJakub Wojciech Klama        """
284*134e1779SJakub Wojciech Klama        size = len(data)
285*134e1779SJakub Wojciech Klama        assert size >= 4
286*134e1779SJakub Wojciech Klama        if size > self.max_payload:
287*134e1779SJakub Wojciech Klama            raise LocalError('data length {0} exceeds '
288*134e1779SJakub Wojciech Klama                             'maximum {1}'.format(size, self.max_payload))
289*134e1779SJakub Wojciech Klama        self.channel.sendall(data)
290*134e1779SJakub Wojciech Klama
291*134e1779SJakub Wojciech Klamadef _pathcat(prefix, suffix):
292*134e1779SJakub Wojciech Klama    """
293*134e1779SJakub Wojciech Klama    Concatenate paths we are using on the server side.  This is
294*134e1779SJakub Wojciech Klama    basically just prefix + / + suffix, with two complications:
295*134e1779SJakub Wojciech Klama
296*134e1779SJakub Wojciech Klama    It's possible we don't have a prefix path, in which case
297*134e1779SJakub Wojciech Klama    we want the suffix without a leading slash.
298*134e1779SJakub Wojciech Klama
299*134e1779SJakub Wojciech Klama    It's possible that the prefix is just b'/', in which case we
300*134e1779SJakub Wojciech Klama    want prefix + suffix.
301*134e1779SJakub Wojciech Klama    """
302*134e1779SJakub Wojciech Klama    if prefix:
303*134e1779SJakub Wojciech Klama        if prefix == b'/':  # or prefix.endswith(b'/')?
304*134e1779SJakub Wojciech Klama            return prefix + suffix
305*134e1779SJakub Wojciech Klama        return prefix + b'/' + suffix
306*134e1779SJakub Wojciech Klama    return suffix
307*134e1779SJakub Wojciech Klama
308*134e1779SJakub Wojciech Klamaclass P9Client(P9SockIO):
309*134e1779SJakub Wojciech Klama    """
310*134e1779SJakub Wojciech Klama    Act as client.
311*134e1779SJakub Wojciech Klama
312*134e1779SJakub Wojciech Klama    We need the a logger (see logging), a timeout, and a protocol
313*134e1779SJakub Wojciech Klama    version to request.  By default, we will downgrade to a lower
314*134e1779SJakub Wojciech Klama    version if asked.
315*134e1779SJakub Wojciech Klama
316*134e1779SJakub Wojciech Klama    If server and port are supplied, they are remembered and become
317*134e1779SJakub Wojciech Klama    the default for .connect() (which is still deferred).
318*134e1779SJakub Wojciech Klama
319*134e1779SJakub Wojciech Klama    Note that we keep a table of fid-to-path in self.live_fids,
320*134e1779SJakub Wojciech Klama    but at any time (except while holding the lock) a fid can
321*134e1779SJakub Wojciech Klama    be deleted entirely, and the table entry may just be True
322*134e1779SJakub Wojciech Klama    if we have no path name.  In general, we update the name
323*134e1779SJakub Wojciech Klama    when we can.
324*134e1779SJakub Wojciech Klama    """
325*134e1779SJakub Wojciech Klama    def __init__(self, logger, timeout, version, may_downgrade=True,
326*134e1779SJakub Wojciech Klama                 server=None, port=None):
327*134e1779SJakub Wojciech Klama        super(P9Client, self).__init__(logger)
328*134e1779SJakub Wojciech Klama        self.timeout = timeout
329*134e1779SJakub Wojciech Klama        self.iproto = protocol.p9_version(version)
330*134e1779SJakub Wojciech Klama        self.may_downgrade = may_downgrade
331*134e1779SJakub Wojciech Klama        self.tagalloc = numalloc.NumAlloc(0, 65534)
332*134e1779SJakub Wojciech Klama        self.tagstate = {}
333*134e1779SJakub Wojciech Klama        # The next bit is slighlty dirty: perhaps we should just
334*134e1779SJakub Wojciech Klama        # allocate NOFID out of the 2**32-1 range, so as to avoid
335*134e1779SJakub Wojciech Klama        # "knowing" that it's 2**32-1.
336*134e1779SJakub Wojciech Klama        self.fidalloc = numalloc.NumAlloc(0, protocol.td.NOFID - 1)
337*134e1779SJakub Wojciech Klama        self.live_fids = {}
338*134e1779SJakub Wojciech Klama        self.rootfid = None
339*134e1779SJakub Wojciech Klama        self.rootqid = None
340*134e1779SJakub Wojciech Klama        self.rthread = None
341*134e1779SJakub Wojciech Klama        self.lock = threading.Lock()
342*134e1779SJakub Wojciech Klama        self.new_replies = threading.Condition(self.lock)
343*134e1779SJakub Wojciech Klama        self._monkeywrench = {}
344*134e1779SJakub Wojciech Klama        self._server = server
345*134e1779SJakub Wojciech Klama        self._port = port
346*134e1779SJakub Wojciech Klama        self._unsup = {}
347*134e1779SJakub Wojciech Klama
348*134e1779SJakub Wojciech Klama    def get_monkey(self, what):
349*134e1779SJakub Wojciech Klama        "check for a monkey-wrench"
350*134e1779SJakub Wojciech Klama        with self.lock:
351*134e1779SJakub Wojciech Klama            wrench = self._monkeywrench.get(what)
352*134e1779SJakub Wojciech Klama            if wrench is None:
353*134e1779SJakub Wojciech Klama                return None
354*134e1779SJakub Wojciech Klama            if isinstance(wrench, list):
355*134e1779SJakub Wojciech Klama                # repeats wrench[0] times, or forever if that's 0
356*134e1779SJakub Wojciech Klama                ret = wrench[1]
357*134e1779SJakub Wojciech Klama                if wrench[0] > 0:
358*134e1779SJakub Wojciech Klama                    wrench[0] -= 1
359*134e1779SJakub Wojciech Klama                    if wrench[0] == 0:
360*134e1779SJakub Wojciech Klama                        del self._monkeywrench[what]
361*134e1779SJakub Wojciech Klama            else:
362*134e1779SJakub Wojciech Klama                ret = wrench
363*134e1779SJakub Wojciech Klama                del self._monkeywrench[what]
364*134e1779SJakub Wojciech Klama        return ret
365*134e1779SJakub Wojciech Klama
366*134e1779SJakub Wojciech Klama    def set_monkey(self, what, how, repeat=None):
367*134e1779SJakub Wojciech Klama        """
368*134e1779SJakub Wojciech Klama        Set a monkey-wrench.  If repeat is not None it is the number of
369*134e1779SJakub Wojciech Klama        times the wrench is applied (0 means forever, or until you call
370*134e1779SJakub Wojciech Klama        set again with how=None).  What is what to monkey-wrench, which
371*134e1779SJakub Wojciech Klama        depends on the op.  How is generally a replacement value.
372*134e1779SJakub Wojciech Klama        """
373*134e1779SJakub Wojciech Klama        if how is None:
374*134e1779SJakub Wojciech Klama            with self.lock:
375*134e1779SJakub Wojciech Klama                try:
376*134e1779SJakub Wojciech Klama                    del self._monkeywrench[what]
377*134e1779SJakub Wojciech Klama                except KeyError:
378*134e1779SJakub Wojciech Klama                    pass
379*134e1779SJakub Wojciech Klama            return
380*134e1779SJakub Wojciech Klama        if repeat is not None:
381*134e1779SJakub Wojciech Klama            how = [repeat, how]
382*134e1779SJakub Wojciech Klama        with self.lock:
383*134e1779SJakub Wojciech Klama            self._monkeywrench[what] = how
384*134e1779SJakub Wojciech Klama
385*134e1779SJakub Wojciech Klama    def get_tag(self, for_Tversion=False):
386*134e1779SJakub Wojciech Klama        "get next available tag ID"
387*134e1779SJakub Wojciech Klama        with self.lock:
388*134e1779SJakub Wojciech Klama            if for_Tversion:
389*134e1779SJakub Wojciech Klama                tag = 65535
390*134e1779SJakub Wojciech Klama            else:
391*134e1779SJakub Wojciech Klama                tag = self.tagalloc.alloc()
392*134e1779SJakub Wojciech Klama            if tag is None:
393*134e1779SJakub Wojciech Klama                raise LocalError('all tags in use')
394*134e1779SJakub Wojciech Klama            self.tagstate[tag] = True # ie, in use, still waiting
395*134e1779SJakub Wojciech Klama        return tag
396*134e1779SJakub Wojciech Klama
397*134e1779SJakub Wojciech Klama    def set_tag(self, tag, reply):
398*134e1779SJakub Wojciech Klama        "set the reply info for the given tag"
399*134e1779SJakub Wojciech Klama        assert tag >= 0 and tag < 65536
400*134e1779SJakub Wojciech Klama        with self.lock:
401*134e1779SJakub Wojciech Klama            # check whether we're still waiting for the tag
402*134e1779SJakub Wojciech Klama            state = self.tagstate.get(tag)
403*134e1779SJakub Wojciech Klama            if state is True:
404*134e1779SJakub Wojciech Klama                self.tagstate[tag] = reply # i.e., here's the answer
405*134e1779SJakub Wojciech Klama                self.new_replies.notify_all()
406*134e1779SJakub Wojciech Klama                return
407*134e1779SJakub Wojciech Klama            # state must be one of these...
408*134e1779SJakub Wojciech Klama            if state is False:
409*134e1779SJakub Wojciech Klama                # We gave up on this tag.  Reply came anyway.
410*134e1779SJakub Wojciech Klama                self.logger.log(logging.INFO,
411*134e1779SJakub Wojciech Klama                                '%s: got tag %d = %r after timing out on it',
412*134e1779SJakub Wojciech Klama                                self, tag, reply)
413*134e1779SJakub Wojciech Klama                self.retire_tag_locked(tag)
414*134e1779SJakub Wojciech Klama                return
415*134e1779SJakub Wojciech Klama            if state is None:
416*134e1779SJakub Wojciech Klama                # We got a tag back from the server that was not
417*134e1779SJakub Wojciech Klama                # outstanding!
418*134e1779SJakub Wojciech Klama                self.logger.log(logging.WARNING,
419*134e1779SJakub Wojciech Klama                                '%s: got tag %d = %r when tag %d not in use!',
420*134e1779SJakub Wojciech Klama                                self, tag, reply, tag)
421*134e1779SJakub Wojciech Klama                return
422*134e1779SJakub Wojciech Klama            # We got a second reply before handling the first reply!
423*134e1779SJakub Wojciech Klama            self.logger.log(logging.WARNING,
424*134e1779SJakub Wojciech Klama                            '%s: got tag %d = %r when tag %d = %r!',
425*134e1779SJakub Wojciech Klama                            self, tag, reply, tag, state)
426*134e1779SJakub Wojciech Klama            return
427*134e1779SJakub Wojciech Klama
428*134e1779SJakub Wojciech Klama    def retire_tag(self, tag):
429*134e1779SJakub Wojciech Klama        "retire the given tag - only used by the thread that handled the result"
430*134e1779SJakub Wojciech Klama        if tag == 65535:
431*134e1779SJakub Wojciech Klama            return
432*134e1779SJakub Wojciech Klama        assert tag >= 0 and tag < 65535
433*134e1779SJakub Wojciech Klama        with self.lock:
434*134e1779SJakub Wojciech Klama            self.retire_tag_locked(tag)
435*134e1779SJakub Wojciech Klama
436*134e1779SJakub Wojciech Klama    def retire_tag_locked(self, tag):
437*134e1779SJakub Wojciech Klama        "retire the given tag while holding self.lock"
438*134e1779SJakub Wojciech Klama        # must check "in tagstate" because we can race
439*134e1779SJakub Wojciech Klama        # with retire_all_tags.
440*134e1779SJakub Wojciech Klama        if tag in self.tagstate:
441*134e1779SJakub Wojciech Klama            del self.tagstate[tag]
442*134e1779SJakub Wojciech Klama            self.tagalloc.free(tag)
443*134e1779SJakub Wojciech Klama
444*134e1779SJakub Wojciech Klama    def retire_all_tags(self):
445*134e1779SJakub Wojciech Klama        "retire all tags, after connection drop"
446*134e1779SJakub Wojciech Klama        with self.lock:
447*134e1779SJakub Wojciech Klama            # release all tags in any state (waiting, answered, timedout)
448*134e1779SJakub Wojciech Klama            self.tagalloc.free_multi(self.tagstate.keys())
449*134e1779SJakub Wojciech Klama            self.tagstate = {}
450*134e1779SJakub Wojciech Klama            self.new_replies.notify_all()
451*134e1779SJakub Wojciech Klama
452*134e1779SJakub Wojciech Klama    def alloc_fid(self):
453*134e1779SJakub Wojciech Klama        "allocate new fid"
454*134e1779SJakub Wojciech Klama        with self.lock:
455*134e1779SJakub Wojciech Klama            fid = self.fidalloc.alloc()
456*134e1779SJakub Wojciech Klama            self.live_fids[fid] = True
457*134e1779SJakub Wojciech Klama        return fid
458*134e1779SJakub Wojciech Klama
459*134e1779SJakub Wojciech Klama    def getpath(self, fid):
460*134e1779SJakub Wojciech Klama        "get path from fid, or return None if no path known, or not valid"
461*134e1779SJakub Wojciech Klama        with self.lock:
462*134e1779SJakub Wojciech Klama            path = self.live_fids.get(fid)
463*134e1779SJakub Wojciech Klama        if path is True:
464*134e1779SJakub Wojciech Klama            path = None
465*134e1779SJakub Wojciech Klama        return path
466*134e1779SJakub Wojciech Klama
467*134e1779SJakub Wojciech Klama    def getpathX(self, fid):
468*134e1779SJakub Wojciech Klama        """
469*134e1779SJakub Wojciech Klama        Much like getpath, but return <fid N, unknown path> if necessary.
470*134e1779SJakub Wojciech Klama        If we do have a path, return its repr().
471*134e1779SJakub Wojciech Klama        """
472*134e1779SJakub Wojciech Klama        path = self.getpath(fid)
473*134e1779SJakub Wojciech Klama        if path is None:
474*134e1779SJakub Wojciech Klama            return '<fid {0}, unknown path>'.format(fid)
475*134e1779SJakub Wojciech Klama        return repr(path)
476*134e1779SJakub Wojciech Klama
477*134e1779SJakub Wojciech Klama    def setpath(self, fid, path):
478*134e1779SJakub Wojciech Klama        "associate fid with new path (possibly from another fid)"
479*134e1779SJakub Wojciech Klama        with self.lock:
480*134e1779SJakub Wojciech Klama            if isinstance(path, int):
481*134e1779SJakub Wojciech Klama                path = self.live_fids.get(path)
482*134e1779SJakub Wojciech Klama            # path might now be None (not a live fid after all), or
483*134e1779SJakub Wojciech Klama            # True (we have no path name), or potentially even the
484*134e1779SJakub Wojciech Klama            # empty string (invalid for our purposes).  Treat all of
485*134e1779SJakub Wojciech Klama            # those as True, meaning "no known path".
486*134e1779SJakub Wojciech Klama            if not path:
487*134e1779SJakub Wojciech Klama                path = True
488*134e1779SJakub Wojciech Klama            if self.live_fids.get(fid):
489*134e1779SJakub Wojciech Klama                # Existing fid maps to either True or its old path.
490*134e1779SJakub Wojciech Klama                # Set the new path (which may be just a placeholder).
491*134e1779SJakub Wojciech Klama                self.live_fids[fid] = path
492*134e1779SJakub Wojciech Klama
493*134e1779SJakub Wojciech Klama    def did_rename(self, fid, ncomp, newdir=None):
494*134e1779SJakub Wojciech Klama        """
495*134e1779SJakub Wojciech Klama        Announce that we renamed using a fid - we'll try to update
496*134e1779SJakub Wojciech Klama        other fids based on this (we can't really do it perfectly).
497*134e1779SJakub Wojciech Klama
498*134e1779SJakub Wojciech Klama        NOTE: caller must provide a final-component.
499*134e1779SJakub Wojciech Klama        The caller can supply the new path (and should
500*134e1779SJakub Wojciech Klama        do so if the rename is not based on the retained path
501*134e1779SJakub Wojciech Klama        for the supplied fid, i.e., for rename ops where fid
502*134e1779SJakub Wojciech Klama        can move across directories).  The rules:
503*134e1779SJakub Wojciech Klama
504*134e1779SJakub Wojciech Klama         - If newdir is None (default), we use stored path.
505*134e1779SJakub Wojciech Klama         - Otherwise, newdir provides the best approximation
506*134e1779SJakub Wojciech Klama           we have to the path that needs ncomp appended.
507*134e1779SJakub Wojciech Klama
508*134e1779SJakub Wojciech Klama        (This is based on the fact that renames happen via Twstat
509*134e1779SJakub Wojciech Klama        or Trename, or Trenameat, which change just one tail component,
510*134e1779SJakub Wojciech Klama        but the path names vary.)
511*134e1779SJakub Wojciech Klama        """
512*134e1779SJakub Wojciech Klama        if ncomp is None:
513*134e1779SJakub Wojciech Klama            return
514*134e1779SJakub Wojciech Klama        opath = self.getpath(fid)
515*134e1779SJakub Wojciech Klama        if newdir is None:
516*134e1779SJakub Wojciech Klama            if opath is None:
517*134e1779SJakub Wojciech Klama                return
518*134e1779SJakub Wojciech Klama            ocomps = opath.split(b'/')
519*134e1779SJakub Wojciech Klama            ncomps = ocomps[0:-1]
520*134e1779SJakub Wojciech Klama        else:
521*134e1779SJakub Wojciech Klama            ocomps = None           # well, none yet anyway
522*134e1779SJakub Wojciech Klama            ncomps = newdir.split(b'/')
523*134e1779SJakub Wojciech Klama        ncomps.append(ncomp)
524*134e1779SJakub Wojciech Klama        if opath is None or opath[0] != '/':
525*134e1779SJakub Wojciech Klama            # We don't have enough information to fix anything else.
526*134e1779SJakub Wojciech Klama            # Just store the new path and return.  We have at least
527*134e1779SJakub Wojciech Klama            # a partial path now, which is no worse than before.
528*134e1779SJakub Wojciech Klama            npath = b'/'.join(ncomps)
529*134e1779SJakub Wojciech Klama            with self.lock:
530*134e1779SJakub Wojciech Klama                if fid in self.live_fids:
531*134e1779SJakub Wojciech Klama                    self.live_fids[fid] = npath
532*134e1779SJakub Wojciech Klama            return
533*134e1779SJakub Wojciech Klama        if ocomps is None:
534*134e1779SJakub Wojciech Klama            ocomps = opath.split(b'/')
535*134e1779SJakub Wojciech Klama        olen = len(ocomps)
536*134e1779SJakub Wojciech Klama        ofinal = ocomps[olen - 1]
537*134e1779SJakub Wojciech Klama        # Old paths is full path.  Find any other fids that start
538*134e1779SJakub Wojciech Klama        # with some or all the components in ocomps.  Note that if
539*134e1779SJakub Wojciech Klama        # we renamed /one/two/three to /four/five this winds up
540*134e1779SJakub Wojciech Klama        # renaming files /one/a to /four/a, /one/two/b to /four/five/b,
541*134e1779SJakub Wojciech Klama        # and so on.
542*134e1779SJakub Wojciech Klama        with self.lock:
543*134e1779SJakub Wojciech Klama            for fid2, path2 in self.live_fids.iteritems():
544*134e1779SJakub Wojciech Klama                # Skip fids without byte-string paths
545*134e1779SJakub Wojciech Klama                if not isinstance(path2, bytes):
546*134e1779SJakub Wojciech Klama                    continue
547*134e1779SJakub Wojciech Klama                # Before splitting (which is a bit expensive), try
548*134e1779SJakub Wojciech Klama                # a straightforward prefix match.  This might give
549*134e1779SJakub Wojciech Klama                # some false hits, e.g., prefix /one/two/threepenny
550*134e1779SJakub Wojciech Klama                # starts with /one/two/three, but it quickly eliminates
551*134e1779SJakub Wojciech Klama                # /raz/baz/mataz and the like.
552*134e1779SJakub Wojciech Klama                if not path2.startswith(opath):
553*134e1779SJakub Wojciech Klama                    continue
554*134e1779SJakub Wojciech Klama                # Split up the path, and use that to make sure that
555*134e1779SJakub Wojciech Klama                # the final component is a full match.
556*134e1779SJakub Wojciech Klama                parts2 = path2.split(b'/')
557*134e1779SJakub Wojciech Klama                if parts2[olen - 1] != ofinal:
558*134e1779SJakub Wojciech Klama                    continue
559*134e1779SJakub Wojciech Klama                # OK, path2 starts with the old (renamed) sequence.
560*134e1779SJakub Wojciech Klama                # Replace the old components with the new ones.
561*134e1779SJakub Wojciech Klama                # This updates the renamed fid when we come across
562*134e1779SJakub Wojciech Klama                # it!  It also handles a change in the number of
563*134e1779SJakub Wojciech Klama                # components, thanks to Python's slice assignment.
564*134e1779SJakub Wojciech Klama                parts2[0:olen] = ncomps
565*134e1779SJakub Wojciech Klama                self.live_fids[fid2] = b'/'.join(parts2)
566*134e1779SJakub Wojciech Klama
567*134e1779SJakub Wojciech Klama    def retire_fid(self, fid):
568*134e1779SJakub Wojciech Klama        "retire one fid"
569*134e1779SJakub Wojciech Klama        with self.lock:
570*134e1779SJakub Wojciech Klama            self.fidalloc.free(fid)
571*134e1779SJakub Wojciech Klama            del self.live_fids[fid]
572*134e1779SJakub Wojciech Klama
573*134e1779SJakub Wojciech Klama    def retire_all_fids(self):
574*134e1779SJakub Wojciech Klama        "return live fids to pool"
575*134e1779SJakub Wojciech Klama        # this is useful for debugging fid leaks:
576*134e1779SJakub Wojciech Klama        #for fid in self.live_fids:
577*134e1779SJakub Wojciech Klama        #    print 'retiring', fid, self.getpathX(fid)
578*134e1779SJakub Wojciech Klama        with self.lock:
579*134e1779SJakub Wojciech Klama            self.fidalloc.free_multi(self.live_fids.keys())
580*134e1779SJakub Wojciech Klama            self.live_fids = {}
581*134e1779SJakub Wojciech Klama
582*134e1779SJakub Wojciech Klama    def read_responses(self):
583*134e1779SJakub Wojciech Klama        "Read responses.  This gets spun off as a thread."
584*134e1779SJakub Wojciech Klama        while self.is_connected():
585*134e1779SJakub Wojciech Klama            pkt, is_full = super(P9Client, self).read()
586*134e1779SJakub Wojciech Klama            if pkt == b'':
587*134e1779SJakub Wojciech Klama                self.shutwrite()
588*134e1779SJakub Wojciech Klama                self.retire_all_tags()
589*134e1779SJakub Wojciech Klama                return
590*134e1779SJakub Wojciech Klama            if not is_full:
591*134e1779SJakub Wojciech Klama                self.logger.log(logging.WARNING, '%s: got short packet', self)
592*134e1779SJakub Wojciech Klama            try:
593*134e1779SJakub Wojciech Klama                # We have one special case: if we're not yet connected
594*134e1779SJakub Wojciech Klama                # with a version, we must unpack *as if* it's a plain
595*134e1779SJakub Wojciech Klama                # 9P2000 response.
596*134e1779SJakub Wojciech Klama                if self.have_version:
597*134e1779SJakub Wojciech Klama                    resp = self.proto.unpack(pkt)
598*134e1779SJakub Wojciech Klama                else:
599*134e1779SJakub Wojciech Klama                    resp = protocol.plain.unpack(pkt)
600*134e1779SJakub Wojciech Klama            except protocol.SequenceError as err:
601*134e1779SJakub Wojciech Klama                self.logger.log(logging.ERROR, '%s: bad response: %s',
602*134e1779SJakub Wojciech Klama                                self, err)
603*134e1779SJakub Wojciech Klama                try:
604*134e1779SJakub Wojciech Klama                    resp = self.proto.unpack(pkt, noerror=True)
605*134e1779SJakub Wojciech Klama                except protocol.SequenceError:
606*134e1779SJakub Wojciech Klama                    header = self.proto.unpack_header(pkt, noerror=True)
607*134e1779SJakub Wojciech Klama                    self.logger.log(logging.ERROR,
608*134e1779SJakub Wojciech Klama                                    '%s: (not even raw-decodable)', self)
609*134e1779SJakub Wojciech Klama                    self.logger.log(logging.ERROR,
610*134e1779SJakub Wojciech Klama                                    '%s: header decode produced %r',
611*134e1779SJakub Wojciech Klama                                    self, header)
612*134e1779SJakub Wojciech Klama                else:
613*134e1779SJakub Wojciech Klama                    self.logger.log(logging.ERROR,
614*134e1779SJakub Wojciech Klama                                    '%s: raw decode produced %r',
615*134e1779SJakub Wojciech Klama                                    self, resp)
616*134e1779SJakub Wojciech Klama                # after this kind of problem, probably need to
617*134e1779SJakub Wojciech Klama                # shut down, but let's leave that out for a bit
618*134e1779SJakub Wojciech Klama            else:
619*134e1779SJakub Wojciech Klama                # NB: all protocol responses have a "tag",
620*134e1779SJakub Wojciech Klama                # so resp['tag'] always exists.
621*134e1779SJakub Wojciech Klama                self.logger.log(logging.DEBUG, "read_resp: tag %d resp %r", resp.tag, resp)
622*134e1779SJakub Wojciech Klama                self.set_tag(resp.tag, resp)
623*134e1779SJakub Wojciech Klama
624*134e1779SJakub Wojciech Klama    def wait_for(self, tag):
625*134e1779SJakub Wojciech Klama        """
626*134e1779SJakub Wojciech Klama        Wait for a response to the given tag.  Return the response,
627*134e1779SJakub Wojciech Klama        releasing the tag.  If self.timeout is not None, wait at most
628*134e1779SJakub Wojciech Klama        that long (and release the tag even if there's no reply), else
629*134e1779SJakub Wojciech Klama        wait forever.
630*134e1779SJakub Wojciech Klama
631*134e1779SJakub Wojciech Klama        If this returns None, either the tag was bad initially, or
632*134e1779SJakub Wojciech Klama        a timeout occurred, or the connection got shut down.
633*134e1779SJakub Wojciech Klama        """
634*134e1779SJakub Wojciech Klama        self.logger.log(logging.DEBUG, "wait_for: tag %d", tag)
635*134e1779SJakub Wojciech Klama        if self.timeout is None:
636*134e1779SJakub Wojciech Klama            deadline = None
637*134e1779SJakub Wojciech Klama        else:
638*134e1779SJakub Wojciech Klama            deadline = time.time() + self.timeout
639*134e1779SJakub Wojciech Klama        with self.lock:
640*134e1779SJakub Wojciech Klama            while True:
641*134e1779SJakub Wojciech Klama                # tagstate is True (waiting) or False (timedout) or
642*134e1779SJakub Wojciech Klama                # a valid response, or None if we've reset the tag
643*134e1779SJakub Wojciech Klama                # states (retire_all_tags, after connection drop).
644*134e1779SJakub Wojciech Klama                resp = self.tagstate.get(tag, None)
645*134e1779SJakub Wojciech Klama                if resp is None:
646*134e1779SJakub Wojciech Klama                    # out of sync, exit loop
647*134e1779SJakub Wojciech Klama                    break
648*134e1779SJakub Wojciech Klama                if resp is True:
649*134e1779SJakub Wojciech Klama                    # still waiting for a response - wait some more
650*134e1779SJakub Wojciech Klama                    self.new_replies.wait(self.timeout)
651*134e1779SJakub Wojciech Klama                    if deadline and time.time() > deadline:
652*134e1779SJakub Wojciech Klama                        # Halt the waiting, but go around once more.
653*134e1779SJakub Wojciech Klama                        # Note we may have killed the tag by now though.
654*134e1779SJakub Wojciech Klama                        if tag in self.tagstate:
655*134e1779SJakub Wojciech Klama                            self.tagstate[tag] = False
656*134e1779SJakub Wojciech Klama                    continue
657*134e1779SJakub Wojciech Klama                # resp is either False (timeout) or a reply.
658*134e1779SJakub Wojciech Klama                # If resp is False, change it to None; the tag
659*134e1779SJakub Wojciech Klama                # is now dead until we get a reply (then we
660*134e1779SJakub Wojciech Klama                # just toss the reply).
661*134e1779SJakub Wojciech Klama                # Otherwise, we're done with the tag: free it.
662*134e1779SJakub Wojciech Klama                # In either case, stop now.
663*134e1779SJakub Wojciech Klama                if resp is False:
664*134e1779SJakub Wojciech Klama                    resp = None
665*134e1779SJakub Wojciech Klama                else:
666*134e1779SJakub Wojciech Klama                    self.tagalloc.free(tag)
667*134e1779SJakub Wojciech Klama                    del self.tagstate[tag]
668*134e1779SJakub Wojciech Klama                break
669*134e1779SJakub Wojciech Klama        return resp
670*134e1779SJakub Wojciech Klama
671*134e1779SJakub Wojciech Klama    def badresp(self, req, resp):
672*134e1779SJakub Wojciech Klama        """
673*134e1779SJakub Wojciech Klama        Complain that a response was not something expected.
674*134e1779SJakub Wojciech Klama        """
675*134e1779SJakub Wojciech Klama        if resp is None:
676*134e1779SJakub Wojciech Klama            self.shutdown()
677*134e1779SJakub Wojciech Klama            raise TEError('{0}: {1}: timeout or EOF'.format(self, req))
678*134e1779SJakub Wojciech Klama        if isinstance(resp, protocol.rrd.Rlerror):
679*134e1779SJakub Wojciech Klama            raise RemoteError(self, req, None, 'Rlerror', resp.ecode)
680*134e1779SJakub Wojciech Klama        if isinstance(resp, protocol.rrd.Rerror):
681*134e1779SJakub Wojciech Klama            if resp.errnum is None:
682*134e1779SJakub Wojciech Klama                raise RemoteError(self, req, resp.errstr, 'Rerror', None)
683*134e1779SJakub Wojciech Klama            raise RemoteError(self, req, None, 'Rerror', resp.errnum)
684*134e1779SJakub Wojciech Klama        raise LocalError('{0}: {1} got response {2!r}'.format(self, req, resp))
685*134e1779SJakub Wojciech Klama
686*134e1779SJakub Wojciech Klama    def supports(self, req_code):
687*134e1779SJakub Wojciech Klama        """
688*134e1779SJakub Wojciech Klama        Test self.proto.support(req_code) unless we've recorded that
689*134e1779SJakub Wojciech Klama        while the protocol supports it, the client does not.
690*134e1779SJakub Wojciech Klama        """
691*134e1779SJakub Wojciech Klama        return req_code not in self._unsup and self.proto.supports(req_code)
692*134e1779SJakub Wojciech Klama
693*134e1779SJakub Wojciech Klama    def supports_all(self, *req_codes):
694*134e1779SJakub Wojciech Klama        "basically just all(supports(...))"
695*134e1779SJakub Wojciech Klama        return all(self.supports(code) for code in req_codes)
696*134e1779SJakub Wojciech Klama
697*134e1779SJakub Wojciech Klama    def unsupported(self, req_code):
698*134e1779SJakub Wojciech Klama        """
699*134e1779SJakub Wojciech Klama        Record an ENOTSUP (RemoteError was ENOTSUP) for a request.
700*134e1779SJakub Wojciech Klama        Must be called from the op, this does not happen automatically.
701*134e1779SJakub Wojciech Klama        (It's just an optimization.)
702*134e1779SJakub Wojciech Klama        """
703*134e1779SJakub Wojciech Klama        self._unsup[req_code] = True
704*134e1779SJakub Wojciech Klama
705*134e1779SJakub Wojciech Klama    def connect(self, server=None, port=None):
706*134e1779SJakub Wojciech Klama        """
707*134e1779SJakub Wojciech Klama        Connect to given server/port pair.
708*134e1779SJakub Wojciech Klama
709*134e1779SJakub Wojciech Klama        The server and port are remembered.  If given as None,
710*134e1779SJakub Wojciech Klama        the last remembered values are used.  The initial
711*134e1779SJakub Wojciech Klama        remembered values are from the creation of this client
712*134e1779SJakub Wojciech Klama        instance.
713*134e1779SJakub Wojciech Klama
714*134e1779SJakub Wojciech Klama        New values are only remembered here on a *successful*
715*134e1779SJakub Wojciech Klama        connect, however.
716*134e1779SJakub Wojciech Klama        """
717*134e1779SJakub Wojciech Klama        if server is None:
718*134e1779SJakub Wojciech Klama            server = self._server
719*134e1779SJakub Wojciech Klama            if server is None:
720*134e1779SJakub Wojciech Klama                raise LocalError('connect: no server specified and no default')
721*134e1779SJakub Wojciech Klama        if port is None:
722*134e1779SJakub Wojciech Klama            port = self._port
723*134e1779SJakub Wojciech Klama            if port is None:
724*134e1779SJakub Wojciech Klama                port = STD_P9_PORT
725*134e1779SJakub Wojciech Klama        self.name = None            # wipe out previous name, if any
726*134e1779SJakub Wojciech Klama        super(P9Client, self).connect(server, port)
727*134e1779SJakub Wojciech Klama        maxio = self.get_recommended_maxio()
728*134e1779SJakub Wojciech Klama        self.declare_connected(None, None, maxio)
729*134e1779SJakub Wojciech Klama        self.proto = self.iproto    # revert to initial protocol
730*134e1779SJakub Wojciech Klama        self.have_version = False
731*134e1779SJakub Wojciech Klama        self.rthread = threading.Thread(target=self.read_responses)
732*134e1779SJakub Wojciech Klama        self.rthread.start()
733*134e1779SJakub Wojciech Klama        tag = self.get_tag(for_Tversion=True)
734*134e1779SJakub Wojciech Klama        req = protocol.rrd.Tversion(tag=tag, msize=maxio,
735*134e1779SJakub Wojciech Klama                                    version=self.get_monkey('version'))
736*134e1779SJakub Wojciech Klama        super(P9Client, self).write(self.proto.pack_from(req))
737*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
738*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rversion):
739*134e1779SJakub Wojciech Klama            self.shutdown()
740*134e1779SJakub Wojciech Klama            if isinstance(resp, protocol.rrd.Rerror):
741*134e1779SJakub Wojciech Klama                version = req.version or self.proto.get_version()
742*134e1779SJakub Wojciech Klama                # for python3, we need to convert version to string
743*134e1779SJakub Wojciech Klama                if not isinstance(version, str):
744*134e1779SJakub Wojciech Klama                    version = version.decode('utf-8', 'surrogateescape')
745*134e1779SJakub Wojciech Klama                raise RemoteError(self, 'version ' + version,
746*134e1779SJakub Wojciech Klama                                  resp.errstr, 'Rerror', None)
747*134e1779SJakub Wojciech Klama            self.badresp('version', resp)
748*134e1779SJakub Wojciech Klama        their_maxio = resp.msize
749*134e1779SJakub Wojciech Klama        try:
750*134e1779SJakub Wojciech Klama            self.reduce_maxio(their_maxio)
751*134e1779SJakub Wojciech Klama        except LocalError as err:
752*134e1779SJakub Wojciech Klama            raise LocalError('{0}: sent maxio={1}, they tried {2}: '
753*134e1779SJakub Wojciech Klama                             '{3}'.format(self, maxio, their_maxio,
754*134e1779SJakub Wojciech Klama                                          err.args[0]))
755*134e1779SJakub Wojciech Klama        if resp.version != self.proto.get_version():
756*134e1779SJakub Wojciech Klama            if not self.may_downgrade:
757*134e1779SJakub Wojciech Klama                self.shutdown()
758*134e1779SJakub Wojciech Klama                raise LocalError('{0}: they only support '
759*134e1779SJakub Wojciech Klama                                 'version {1!r}'.format(self, resp.version))
760*134e1779SJakub Wojciech Klama            # raises LocalError if the version is bad
761*134e1779SJakub Wojciech Klama            # (should we wrap it with a connect-to-{0} msg?)
762*134e1779SJakub Wojciech Klama            self.proto = self.proto.downgrade_to(resp.version)
763*134e1779SJakub Wojciech Klama        self._server = server
764*134e1779SJakub Wojciech Klama        self._port = port
765*134e1779SJakub Wojciech Klama        self.have_version = True
766*134e1779SJakub Wojciech Klama
767*134e1779SJakub Wojciech Klama    def attach(self, afid, uname, aname, n_uname):
768*134e1779SJakub Wojciech Klama        """
769*134e1779SJakub Wojciech Klama        Attach.
770*134e1779SJakub Wojciech Klama
771*134e1779SJakub Wojciech Klama        Currently we don't know how to do authentication,
772*134e1779SJakub Wojciech Klama        but we'll pass any provided afid through.
773*134e1779SJakub Wojciech Klama        """
774*134e1779SJakub Wojciech Klama        if afid is None:
775*134e1779SJakub Wojciech Klama            afid = protocol.td.NOFID
776*134e1779SJakub Wojciech Klama        if uname is None:
777*134e1779SJakub Wojciech Klama            uname = ''
778*134e1779SJakub Wojciech Klama        if aname is None:
779*134e1779SJakub Wojciech Klama            aname = ''
780*134e1779SJakub Wojciech Klama        if n_uname is None:
781*134e1779SJakub Wojciech Klama            n_uname = protocol.td.NONUNAME
782*134e1779SJakub Wojciech Klama        tag = self.get_tag()
783*134e1779SJakub Wojciech Klama        fid = self.alloc_fid()
784*134e1779SJakub Wojciech Klama        pkt = self.proto.Tattach(tag=tag, fid=fid, afid=afid,
785*134e1779SJakub Wojciech Klama                                 uname=uname, aname=aname,
786*134e1779SJakub Wojciech Klama                                 n_uname=n_uname)
787*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
788*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
789*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rattach):
790*134e1779SJakub Wojciech Klama            self.retire_fid(fid)
791*134e1779SJakub Wojciech Klama            self.badresp('attach', resp)
792*134e1779SJakub Wojciech Klama        # probably should check resp.qid
793*134e1779SJakub Wojciech Klama        self.rootfid = fid
794*134e1779SJakub Wojciech Klama        self.rootqid = resp.qid
795*134e1779SJakub Wojciech Klama        self.setpath(fid, b'/')
796*134e1779SJakub Wojciech Klama
797*134e1779SJakub Wojciech Klama    def shutdown(self):
798*134e1779SJakub Wojciech Klama        "disconnect from server"
799*134e1779SJakub Wojciech Klama        if self.rootfid is not None:
800*134e1779SJakub Wojciech Klama            self.clunk(self.rootfid, ignore_error=True)
801*134e1779SJakub Wojciech Klama        self.retire_all_tags()
802*134e1779SJakub Wojciech Klama        self.retire_all_fids()
803*134e1779SJakub Wojciech Klama        self.rootfid = None
804*134e1779SJakub Wojciech Klama        self.rootqid = None
805*134e1779SJakub Wojciech Klama        super(P9Client, self).shutdown()
806*134e1779SJakub Wojciech Klama        if self.rthread:
807*134e1779SJakub Wojciech Klama            self.rthread.join()
808*134e1779SJakub Wojciech Klama            self.rthread = None
809*134e1779SJakub Wojciech Klama
810*134e1779SJakub Wojciech Klama    def dupfid(self, fid):
811*134e1779SJakub Wojciech Klama        """
812*134e1779SJakub Wojciech Klama        Copy existing fid to a new fid.
813*134e1779SJakub Wojciech Klama        """
814*134e1779SJakub Wojciech Klama        tag = self.get_tag()
815*134e1779SJakub Wojciech Klama        newfid = self.alloc_fid()
816*134e1779SJakub Wojciech Klama        pkt = self.proto.Twalk(tag=tag, fid=fid, newfid=newfid, nwname=0,
817*134e1779SJakub Wojciech Klama                               wname=[])
818*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
819*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
820*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rwalk):
821*134e1779SJakub Wojciech Klama            self.retire_fid(newfid)
822*134e1779SJakub Wojciech Klama            self.badresp('walk {0}'.format(self.getpathX(fid)), resp)
823*134e1779SJakub Wojciech Klama        # Copy path too
824*134e1779SJakub Wojciech Klama        self.setpath(newfid, fid)
825*134e1779SJakub Wojciech Klama        return newfid
826*134e1779SJakub Wojciech Klama
827*134e1779SJakub Wojciech Klama    def lookup(self, fid, components):
828*134e1779SJakub Wojciech Klama        """
829*134e1779SJakub Wojciech Klama        Do Twalk.  Caller must provide a starting fid, which should
830*134e1779SJakub Wojciech Klama        be rootfid to look up from '/' - we do not do / vs . here.
831*134e1779SJakub Wojciech Klama        Caller must also provide a component-ized path (on purpose,
832*134e1779SJakub Wojciech Klama        so that caller can provide invalid components like '' or '/').
833*134e1779SJakub Wojciech Klama        The components must be byte-strings as well, for the same
834*134e1779SJakub Wojciech Klama        reason.
835*134e1779SJakub Wojciech Klama
836*134e1779SJakub Wojciech Klama        We do allocate the new fid ourselves here, though.
837*134e1779SJakub Wojciech Klama
838*134e1779SJakub Wojciech Klama        There's no logic here to split up long walks (yet?).
839*134e1779SJakub Wojciech Klama        """
840*134e1779SJakub Wojciech Klama        # these are too easy to screw up, so check
841*134e1779SJakub Wojciech Klama        if self.rootfid is None:
842*134e1779SJakub Wojciech Klama            raise LocalError('{0}: not attached'.format(self))
843*134e1779SJakub Wojciech Klama        if (isinstance(components, (str, bytes) or
844*134e1779SJakub Wojciech Klama            not all(isinstance(i, bytes) for i in components))):
845*134e1779SJakub Wojciech Klama            raise LocalError('{0}: lookup: invalid '
846*134e1779SJakub Wojciech Klama                             'components {1!r}'.format(self, components))
847*134e1779SJakub Wojciech Klama        tag = self.get_tag()
848*134e1779SJakub Wojciech Klama        newfid = self.alloc_fid()
849*134e1779SJakub Wojciech Klama        startpath = self.getpath(fid)
850*134e1779SJakub Wojciech Klama        pkt = self.proto.Twalk(tag=tag, fid=fid, newfid=newfid,
851*134e1779SJakub Wojciech Klama                               nwname=len(components), wname=components)
852*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
853*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
854*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rwalk):
855*134e1779SJakub Wojciech Klama            self.retire_fid(newfid)
856*134e1779SJakub Wojciech Klama            self.badresp('walk {0} in '
857*134e1779SJakub Wojciech Klama                         '{1}'.format(components, self.getpathX(fid)),
858*134e1779SJakub Wojciech Klama                         resp)
859*134e1779SJakub Wojciech Klama        # Just because we got Rwalk does not mean we got ALL the
860*134e1779SJakub Wojciech Klama        # way down the path.  Raise OSError(ENOENT) if we're short.
861*134e1779SJakub Wojciech Klama        if resp.nwqid > len(components):
862*134e1779SJakub Wojciech Klama            # ??? this should be impossible. Local error?  Remote error?
863*134e1779SJakub Wojciech Klama            # OS Error?
864*134e1779SJakub Wojciech Klama            self.clunk(newfid, ignore_error=True)
865*134e1779SJakub Wojciech Klama            raise LocalError('{0}: walk {1} in {2} returned {3} '
866*134e1779SJakub Wojciech Klama                             'items'.format(self, components,
867*134e1779SJakub Wojciech Klama                                            self.getpathX(fid), resp.nwqid))
868*134e1779SJakub Wojciech Klama        if resp.nwqid < len(components):
869*134e1779SJakub Wojciech Klama            self.clunk(newfid, ignore_error=True)
870*134e1779SJakub Wojciech Klama            # Looking up a/b/c and got just a/b, c is what's missing.
871*134e1779SJakub Wojciech Klama            # Looking up a/b/c and got just a, b is what's missing.
872*134e1779SJakub Wojciech Klama            missing = components[resp.nwqid]
873*134e1779SJakub Wojciech Klama            within = _pathcat(startpath, b'/'.join(components[:resp.nwqid]))
874*134e1779SJakub Wojciech Klama            raise OSError(errno.ENOENT,
875*134e1779SJakub Wojciech Klama                          '{0}: {1} in {2}'.format(os.strerror(errno.ENOENT),
876*134e1779SJakub Wojciech Klama                                                   missing, within))
877*134e1779SJakub Wojciech Klama        self.setpath(newfid, _pathcat(startpath, b'/'.join(components)))
878*134e1779SJakub Wojciech Klama        return newfid, resp.wqid
879*134e1779SJakub Wojciech Klama
880*134e1779SJakub Wojciech Klama    def lookup_last(self, fid, components):
881*134e1779SJakub Wojciech Klama        """
882*134e1779SJakub Wojciech Klama        Like lookup, but return only the last component's qid.
883*134e1779SJakub Wojciech Klama        As a special case, if components is an empty list, we
884*134e1779SJakub Wojciech Klama        handle that.
885*134e1779SJakub Wojciech Klama        """
886*134e1779SJakub Wojciech Klama        rfid, wqid = self.lookup(fid, components)
887*134e1779SJakub Wojciech Klama        if len(wqid):
888*134e1779SJakub Wojciech Klama            return rfid, wqid[-1]
889*134e1779SJakub Wojciech Klama        if fid == self.rootfid:         # usually true, if we get here at all
890*134e1779SJakub Wojciech Klama            return rfid, self.rootqid
891*134e1779SJakub Wojciech Klama        tag = self.get_tag()
892*134e1779SJakub Wojciech Klama        pkt = self.proto.Tstat(tag=tag, fid=rfid)
893*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
894*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
895*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rstat):
896*134e1779SJakub Wojciech Klama            self.badresp('stat {0}'.format(self.getpathX(fid)), resp)
897*134e1779SJakub Wojciech Klama        statval = self.proto.unpack_wirestat(resp.data)
898*134e1779SJakub Wojciech Klama        return rfid, statval.qid
899*134e1779SJakub Wojciech Klama
900*134e1779SJakub Wojciech Klama    def clunk(self, fid, ignore_error=False):
901*134e1779SJakub Wojciech Klama        "issue clunk(fid)"
902*134e1779SJakub Wojciech Klama        tag = self.get_tag()
903*134e1779SJakub Wojciech Klama        pkt = self.proto.Tclunk(tag=tag, fid=fid)
904*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
905*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
906*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rclunk):
907*134e1779SJakub Wojciech Klama            if ignore_error:
908*134e1779SJakub Wojciech Klama                return
909*134e1779SJakub Wojciech Klama            self.badresp('clunk {0}'.format(self.getpathX(fid)), resp)
910*134e1779SJakub Wojciech Klama        self.retire_fid(fid)
911*134e1779SJakub Wojciech Klama
912*134e1779SJakub Wojciech Klama    def remove(self, fid, ignore_error=False):
913*134e1779SJakub Wojciech Klama        "issue remove (old style), which also clunks fid"
914*134e1779SJakub Wojciech Klama        tag = self.get_tag()
915*134e1779SJakub Wojciech Klama        pkt = self.proto.Tremove(tag=tag, fid=fid)
916*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
917*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
918*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rremove):
919*134e1779SJakub Wojciech Klama            if ignore_error:
920*134e1779SJakub Wojciech Klama                # remove failed: still need to clunk the fid
921*134e1779SJakub Wojciech Klama                self.clunk(fid, True)
922*134e1779SJakub Wojciech Klama                return
923*134e1779SJakub Wojciech Klama            self.badresp('remove {0}'.format(self.getpathX(fid)), resp)
924*134e1779SJakub Wojciech Klama        self.retire_fid(fid)
925*134e1779SJakub Wojciech Klama
926*134e1779SJakub Wojciech Klama    def create(self, fid, name, perm, mode, filetype=None, extension=b''):
927*134e1779SJakub Wojciech Klama        """
928*134e1779SJakub Wojciech Klama        Issue create op (note that this may be mkdir, symlink, etc).
929*134e1779SJakub Wojciech Klama        fid is the directory in which the create happens, and for
930*134e1779SJakub Wojciech Klama        regular files, it becomes, on success, a fid referring to
931*134e1779SJakub Wojciech Klama        the now-open file.  perm is, e.g., 0644, 0755, etc.,
932*134e1779SJakub Wojciech Klama        optionally with additional high bits.  mode is a mode
933*134e1779SJakub Wojciech Klama        byte (e.g., protocol.td.ORDWR, or OWRONLY|OTRUNC, etc.).
934*134e1779SJakub Wojciech Klama
935*134e1779SJakub Wojciech Klama        As a service to callers, we take two optional arguments
936*134e1779SJakub Wojciech Klama        specifying the file type ('dir', 'symlink', 'device',
937*134e1779SJakub Wojciech Klama        'fifo', or 'socket') and additional info if needed.
938*134e1779SJakub Wojciech Klama        The additional info for a symlink is the target of the
939*134e1779SJakub Wojciech Klama        link (a byte string), and the additional info for a device
940*134e1779SJakub Wojciech Klama        is a byte string with "b <major> <minor>" or "c <major> <minor>".
941*134e1779SJakub Wojciech Klama
942*134e1779SJakub Wojciech Klama        Otherwise, callers can leave filetype=None and encode the bits
943*134e1779SJakub Wojciech Klama        into the mode (caller must still provide extension if needed).
944*134e1779SJakub Wojciech Klama
945*134e1779SJakub Wojciech Klama        We do NOT check whether the extension matches extra DM bits,
946*134e1779SJakub Wojciech Klama        or that there's only one DM bit set, or whatever, since this
947*134e1779SJakub Wojciech Klama        is a testing setup.
948*134e1779SJakub Wojciech Klama        """
949*134e1779SJakub Wojciech Klama        tag = self.get_tag()
950*134e1779SJakub Wojciech Klama        if filetype is not None:
951*134e1779SJakub Wojciech Klama            perm |= {
952*134e1779SJakub Wojciech Klama                'dir': protocol.td.DMDIR,
953*134e1779SJakub Wojciech Klama                'symlink': protocol.td.DMSYMLINK,
954*134e1779SJakub Wojciech Klama                'device': protocol.td.DMDEVICE,
955*134e1779SJakub Wojciech Klama                'fifo': protocol.td.DMNAMEDPIPE,
956*134e1779SJakub Wojciech Klama                'socket': protocol.td.DMSOCKET,
957*134e1779SJakub Wojciech Klama            }[filetype]
958*134e1779SJakub Wojciech Klama        pkt = self.proto.Tcreate(tag=tag, fid=fid, name=name,
959*134e1779SJakub Wojciech Klama            perm=perm, mode=mode, extension=extension)
960*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
961*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
962*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rcreate):
963*134e1779SJakub Wojciech Klama            self.badresp('create {0} in {1}'.format(name, self.getpathX(fid)),
964*134e1779SJakub Wojciech Klama                         resp)
965*134e1779SJakub Wojciech Klama        if resp.qid.type == protocol.td.QTFILE:
966*134e1779SJakub Wojciech Klama            # Creating a regular file opens the file,
967*134e1779SJakub Wojciech Klama            # thus changing the fid's path.
968*134e1779SJakub Wojciech Klama            self.setpath(fid, _pathcat(self.getpath(fid), name))
969*134e1779SJakub Wojciech Klama        return resp.qid, resp.iounit
970*134e1779SJakub Wojciech Klama
971*134e1779SJakub Wojciech Klama    def open(self, fid, mode):
972*134e1779SJakub Wojciech Klama        "use Topen to open file or directory fid (mode is 1 byte)"
973*134e1779SJakub Wojciech Klama        tag = self.get_tag()
974*134e1779SJakub Wojciech Klama        pkt = self.proto.Topen(tag=tag, fid=fid, mode=mode)
975*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
976*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
977*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Ropen):
978*134e1779SJakub Wojciech Klama            self.badresp('open {0}'.format(self.getpathX(fid)), resp)
979*134e1779SJakub Wojciech Klama        return resp.qid, resp.iounit
980*134e1779SJakub Wojciech Klama
981*134e1779SJakub Wojciech Klama    def lopen(self, fid, flags):
982*134e1779SJakub Wojciech Klama        "use Tlopen to open file or directory fid (flags from L_O_*)"
983*134e1779SJakub Wojciech Klama        tag = self.get_tag()
984*134e1779SJakub Wojciech Klama        pkt = self.proto.Tlopen(tag=tag, fid=fid, flags=flags)
985*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
986*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
987*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rlopen):
988*134e1779SJakub Wojciech Klama            self.badresp('lopen {0}'.format(self.getpathX(fid)), resp)
989*134e1779SJakub Wojciech Klama        return resp.qid, resp.iounit
990*134e1779SJakub Wojciech Klama
991*134e1779SJakub Wojciech Klama    def read(self, fid, offset, count):
992*134e1779SJakub Wojciech Klama        "read (up to) count bytes from offset, given open fid"
993*134e1779SJakub Wojciech Klama        tag = self.get_tag()
994*134e1779SJakub Wojciech Klama        pkt = self.proto.Tread(tag=tag, fid=fid, offset=offset, count=count)
995*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
996*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
997*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rread):
998*134e1779SJakub Wojciech Klama            self.badresp('read {0} bytes at offset {1} in '
999*134e1779SJakub Wojciech Klama                         '{2}'.format(count, offset, self.getpathX(fid)),
1000*134e1779SJakub Wojciech Klama                         resp)
1001*134e1779SJakub Wojciech Klama        return resp.data
1002*134e1779SJakub Wojciech Klama
1003*134e1779SJakub Wojciech Klama    def write(self, fid, offset, data):
1004*134e1779SJakub Wojciech Klama        "write (up to) count bytes to offset, given open fid"
1005*134e1779SJakub Wojciech Klama        tag = self.get_tag()
1006*134e1779SJakub Wojciech Klama        pkt = self.proto.Twrite(tag=tag, fid=fid, offset=offset,
1007*134e1779SJakub Wojciech Klama                                count=len(data), data=data)
1008*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
1009*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
1010*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rwrite):
1011*134e1779SJakub Wojciech Klama            self.badresp('write {0} bytes at offset {1} in '
1012*134e1779SJakub Wojciech Klama                         '{2}'.format(len(data), offset, self.getpathX(fid)),
1013*134e1779SJakub Wojciech Klama                         resp)
1014*134e1779SJakub Wojciech Klama        return resp.count
1015*134e1779SJakub Wojciech Klama
1016*134e1779SJakub Wojciech Klama    # Caller may
1017*134e1779SJakub Wojciech Klama    #  - pass an actual stat object, or
1018*134e1779SJakub Wojciech Klama    #  - pass in all the individual to-set items by keyword, or
1019*134e1779SJakub Wojciech Klama    #  - mix and match a bit: get an existing stat, then use
1020*134e1779SJakub Wojciech Klama    #    keywords to override fields.
1021*134e1779SJakub Wojciech Klama    # We convert "None"s to the internal "do not change" values,
1022*134e1779SJakub Wojciech Klama    # and for diagnostic purposes, can turn "do not change" back
1023*134e1779SJakub Wojciech Klama    # to None at the end, too.
1024*134e1779SJakub Wojciech Klama    def wstat(self, fid, statobj=None, **kwargs):
1025*134e1779SJakub Wojciech Klama        if statobj is None:
1026*134e1779SJakub Wojciech Klama            statobj = protocol.td.stat()
1027*134e1779SJakub Wojciech Klama        else:
1028*134e1779SJakub Wojciech Klama            statobj = statobj._copy()
1029*134e1779SJakub Wojciech Klama        # Fields in stat that you can't send as a wstat: the
1030*134e1779SJakub Wojciech Klama        # type and qid are informative.  Similarly, the
1031*134e1779SJakub Wojciech Klama        # 'extension' is an input when creating a file but
1032*134e1779SJakub Wojciech Klama        # read-only when stat-ing.
1033*134e1779SJakub Wojciech Klama        #
1034*134e1779SJakub Wojciech Klama        # It's not clear what it means to set dev, but we'll leave
1035*134e1779SJakub Wojciech Klama        # it in as an optional parameter here.  fs/backend.c just
1036*134e1779SJakub Wojciech Klama        # errors out on an attempt to change it.
1037*134e1779SJakub Wojciech Klama        if self.proto == protocol.plain:
1038*134e1779SJakub Wojciech Klama            forbid = ('type', 'qid', 'extension',
1039*134e1779SJakub Wojciech Klama                      'n_uid', 'n_gid', 'n_muid')
1040*134e1779SJakub Wojciech Klama        else:
1041*134e1779SJakub Wojciech Klama            forbid = ('type', 'qid', 'extension')
1042*134e1779SJakub Wojciech Klama        nochange = {
1043*134e1779SJakub Wojciech Klama            'type': 0,
1044*134e1779SJakub Wojciech Klama            'qid': protocol.td.qid(0, 0, 0),
1045*134e1779SJakub Wojciech Klama            'dev': 2**32 - 1,
1046*134e1779SJakub Wojciech Klama            'mode': 2**32 - 1,
1047*134e1779SJakub Wojciech Klama            'atime': 2**32 - 1,
1048*134e1779SJakub Wojciech Klama            'mtime': 2**32 - 1,
1049*134e1779SJakub Wojciech Klama            'length': 2**64 - 1,
1050*134e1779SJakub Wojciech Klama            'name': b'',
1051*134e1779SJakub Wojciech Klama            'uid': b'',
1052*134e1779SJakub Wojciech Klama            'gid': b'',
1053*134e1779SJakub Wojciech Klama            'muid': b'',
1054*134e1779SJakub Wojciech Klama            'extension': b'',
1055*134e1779SJakub Wojciech Klama            'n_uid': 2**32 - 1,
1056*134e1779SJakub Wojciech Klama            'n_gid': 2**32 - 1,
1057*134e1779SJakub Wojciech Klama            'n_muid': 2**32 - 1,
1058*134e1779SJakub Wojciech Klama        }
1059*134e1779SJakub Wojciech Klama        for field in statobj._fields:
1060*134e1779SJakub Wojciech Klama            if field in kwargs:
1061*134e1779SJakub Wojciech Klama                if field in forbid:
1062*134e1779SJakub Wojciech Klama                    raise ValueError('cannot wstat a stat.{0}'.format(field))
1063*134e1779SJakub Wojciech Klama                statobj[field] = kwargs.pop(field)
1064*134e1779SJakub Wojciech Klama            else:
1065*134e1779SJakub Wojciech Klama                if field in forbid or statobj[field] is None:
1066*134e1779SJakub Wojciech Klama                    statobj[field] = nochange[field]
1067*134e1779SJakub Wojciech Klama        if kwargs:
1068*134e1779SJakub Wojciech Klama            raise TypeError('wstat() got an unexpected keyword argument '
1069*134e1779SJakub Wojciech Klama                            '{0!r}'.format(kwargs.popitem()))
1070*134e1779SJakub Wojciech Klama
1071*134e1779SJakub Wojciech Klama        data = self.proto.pack_wirestat(statobj)
1072*134e1779SJakub Wojciech Klama        tag = self.get_tag()
1073*134e1779SJakub Wojciech Klama        pkt = self.proto.Twstat(tag=tag, fid=fid, data=data)
1074*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
1075*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
1076*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rwstat):
1077*134e1779SJakub Wojciech Klama            # For error viewing, switch all the do-not-change
1078*134e1779SJakub Wojciech Klama            # and can't-change fields to None.
1079*134e1779SJakub Wojciech Klama            statobj.qid = None
1080*134e1779SJakub Wojciech Klama            for field in statobj._fields:
1081*134e1779SJakub Wojciech Klama                if field in forbid:
1082*134e1779SJakub Wojciech Klama                    statobj[field] = None
1083*134e1779SJakub Wojciech Klama                elif field in nochange and statobj[field] == nochange[field]:
1084*134e1779SJakub Wojciech Klama                    statobj[field] = None
1085*134e1779SJakub Wojciech Klama            self.badresp('wstat {0}={1}'.format(self.getpathX(fid), statobj),
1086*134e1779SJakub Wojciech Klama                         resp)
1087*134e1779SJakub Wojciech Klama        # wstat worked - change path names if needed
1088*134e1779SJakub Wojciech Klama        if statobj.name != b'':
1089*134e1779SJakub Wojciech Klama            self.did_rename(fid, statobj.name)
1090*134e1779SJakub Wojciech Klama
1091*134e1779SJakub Wojciech Klama    def readdir(self, fid, offset, count):
1092*134e1779SJakub Wojciech Klama        "read (up to) count bytes of dir data from offset, given open fid"
1093*134e1779SJakub Wojciech Klama        tag = self.get_tag()
1094*134e1779SJakub Wojciech Klama        pkt = self.proto.Treaddir(tag=tag, fid=fid, offset=offset, count=count)
1095*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
1096*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
1097*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rreaddir):
1098*134e1779SJakub Wojciech Klama            self.badresp('readdir {0} bytes at offset {1} in '
1099*134e1779SJakub Wojciech Klama                         '{2}'.format(count, offset, self.getpathX(fid)),
1100*134e1779SJakub Wojciech Klama                         resp)
1101*134e1779SJakub Wojciech Klama        return resp.data
1102*134e1779SJakub Wojciech Klama
1103*134e1779SJakub Wojciech Klama    def rename(self, fid, dfid, name):
1104*134e1779SJakub Wojciech Klama        "invoke Trename: rename file <fid> to <dfid>/name"
1105*134e1779SJakub Wojciech Klama        tag = self.get_tag()
1106*134e1779SJakub Wojciech Klama        pkt = self.proto.Trename(tag=tag, fid=fid, dfid=dfid, name=name)
1107*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
1108*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
1109*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rrename):
1110*134e1779SJakub Wojciech Klama            self.badresp('rename {0} to {2} in '
1111*134e1779SJakub Wojciech Klama                         '{1}'.format(self.getpathX(fid),
1112*134e1779SJakub Wojciech Klama                                      self.getpathX(dfid), name),
1113*134e1779SJakub Wojciech Klama                         resp)
1114*134e1779SJakub Wojciech Klama        self.did_rename(fid, name, self.getpath(dfid))
1115*134e1779SJakub Wojciech Klama
1116*134e1779SJakub Wojciech Klama    def renameat(self, olddirfid, oldname, newdirfid, newname):
1117*134e1779SJakub Wojciech Klama        "invoke Trenameat: rename <olddirfid>/oldname to <newdirfid>/newname"
1118*134e1779SJakub Wojciech Klama        tag = self.get_tag()
1119*134e1779SJakub Wojciech Klama        pkt = self.proto.Trenameat(tag=tag,
1120*134e1779SJakub Wojciech Klama                                   olddirfid=olddirfid, oldname=oldname,
1121*134e1779SJakub Wojciech Klama                                   newdirfid=newdirfid, newname=newname)
1122*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
1123*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
1124*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rrenameat):
1125*134e1779SJakub Wojciech Klama            self.badresp('rename {1} in {0} to {3} in '
1126*134e1779SJakub Wojciech Klama                         '{2}'.format(oldname, self.getpathX(olddirfid),
1127*134e1779SJakub Wojciech Klama                                      newname, self.getpathX(newdirdfid)),
1128*134e1779SJakub Wojciech Klama                         resp)
1129*134e1779SJakub Wojciech Klama        # There's no renamed *fid*, just a renamed file!  So no
1130*134e1779SJakub Wojciech Klama        # call to self.did_rename().
1131*134e1779SJakub Wojciech Klama
1132*134e1779SJakub Wojciech Klama    def unlinkat(self, dirfd, name, flags):
1133*134e1779SJakub Wojciech Klama        "invoke Tunlinkat - flags should be 0 or protocol.td.AT_REMOVEDIR"
1134*134e1779SJakub Wojciech Klama        tag = self.get_tag()
1135*134e1779SJakub Wojciech Klama        pkt = self.proto.Tunlinkat(tag=tag, dirfd=dirfd,
1136*134e1779SJakub Wojciech Klama                                   name=name, flags=flags)
1137*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
1138*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
1139*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Runlinkat):
1140*134e1779SJakub Wojciech Klama            self.badresp('unlinkat {0} in '
1141*134e1779SJakub Wojciech Klama                         '{1}'.format(name, self.getpathX(dirfd)), resp)
1142*134e1779SJakub Wojciech Klama
1143*134e1779SJakub Wojciech Klama    def decode_stat_objects(self, bstring, noerror=False):
1144*134e1779SJakub Wojciech Klama        """
1145*134e1779SJakub Wojciech Klama        Read on a directory returns an array of stat objects.
1146*134e1779SJakub Wojciech Klama        Note that for .u these encode extra data.
1147*134e1779SJakub Wojciech Klama
1148*134e1779SJakub Wojciech Klama        It's possible for this to produce a SequenceError, if
1149*134e1779SJakub Wojciech Klama        the data are incorrect, unless you pass noerror=True.
1150*134e1779SJakub Wojciech Klama        """
1151*134e1779SJakub Wojciech Klama        objlist = []
1152*134e1779SJakub Wojciech Klama        offset = 0
1153*134e1779SJakub Wojciech Klama        while offset < len(bstring):
1154*134e1779SJakub Wojciech Klama            obj, offset = self.proto.unpack_wirestat(bstring, offset, noerror)
1155*134e1779SJakub Wojciech Klama            objlist.append(obj)
1156*134e1779SJakub Wojciech Klama        return objlist
1157*134e1779SJakub Wojciech Klama
1158*134e1779SJakub Wojciech Klama    def decode_readdir_dirents(self, bstring, noerror=False):
1159*134e1779SJakub Wojciech Klama        """
1160*134e1779SJakub Wojciech Klama        Readdir on a directory returns an array of dirent objects.
1161*134e1779SJakub Wojciech Klama
1162*134e1779SJakub Wojciech Klama        It's possible for this to produce a SequenceError, if
1163*134e1779SJakub Wojciech Klama        the data are incorrect, unless you pass noerror=True.
1164*134e1779SJakub Wojciech Klama        """
1165*134e1779SJakub Wojciech Klama        objlist = []
1166*134e1779SJakub Wojciech Klama        offset = 0
1167*134e1779SJakub Wojciech Klama        while offset < len(bstring):
1168*134e1779SJakub Wojciech Klama            obj, offset = self.proto.unpack_dirent(bstring, offset, noerror)
1169*134e1779SJakub Wojciech Klama            objlist.append(obj)
1170*134e1779SJakub Wojciech Klama        return objlist
1171*134e1779SJakub Wojciech Klama
1172*134e1779SJakub Wojciech Klama    def lcreate(self, fid, name, lflags, mode, gid):
1173*134e1779SJakub Wojciech Klama        "issue lcreate (.L)"
1174*134e1779SJakub Wojciech Klama        tag = self.get_tag()
1175*134e1779SJakub Wojciech Klama        pkt = self.proto.Tlcreate(tag=tag, fid=fid, name=name,
1176*134e1779SJakub Wojciech Klama                                  flags=lflags, mode=mode, gid=gid)
1177*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
1178*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
1179*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rlcreate):
1180*134e1779SJakub Wojciech Klama            self.badresp('create {0} in '
1181*134e1779SJakub Wojciech Klama                         '{1}'.format(name, self.getpathX(fid)), resp)
1182*134e1779SJakub Wojciech Klama        # Creating a file opens the file,
1183*134e1779SJakub Wojciech Klama        # thus changing the fid's path.
1184*134e1779SJakub Wojciech Klama        self.setpath(fid, _pathcat(self.getpath(fid), name))
1185*134e1779SJakub Wojciech Klama        return resp.qid, resp.iounit
1186*134e1779SJakub Wojciech Klama
1187*134e1779SJakub Wojciech Klama    def mkdir(self, dfid, name, mode, gid):
1188*134e1779SJakub Wojciech Klama        "issue mkdir (.L)"
1189*134e1779SJakub Wojciech Klama        tag = self.get_tag()
1190*134e1779SJakub Wojciech Klama        pkt = self.proto.Tmkdir(tag=tag, dfid=dfid, name=name,
1191*134e1779SJakub Wojciech Klama                                mode=mode, gid=gid)
1192*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
1193*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
1194*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rmkdir):
1195*134e1779SJakub Wojciech Klama            self.badresp('mkdir {0} in '
1196*134e1779SJakub Wojciech Klama                         '{1}'.format(name, self.getpathX(dfid)), resp)
1197*134e1779SJakub Wojciech Klama        return resp.qid
1198*134e1779SJakub Wojciech Klama
1199*134e1779SJakub Wojciech Klama    # We don't call this getattr(), for the obvious reason.
1200*134e1779SJakub Wojciech Klama    def Tgetattr(self, fid, request_mask=protocol.td.GETATTR_ALL):
1201*134e1779SJakub Wojciech Klama        "issue Tgetattr.L - get what you ask for, or everything by default"
1202*134e1779SJakub Wojciech Klama        tag = self.get_tag()
1203*134e1779SJakub Wojciech Klama        pkt = self.proto.Tgetattr(tag=tag, fid=fid, request_mask=request_mask)
1204*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
1205*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
1206*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rgetattr):
1207*134e1779SJakub Wojciech Klama            self.badresp('Tgetattr {0} of '
1208*134e1779SJakub Wojciech Klama                         '{1}'.format(request_mask, self.getpathX(fid)), resp)
1209*134e1779SJakub Wojciech Klama        attrs = Fileattrs()
1210*134e1779SJakub Wojciech Klama        # Handle the simplest valid-bit tests:
1211*134e1779SJakub Wojciech Klama        for name in ('mode', 'nlink', 'uid', 'gid', 'rdev',
1212*134e1779SJakub Wojciech Klama                     'size', 'blocks', 'gen', 'data_version'):
1213*134e1779SJakub Wojciech Klama            bit = getattr(protocol.td, 'GETATTR_' + name.upper())
1214*134e1779SJakub Wojciech Klama            if resp.valid & bit:
1215*134e1779SJakub Wojciech Klama                attrs[name] = resp[name]
1216*134e1779SJakub Wojciech Klama        # Handle the timestamps, which are timespec pairs
1217*134e1779SJakub Wojciech Klama        for name in ('atime', 'mtime', 'ctime', 'btime'):
1218*134e1779SJakub Wojciech Klama            bit = getattr(protocol.td, 'GETATTR_' + name.upper())
1219*134e1779SJakub Wojciech Klama            if resp.valid & bit:
1220*134e1779SJakub Wojciech Klama                attrs[name] = Timespec(sec=resp[name + '_sec'],
1221*134e1779SJakub Wojciech Klama                                       nsec=resp[name + '_nsec'])
1222*134e1779SJakub Wojciech Klama        # There is no control bit for blksize; qemu and Linux always
1223*134e1779SJakub Wojciech Klama        # provide one.
1224*134e1779SJakub Wojciech Klama        attrs.blksize = resp.blksize
1225*134e1779SJakub Wojciech Klama        # Handle ino, which comes out of qid.path
1226*134e1779SJakub Wojciech Klama        if resp.valid & protocol.td.GETATTR_INO:
1227*134e1779SJakub Wojciech Klama            attrs.ino = resp.qid.path
1228*134e1779SJakub Wojciech Klama        return attrs
1229*134e1779SJakub Wojciech Klama
1230*134e1779SJakub Wojciech Klama    # We don't call this setattr(), for the obvious reason.
1231*134e1779SJakub Wojciech Klama    # See wstat for usage.  Note that time fields can be set
1232*134e1779SJakub Wojciech Klama    # with either second or nanosecond resolutions, and some
1233*134e1779SJakub Wojciech Klama    # can be set without supplying an actual timestamp, so
1234*134e1779SJakub Wojciech Klama    # this is all pretty ad-hoc.
1235*134e1779SJakub Wojciech Klama    #
1236*134e1779SJakub Wojciech Klama    # There's also one keyword-only argument, ctime=<anything>,
1237*134e1779SJakub Wojciech Klama    # which means "set SETATTR_CTIME".  This has the same effect
1238*134e1779SJakub Wojciech Klama    # as supplying valid=protocol.td.SETATTR_CTIME.
1239*134e1779SJakub Wojciech Klama    def Tsetattr(self, fid, valid=0, attrs=None, **kwargs):
1240*134e1779SJakub Wojciech Klama        if attrs is None:
1241*134e1779SJakub Wojciech Klama            attrs = Fileattrs()
1242*134e1779SJakub Wojciech Klama        else:
1243*134e1779SJakub Wojciech Klama            attrs = attrs._copy()
1244*134e1779SJakub Wojciech Klama
1245*134e1779SJakub Wojciech Klama        # Start with an empty (all-zero) Tsetattr instance.  We
1246*134e1779SJakub Wojciech Klama        # don't really need to zero out tag and fid, but it doesn't
1247*134e1779SJakub Wojciech Klama        # hurt.  Note that if caller says, e.g., valid=SETATTR_SIZE
1248*134e1779SJakub Wojciech Klama        # but does not supply an incoming size (via "attrs" or a size=
1249*134e1779SJakub Wojciech Klama        # argument), we'll ask to set that field to 0.
1250*134e1779SJakub Wojciech Klama        attrobj = protocol.rrd.Tsetattr()
1251*134e1779SJakub Wojciech Klama        for field in attrobj._fields:
1252*134e1779SJakub Wojciech Klama            attrobj[field] = 0
1253*134e1779SJakub Wojciech Klama
1254*134e1779SJakub Wojciech Klama        # In this case, forbid means "only as kwargs": these values
1255*134e1779SJakub Wojciech Klama        # in an incoming attrs object are merely ignored.
1256*134e1779SJakub Wojciech Klama        forbid = ('ino', 'nlink', 'rdev', 'blksize', 'blocks', 'btime',
1257*134e1779SJakub Wojciech Klama                  'gen', 'data_version')
1258*134e1779SJakub Wojciech Klama        for field in attrs._fields:
1259*134e1779SJakub Wojciech Klama            if field in kwargs:
1260*134e1779SJakub Wojciech Klama                if field in forbid:
1261*134e1779SJakub Wojciech Klama                    raise ValueError('cannot Tsetattr {0}'.format(field))
1262*134e1779SJakub Wojciech Klama                attrs[field] = kwargs.pop(field)
1263*134e1779SJakub Wojciech Klama            elif attrs[field] is None:
1264*134e1779SJakub Wojciech Klama                continue
1265*134e1779SJakub Wojciech Klama            # OK, we're setting this attribute.  Many are just
1266*134e1779SJakub Wojciech Klama            # numeric - if that's the case, we're good, set the
1267*134e1779SJakub Wojciech Klama            # field and the appropriate bit.
1268*134e1779SJakub Wojciech Klama            bitname = 'SETATTR_' + field.upper()
1269*134e1779SJakub Wojciech Klama            bit = getattr(protocol.td, bitname)
1270*134e1779SJakub Wojciech Klama            if field in ('mode', 'uid', 'gid', 'size'):
1271*134e1779SJakub Wojciech Klama                valid |= bit
1272*134e1779SJakub Wojciech Klama                attrobj[field] = attrs[field]
1273*134e1779SJakub Wojciech Klama                continue
1274*134e1779SJakub Wojciech Klama            # Timestamps are special:  The value may be given as
1275*134e1779SJakub Wojciech Klama            # an integer (seconds), or as a float (we convert to
1276*134e1779SJakub Wojciech Klama            # (we convert to sec+nsec), or as a timespec (sec+nsec).
1277*134e1779SJakub Wojciech Klama            # If specified as 0, we mean "we are not providing the
1278*134e1779SJakub Wojciech Klama            # actual time, use the server's time."
1279*134e1779SJakub Wojciech Klama            #
1280*134e1779SJakub Wojciech Klama            # The ctime field's value, if any, is *ignored*.
1281*134e1779SJakub Wojciech Klama            if field in ('atime', 'mtime'):
1282*134e1779SJakub Wojciech Klama                value = attrs[field]
1283*134e1779SJakub Wojciech Klama                if hasattr(value, '__len__'):
1284*134e1779SJakub Wojciech Klama                    if len(value) != 2:
1285*134e1779SJakub Wojciech Klama                        raise ValueError('invalid {0}={1!r}'.format(field,
1286*134e1779SJakub Wojciech Klama                                                                    value))
1287*134e1779SJakub Wojciech Klama                    sec = value[0]
1288*134e1779SJakub Wojciech Klama                    nsec = value[1]
1289*134e1779SJakub Wojciech Klama                else:
1290*134e1779SJakub Wojciech Klama                    sec = value
1291*134e1779SJakub Wojciech Klama                    if isinstance(sec, float):
1292*134e1779SJakub Wojciech Klama                        nsec, sec = math.modf(sec)
1293*134e1779SJakub Wojciech Klama                        nsec = int(round(nsec * 1000000000))
1294*134e1779SJakub Wojciech Klama                    else:
1295*134e1779SJakub Wojciech Klama                        nsec = 0
1296*134e1779SJakub Wojciech Klama                valid |= bit
1297*134e1779SJakub Wojciech Klama                attrobj[field + '_sec'] = sec
1298*134e1779SJakub Wojciech Klama                attrobj[field + '_nsec'] = nsec
1299*134e1779SJakub Wojciech Klama                if sec != 0 or nsec != 0:
1300*134e1779SJakub Wojciech Klama                    # Add SETATTR_ATIME_SET or SETATTR_MTIME_SET
1301*134e1779SJakub Wojciech Klama                    # as appropriate, to tell the server to *this
1302*134e1779SJakub Wojciech Klama                    # specific* time, instead of just "server now".
1303*134e1779SJakub Wojciech Klama                    bit = getattr(protocol.td, bitname + '_SET')
1304*134e1779SJakub Wojciech Klama                    valid |= bit
1305*134e1779SJakub Wojciech Klama        if 'ctime' in kwargs:
1306*134e1779SJakub Wojciech Klama            kwargs.pop('ctime')
1307*134e1779SJakub Wojciech Klama            valid |= protocol.td.SETATTR_CTIME
1308*134e1779SJakub Wojciech Klama        if kwargs:
1309*134e1779SJakub Wojciech Klama            raise TypeError('Tsetattr() got an unexpected keyword argument '
1310*134e1779SJakub Wojciech Klama                            '{0!r}'.format(kwargs.popitem()))
1311*134e1779SJakub Wojciech Klama
1312*134e1779SJakub Wojciech Klama        tag = self.get_tag()
1313*134e1779SJakub Wojciech Klama        attrobj.valid = valid
1314*134e1779SJakub Wojciech Klama        attrobj.tag = tag
1315*134e1779SJakub Wojciech Klama        attrobj.fid = fid
1316*134e1779SJakub Wojciech Klama        pkt = self.proto.pack(attrobj)
1317*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
1318*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
1319*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rsetattr):
1320*134e1779SJakub Wojciech Klama            self.badresp('Tsetattr {0} {1} of '
1321*134e1779SJakub Wojciech Klama                         '{2}'.format(valid, attrs, self.getpathX(fid)), resp)
1322*134e1779SJakub Wojciech Klama
1323*134e1779SJakub Wojciech Klama    def xattrwalk(self, fid, name=None):
1324*134e1779SJakub Wojciech Klama        "walk one name or all names: caller should read() the returned fid"
1325*134e1779SJakub Wojciech Klama        tag = self.get_tag()
1326*134e1779SJakub Wojciech Klama        newfid = self.alloc_fid()
1327*134e1779SJakub Wojciech Klama        pkt = self.proto.Txattrwalk(tag=tag, fid=fid, newfid=newfid,
1328*134e1779SJakub Wojciech Klama                                    name=name or '')
1329*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
1330*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
1331*134e1779SJakub Wojciech Klama        if not isinstance(resp, protocol.rrd.Rxattrwalk):
1332*134e1779SJakub Wojciech Klama            self.retire_fid(newfid)
1333*134e1779SJakub Wojciech Klama            self.badresp('Txattrwalk {0} of '
1334*134e1779SJakub Wojciech Klama                         '{1}'.format(name, self.getpathX(fid)), resp)
1335*134e1779SJakub Wojciech Klama        if name:
1336*134e1779SJakub Wojciech Klama            self.setpath(newfid, 'xattr:' + name)
1337*134e1779SJakub Wojciech Klama        else:
1338*134e1779SJakub Wojciech Klama            self.setpath(newfid, 'xattr')
1339*134e1779SJakub Wojciech Klama        return newfid, resp.size
1340*134e1779SJakub Wojciech Klama
1341*134e1779SJakub Wojciech Klama    def _pathsplit(self, path, startdir, allow_empty=False):
1342*134e1779SJakub Wojciech Klama        "common code for uxlookup and uxopen"
1343*134e1779SJakub Wojciech Klama        if self.rootfid is None:
1344*134e1779SJakub Wojciech Klama            raise LocalError('{0}: not attached'.format(self))
1345*134e1779SJakub Wojciech Klama        if path.startswith(b'/') or startdir is None:
1346*134e1779SJakub Wojciech Klama            startdir = self.rootfid
1347*134e1779SJakub Wojciech Klama        components = [i for i in path.split(b'/') if i != b'']
1348*134e1779SJakub Wojciech Klama        if len(components) == 0 and not allow_empty:
1349*134e1779SJakub Wojciech Klama            raise LocalError('{0}: {1!r}: empty path'.format(self, path))
1350*134e1779SJakub Wojciech Klama        return components, startdir
1351*134e1779SJakub Wojciech Klama
1352*134e1779SJakub Wojciech Klama    def uxlookup(self, path, startdir=None):
1353*134e1779SJakub Wojciech Klama        """
1354*134e1779SJakub Wojciech Klama        Unix-style lookup.  That is, lookup('/foo/bar') or
1355*134e1779SJakub Wojciech Klama        lookup('foo/bar').  If startdir is not None and the
1356*134e1779SJakub Wojciech Klama        path does not start with '/' we look up from there.
1357*134e1779SJakub Wojciech Klama        """
1358*134e1779SJakub Wojciech Klama        components, startdir = self._pathsplit(path, startdir, allow_empty=True)
1359*134e1779SJakub Wojciech Klama        return self.lookup_last(startdir, components)
1360*134e1779SJakub Wojciech Klama
1361*134e1779SJakub Wojciech Klama    def uxopen(self, path, oflags=0, perm=None, gid=None,
1362*134e1779SJakub Wojciech Klama               startdir=None, filetype=None):
1363*134e1779SJakub Wojciech Klama        """
1364*134e1779SJakub Wojciech Klama        Unix-style open()-with-option-to-create, or mkdir().
1365*134e1779SJakub Wojciech Klama        oflags is 0/1/2 with optional os.O_CREAT, perm defaults
1366*134e1779SJakub Wojciech Klama        to 0o666 (files) or 0o777 (directories).  If we use
1367*134e1779SJakub Wojciech Klama        a Linux create or mkdir op, we will need a gid, but it's
1368*134e1779SJakub Wojciech Klama        not required if you are opening an existing file.
1369*134e1779SJakub Wojciech Klama
1370*134e1779SJakub Wojciech Klama        Adds a final boolean value for "did we actually create".
1371*134e1779SJakub Wojciech Klama        Raises OSError if you ask for a directory but it's a file,
1372*134e1779SJakub Wojciech Klama        or vice versa.  (??? reconsider this later)
1373*134e1779SJakub Wojciech Klama
1374*134e1779SJakub Wojciech Klama        Note that this does not handle other file types, only
1375*134e1779SJakub Wojciech Klama        directories.
1376*134e1779SJakub Wojciech Klama        """
1377*134e1779SJakub Wojciech Klama        needtype = {
1378*134e1779SJakub Wojciech Klama            'dir': protocol.td.QTDIR,
1379*134e1779SJakub Wojciech Klama            None: protocol.td.QTFILE,
1380*134e1779SJakub Wojciech Klama        }[filetype]
1381*134e1779SJakub Wojciech Klama        omode_byte = oflags & 3 # cheating
1382*134e1779SJakub Wojciech Klama        # allow looking up /, but not creating /
1383*134e1779SJakub Wojciech Klama        allow_empty = (oflags & os.O_CREAT) == 0
1384*134e1779SJakub Wojciech Klama        components, startdir = self._pathsplit(path, startdir,
1385*134e1779SJakub Wojciech Klama                                               allow_empty=allow_empty)
1386*134e1779SJakub Wojciech Klama        if not (oflags & os.O_CREAT):
1387*134e1779SJakub Wojciech Klama            # Not creating, i.e., just look up and open existing file/dir.
1388*134e1779SJakub Wojciech Klama            fid, qid = self.lookup_last(startdir, components)
1389*134e1779SJakub Wojciech Klama            # If we got this far, use Topen on the fid; we did not
1390*134e1779SJakub Wojciech Klama            # create the file.
1391*134e1779SJakub Wojciech Klama            return self._uxopen2(path, needtype, fid, qid, omode_byte, False)
1392*134e1779SJakub Wojciech Klama
1393*134e1779SJakub Wojciech Klama        # Only used if using dot-L, but make sure it's always provided
1394*134e1779SJakub Wojciech Klama        # since this is generic.
1395*134e1779SJakub Wojciech Klama        if gid is None:
1396*134e1779SJakub Wojciech Klama            raise ValueError('gid is required when creating file or dir')
1397*134e1779SJakub Wojciech Klama
1398*134e1779SJakub Wojciech Klama        if len(components) > 1:
1399*134e1779SJakub Wojciech Klama            # Look up all but last component; this part must succeed.
1400*134e1779SJakub Wojciech Klama            fid, _ = self.lookup(startdir, components[:-1])
1401*134e1779SJakub Wojciech Klama
1402*134e1779SJakub Wojciech Klama            # Now proceed with the final component, using fid
1403*134e1779SJakub Wojciech Klama            # as the start dir.  Remember to clunk it!
1404*134e1779SJakub Wojciech Klama            startdir = fid
1405*134e1779SJakub Wojciech Klama            clunk_startdir = True
1406*134e1779SJakub Wojciech Klama            components = components[-1:]
1407*134e1779SJakub Wojciech Klama        else:
1408*134e1779SJakub Wojciech Klama            # Use startdir as the start dir, and get a new fid.
1409*134e1779SJakub Wojciech Klama            # Do not clunk startdir!
1410*134e1779SJakub Wojciech Klama            clunk_startdir = False
1411*134e1779SJakub Wojciech Klama            fid = self.alloc_fid()
1412*134e1779SJakub Wojciech Klama
1413*134e1779SJakub Wojciech Klama        # Now look up the (single) component.  If this fails,
1414*134e1779SJakub Wojciech Klama        # assume the file or directory needs to be created.
1415*134e1779SJakub Wojciech Klama        tag = self.get_tag()
1416*134e1779SJakub Wojciech Klama        pkt = self.proto.Twalk(tag=tag, fid=startdir, newfid=fid,
1417*134e1779SJakub Wojciech Klama                               nwname=1, wname=components)
1418*134e1779SJakub Wojciech Klama        super(P9Client, self).write(pkt)
1419*134e1779SJakub Wojciech Klama        resp = self.wait_for(tag)
1420*134e1779SJakub Wojciech Klama        if isinstance(resp, protocol.rrd.Rwalk):
1421*134e1779SJakub Wojciech Klama            if clunk_startdir:
1422*134e1779SJakub Wojciech Klama                self.clunk(startdir, ignore_error=True)
1423*134e1779SJakub Wojciech Klama            # fid successfully walked to refer to final component.
1424*134e1779SJakub Wojciech Klama            # Just need to actually open the file.
1425*134e1779SJakub Wojciech Klama            self.setpath(fid, _pathcat(self.getpath(startdir), components[0]))
1426*134e1779SJakub Wojciech Klama            qid = resp.wqid[0]
1427*134e1779SJakub Wojciech Klama            return self._uxopen2(needtype, fid, qid, omode_byte, False)
1428*134e1779SJakub Wojciech Klama
1429*134e1779SJakub Wojciech Klama        # Walk failed.  If we allocated a fid, retire it.  Then set
1430*134e1779SJakub Wojciech Klama        # up a fid that points to the parent directory in which to
1431*134e1779SJakub Wojciech Klama        # create the file or directory.  Note that if we're creating
1432*134e1779SJakub Wojciech Klama        # a file, this fid will get changed so that it points to the
1433*134e1779SJakub Wojciech Klama        # file instead of the directory, but if we're creating a
1434*134e1779SJakub Wojciech Klama        # directory, it will be unchanged.
1435*134e1779SJakub Wojciech Klama        if fid != startdir:
1436*134e1779SJakub Wojciech Klama            self.retire_fid(fid)
1437*134e1779SJakub Wojciech Klama        fid = self.dupfid(startdir)
1438*134e1779SJakub Wojciech Klama
1439*134e1779SJakub Wojciech Klama        try:
1440*134e1779SJakub Wojciech Klama            qid, iounit = self._uxcreate(filetype, fid, components[0],
1441*134e1779SJakub Wojciech Klama                                         oflags, omode_byte, perm, gid)
1442*134e1779SJakub Wojciech Klama
1443*134e1779SJakub Wojciech Klama            # Success.  If we created an ordinary file, we have everything
1444*134e1779SJakub Wojciech Klama            # now as create alters the incoming (dir) fid to open the file.
1445*134e1779SJakub Wojciech Klama            # Otherwise (mkdir), we need to open the file, as with
1446*134e1779SJakub Wojciech Klama            # a successful lookup.
1447*134e1779SJakub Wojciech Klama            #
1448*134e1779SJakub Wojciech Klama            # Note that qid type should match "needtype".
1449*134e1779SJakub Wojciech Klama            if filetype != 'dir':
1450*134e1779SJakub Wojciech Klama                if qid.type == needtype:
1451*134e1779SJakub Wojciech Klama                    return fid, qid, iounit, True
1452*134e1779SJakub Wojciech Klama                self.clunk(fid, ignore_error=True)
1453*134e1779SJakub Wojciech Klama                raise OSError(_wrong_file_type(qid),
1454*134e1779SJakub Wojciech Klama                             '{0}: server told to create {1} but '
1455*134e1779SJakub Wojciech Klama                             'created {2} instead'.format(path,
1456*134e1779SJakub Wojciech Klama                                                          qt2n(needtype),
1457*134e1779SJakub Wojciech Klama                                                          qt2n(qid.type)))
1458*134e1779SJakub Wojciech Klama
1459*134e1779SJakub Wojciech Klama            # Success: created dir; but now need to walk to and open it.
1460*134e1779SJakub Wojciech Klama            fid = self.alloc_fid()
1461*134e1779SJakub Wojciech Klama            tag = self.get_tag()
1462*134e1779SJakub Wojciech Klama            pkt = self.proto.Twalk(tag=tag, fid=startdir, newfid=fid,
1463*134e1779SJakub Wojciech Klama                                   nwname=1, wname=components)
1464*134e1779SJakub Wojciech Klama            super(P9Client, self).write(pkt)
1465*134e1779SJakub Wojciech Klama            resp = self.wait_for(tag)
1466*134e1779SJakub Wojciech Klama            if not isinstance(resp, protocol.rrd.Rwalk):
1467*134e1779SJakub Wojciech Klama                self.clunk(fid, ignore_error=True)
1468*134e1779SJakub Wojciech Klama                raise OSError(errno.ENOENT,
1469*134e1779SJakub Wojciech Klama                              '{0}: server made dir but then failed to '
1470*134e1779SJakub Wojciech Klama                              'find it again'.format(path))
1471*134e1779SJakub Wojciech Klama                self.setpath(fid, _pathcat(self.getpath(fid), components[0]))
1472*134e1779SJakub Wojciech Klama            return self._uxopen2(needtype, fid, qid, omode_byte, True)
1473*134e1779SJakub Wojciech Klama        finally:
1474*134e1779SJakub Wojciech Klama            # Regardless of success/failure/exception, make sure
1475*134e1779SJakub Wojciech Klama            # we clunk startdir if needed.
1476*134e1779SJakub Wojciech Klama            if clunk_startdir:
1477*134e1779SJakub Wojciech Klama                self.clunk(startdir, ignore_error=True)
1478*134e1779SJakub Wojciech Klama
1479*134e1779SJakub Wojciech Klama    def _uxcreate(self, filetype, fid, name, oflags, omode_byte, perm, gid):
1480*134e1779SJakub Wojciech Klama        """
1481*134e1779SJakub Wojciech Klama        Helper for creating dir-or-file.  The fid argument is the
1482*134e1779SJakub Wojciech Klama        parent directory on input, but will point to the file (if
1483*134e1779SJakub Wojciech Klama        we're creating a file) on return.  oflags only applies if
1484*134e1779SJakub Wojciech Klama        we're creating a file (even then we use omode_byte if we
1485*134e1779SJakub Wojciech Klama        are using the plan9 create op).
1486*134e1779SJakub Wojciech Klama        """
1487*134e1779SJakub Wojciech Klama        # Try to create or mkdir as appropriate.
1488*134e1779SJakub Wojciech Klama        if self.supports_all(protocol.td.Tlcreate, protocol.td.Tmkdir):
1489*134e1779SJakub Wojciech Klama            # Use Linux style create / mkdir.
1490*134e1779SJakub Wojciech Klama            if filetype == 'dir':
1491*134e1779SJakub Wojciech Klama                if perm is None:
1492*134e1779SJakub Wojciech Klama                    perm = 0o777
1493*134e1779SJakub Wojciech Klama                return self.mkdir(startdir, name, perm, gid), None
1494*134e1779SJakub Wojciech Klama            if perm is None:
1495*134e1779SJakub Wojciech Klama                perm = 0o666
1496*134e1779SJakub Wojciech Klama            lflags = flags_to_linux_flags(oflags)
1497*134e1779SJakub Wojciech Klama            return self.lcreate(fid, name, lflags, perm, gid)
1498*134e1779SJakub Wojciech Klama
1499*134e1779SJakub Wojciech Klama        if filetype == 'dir':
1500*134e1779SJakub Wojciech Klama            if perm is None:
1501*134e1779SJakub Wojciech Klama                perm = protocol.td.DMDIR | 0o777
1502*134e1779SJakub Wojciech Klama            else:
1503*134e1779SJakub Wojciech Klama                perm |= protocol.td.DMDIR
1504*134e1779SJakub Wojciech Klama        else:
1505*134e1779SJakub Wojciech Klama            if perm is None:
1506*134e1779SJakub Wojciech Klama                perm = 0o666
1507*134e1779SJakub Wojciech Klama        return self.create(fid, name, perm, omode_byte)
1508*134e1779SJakub Wojciech Klama
1509*134e1779SJakub Wojciech Klama    def _uxopen2(self, needtype, fid, qid, omode_byte, didcreate):
1510*134e1779SJakub Wojciech Klama        "common code for finishing up uxopen"
1511*134e1779SJakub Wojciech Klama        if qid.type != needtype:
1512*134e1779SJakub Wojciech Klama            self.clunk(fid, ignore_error=True)
1513*134e1779SJakub Wojciech Klama            raise OSError(_wrong_file_type(qid),
1514*134e1779SJakub Wojciech Klama                          '{0}: is {1}, expected '
1515*134e1779SJakub Wojciech Klama                          '{2}'.format(path, qt2n(qid.type), qt2n(needtype)))
1516*134e1779SJakub Wojciech Klama        qid, iounit = self.open(fid, omode_byte)
1517*134e1779SJakub Wojciech Klama        # ? should we re-check qid? it should not have changed
1518*134e1779SJakub Wojciech Klama        return fid, qid, iounit, didcreate
1519*134e1779SJakub Wojciech Klama
1520*134e1779SJakub Wojciech Klama    def uxmkdir(self, path, perm, gid, startdir=None):
1521*134e1779SJakub Wojciech Klama        """
1522*134e1779SJakub Wojciech Klama        Unix-style mkdir.
1523*134e1779SJakub Wojciech Klama
1524*134e1779SJakub Wojciech Klama        The gid is only applied if we are using .L style mkdir.
1525*134e1779SJakub Wojciech Klama        """
1526*134e1779SJakub Wojciech Klama        components, startdir = self._pathsplit(path, startdir)
1527*134e1779SJakub Wojciech Klama        clunkme = None
1528*134e1779SJakub Wojciech Klama        if len(components) > 1:
1529*134e1779SJakub Wojciech Klama            fid, _ = self.lookup(startdir, components[:-1])
1530*134e1779SJakub Wojciech Klama            startdir = fid
1531*134e1779SJakub Wojciech Klama            clunkme = fid
1532*134e1779SJakub Wojciech Klama            components = components[-1:]
1533*134e1779SJakub Wojciech Klama        try:
1534*134e1779SJakub Wojciech Klama            if self.supports(protocol.td.Tmkdir):
1535*134e1779SJakub Wojciech Klama                qid = self.mkdir(startdir, components[0], perm, gid)
1536*134e1779SJakub Wojciech Klama            else:
1537*134e1779SJakub Wojciech Klama                qid, _ = self.create(startdir, components[0],
1538*134e1779SJakub Wojciech Klama                                     protocol.td.DMDIR | perm,
1539*134e1779SJakub Wojciech Klama                                     protocol.td.OREAD)
1540*134e1779SJakub Wojciech Klama                # Should we chown/chgrp the dir?
1541*134e1779SJakub Wojciech Klama        finally:
1542*134e1779SJakub Wojciech Klama            if clunkme:
1543*134e1779SJakub Wojciech Klama                self.clunk(clunkme, ignore_error=True)
1544*134e1779SJakub Wojciech Klama        return qid
1545*134e1779SJakub Wojciech Klama
1546*134e1779SJakub Wojciech Klama    def uxreaddir(self, path, startdir=None, no_dotl=False):
1547*134e1779SJakub Wojciech Klama        """
1548*134e1779SJakub Wojciech Klama        Read a directory to get a list of names (which may or may not
1549*134e1779SJakub Wojciech Klama        include '.' and '..').
1550*134e1779SJakub Wojciech Klama
1551*134e1779SJakub Wojciech Klama        If no_dotl is True (or anything non-false-y), this uses the
1552*134e1779SJakub Wojciech Klama        plain or .u readdir format, otherwise it uses dot-L readdir
1553*134e1779SJakub Wojciech Klama        if possible.
1554*134e1779SJakub Wojciech Klama        """
1555*134e1779SJakub Wojciech Klama        components, startdir = self._pathsplit(path, startdir, allow_empty=True)
1556*134e1779SJakub Wojciech Klama        fid, qid = self.lookup_last(startdir, components)
1557*134e1779SJakub Wojciech Klama        try:
1558*134e1779SJakub Wojciech Klama            if qid.type != protocol.td.QTDIR:
1559*134e1779SJakub Wojciech Klama                raise OSError(errno.ENOTDIR,
1560*134e1779SJakub Wojciech Klama                              '{0}: {1}'.format(self.getpathX(fid),
1561*134e1779SJakub Wojciech Klama                                                os.strerror(errno.ENOTDIR)))
1562*134e1779SJakub Wojciech Klama            # We need both Tlopen and Treaddir to use Treaddir.
1563*134e1779SJakub Wojciech Klama            if not self.supports_all(protocol.td.Tlopen, protocol.td.Treaddir):
1564*134e1779SJakub Wojciech Klama                no_dotl = True
1565*134e1779SJakub Wojciech Klama            if no_dotl:
1566*134e1779SJakub Wojciech Klama                statvals = self.uxreaddir_stat_fid(fid)
1567*134e1779SJakub Wojciech Klama                return [i.name for i in statvals]
1568*134e1779SJakub Wojciech Klama
1569*134e1779SJakub Wojciech Klama            dirents = self.uxreaddir_dotl_fid(fid)
1570*134e1779SJakub Wojciech Klama            return [dirent.name for dirent in dirents]
1571*134e1779SJakub Wojciech Klama        finally:
1572*134e1779SJakub Wojciech Klama            self.clunk(fid, ignore_error=True)
1573*134e1779SJakub Wojciech Klama
1574*134e1779SJakub Wojciech Klama    def uxreaddir_stat(self, path, startdir=None):
1575*134e1779SJakub Wojciech Klama        """
1576*134e1779SJakub Wojciech Klama        Use directory read to get plan9 style stat data (plain or .u readdir).
1577*134e1779SJakub Wojciech Klama
1578*134e1779SJakub Wojciech Klama        Note that this gets a fid, then opens it, reads, then clunks
1579*134e1779SJakub Wojciech Klama        the fid.  If you already have a fid, you may want to use
1580*134e1779SJakub Wojciech Klama        uxreaddir_stat_fid (but note that this opens, yet does not
1581*134e1779SJakub Wojciech Klama        clunk, the fid).
1582*134e1779SJakub Wojciech Klama
1583*134e1779SJakub Wojciech Klama        We return the qid plus the list of the contents.  If the
1584*134e1779SJakub Wojciech Klama        target is not a directory, the qid will not have type QTDIR
1585*134e1779SJakub Wojciech Klama        and the contents list will be empty.
1586*134e1779SJakub Wojciech Klama
1587*134e1779SJakub Wojciech Klama        Raises OSError if this is applied to a non-directory.
1588*134e1779SJakub Wojciech Klama        """
1589*134e1779SJakub Wojciech Klama        components, startdir = self._pathsplit(path, startdir)
1590*134e1779SJakub Wojciech Klama        fid, qid = self.lookup_last(startdir, components)
1591*134e1779SJakub Wojciech Klama        try:
1592*134e1779SJakub Wojciech Klama            if qid.type != protocol.td.QTDIR:
1593*134e1779SJakub Wojciech Klama                raise OSError(errno.ENOTDIR,
1594*134e1779SJakub Wojciech Klama                              '{0}: {1}'.format(self.getpathX(fid),
1595*134e1779SJakub Wojciech Klama                                                os.strerror(errno.ENOTDIR)))
1596*134e1779SJakub Wojciech Klama            statvals = self.ux_readdir_stat_fid(fid)
1597*134e1779SJakub Wojciech Klama            return qid, statvals
1598*134e1779SJakub Wojciech Klama        finally:
1599*134e1779SJakub Wojciech Klama            self.clunk(fid, ignore_error=True)
1600*134e1779SJakub Wojciech Klama
1601*134e1779SJakub Wojciech Klama    def uxreaddir_stat_fid(self, fid):
1602*134e1779SJakub Wojciech Klama        """
1603*134e1779SJakub Wojciech Klama        Implement readdir loop that extracts stat values.
1604*134e1779SJakub Wojciech Klama        This opens, but does not clunk, the given fid.
1605*134e1779SJakub Wojciech Klama
1606*134e1779SJakub Wojciech Klama        Unlike uxreaddir_stat(), if this is applied to a file,
1607*134e1779SJakub Wojciech Klama        rather than a directory, it just returns no entries.
1608*134e1779SJakub Wojciech Klama        """
1609*134e1779SJakub Wojciech Klama        statvals = []
1610*134e1779SJakub Wojciech Klama        qid, iounit = self.open(fid, protocol.td.OREAD)
1611*134e1779SJakub Wojciech Klama        # ?? is a zero iounit allowed? if so, what do we use here?
1612*134e1779SJakub Wojciech Klama        if qid.type == protocol.td.QTDIR:
1613*134e1779SJakub Wojciech Klama            if iounit <= 0:
1614*134e1779SJakub Wojciech Klama                iounit = 512 # probably good enough
1615*134e1779SJakub Wojciech Klama            offset = 0
1616*134e1779SJakub Wojciech Klama            while True:
1617*134e1779SJakub Wojciech Klama                bstring = self.read(fid, offset, iounit)
1618*134e1779SJakub Wojciech Klama                if bstring == b'':
1619*134e1779SJakub Wojciech Klama                    break
1620*134e1779SJakub Wojciech Klama                statvals.extend(self.decode_stat_objects(bstring))
1621*134e1779SJakub Wojciech Klama                offset += len(bstring)
1622*134e1779SJakub Wojciech Klama        return statvals
1623*134e1779SJakub Wojciech Klama
1624*134e1779SJakub Wojciech Klama    def uxreaddir_dotl_fid(self, fid):
1625*134e1779SJakub Wojciech Klama        """
1626*134e1779SJakub Wojciech Klama        Implement readdir loop that uses dot-L style dirents.
1627*134e1779SJakub Wojciech Klama        This opens, but does not clunk, the given fid.
1628*134e1779SJakub Wojciech Klama
1629*134e1779SJakub Wojciech Klama        If applied to a file, the lopen should fail, because of the
1630*134e1779SJakub Wojciech Klama        L_O_DIRECTORY flag.
1631*134e1779SJakub Wojciech Klama        """
1632*134e1779SJakub Wojciech Klama        dirents = []
1633*134e1779SJakub Wojciech Klama        qid, iounit = self.lopen(fid, protocol.td.OREAD |
1634*134e1779SJakub Wojciech Klama                                      protocol.td.L_O_DIRECTORY)
1635*134e1779SJakub Wojciech Klama        # ?? is a zero iounit allowed? if so, what do we use here?
1636*134e1779SJakub Wojciech Klama        # but, we want a minimum of over 256 anyway, let's go for 512
1637*134e1779SJakub Wojciech Klama        if iounit < 512:
1638*134e1779SJakub Wojciech Klama            iounit = 512
1639*134e1779SJakub Wojciech Klama        offset = 0
1640*134e1779SJakub Wojciech Klama        while True:
1641*134e1779SJakub Wojciech Klama            bstring = self.readdir(fid, offset, iounit)
1642*134e1779SJakub Wojciech Klama            if bstring == b'':
1643*134e1779SJakub Wojciech Klama                break
1644*134e1779SJakub Wojciech Klama            ents = self.decode_readdir_dirents(bstring)
1645*134e1779SJakub Wojciech Klama            if len(ents) == 0:
1646*134e1779SJakub Wojciech Klama                break               # ???
1647*134e1779SJakub Wojciech Klama            dirents.extend(ents)
1648*134e1779SJakub Wojciech Klama            offset = ents[-1].offset
1649*134e1779SJakub Wojciech Klama        return dirents
1650*134e1779SJakub Wojciech Klama
1651*134e1779SJakub Wojciech Klama    def uxremove(self, path, startdir=None, filetype=None,
1652*134e1779SJakub Wojciech Klama                 force=False, recurse=False):
1653*134e1779SJakub Wojciech Klama        """
1654*134e1779SJakub Wojciech Klama        Implement rm / rmdir, with optional -rf.
1655*134e1779SJakub Wojciech Klama        if filetype is None, remove dir or file.  If 'dir' or 'file'
1656*134e1779SJakub Wojciech Klama        remove only if it's one of those.  If force is set, ignore
1657*134e1779SJakub Wojciech Klama        failures to remove.  If recurse is True, remove contents of
1658*134e1779SJakub Wojciech Klama        directories (recursively).
1659*134e1779SJakub Wojciech Klama
1660*134e1779SJakub Wojciech Klama        File type mismatches (when filetype!=None) raise OSError (?).
1661*134e1779SJakub Wojciech Klama        """
1662*134e1779SJakub Wojciech Klama        components, startdir = self._pathsplit(path, startdir, allow_empty=True)
1663*134e1779SJakub Wojciech Klama        # Look up all components. If
1664*134e1779SJakub Wojciech Klama        # we get an error we'll just assume the file does not
1665*134e1779SJakub Wojciech Klama        # exist (is this good?).
1666*134e1779SJakub Wojciech Klama        try:
1667*134e1779SJakub Wojciech Klama            fid, qid = self.lookup_last(startdir, components)
1668*134e1779SJakub Wojciech Klama        except RemoteError:
1669*134e1779SJakub Wojciech Klama            return
1670*134e1779SJakub Wojciech Klama        if qid.type == protocol.td.QTDIR:
1671*134e1779SJakub Wojciech Klama            # it's a directory, remove only if allowed.
1672*134e1779SJakub Wojciech Klama            # Note that we must check for "rm -r /" (len(components)==0).
1673*134e1779SJakub Wojciech Klama            if filetype == 'file':
1674*134e1779SJakub Wojciech Klama                self.clunk(fid, ignore_error=True)
1675*134e1779SJakub Wojciech Klama                raise OSError(_wrong_file_type(qid),
1676*134e1779SJakub Wojciech Klama                              '{0}: is dir, expected file'.format(path))
1677*134e1779SJakub Wojciech Klama            isroot = len(components) == 0
1678*134e1779SJakub Wojciech Klama            closer = self.clunk if isroot else self.remove
1679*134e1779SJakub Wojciech Klama            if recurse:
1680*134e1779SJakub Wojciech Klama                # NB: _rm_recursive does not clunk fid
1681*134e1779SJakub Wojciech Klama                self._rm_recursive(fid, filetype, force)
1682*134e1779SJakub Wojciech Klama            # This will fail if the directory is non-empty, unless of
1683*134e1779SJakub Wojciech Klama            # course we tell it to ignore error.
1684*134e1779SJakub Wojciech Klama            closer(fid, ignore_error=force)
1685*134e1779SJakub Wojciech Klama            return
1686*134e1779SJakub Wojciech Klama        # Not a directory, call it a file (even if socket or fifo etc).
1687*134e1779SJakub Wojciech Klama        if filetype == 'dir':
1688*134e1779SJakub Wojciech Klama            self.clunk(fid, ignore_error=True)
1689*134e1779SJakub Wojciech Klama            raise OSError(_wrong_file_type(qid),
1690*134e1779SJakub Wojciech Klama                          '{0}: is file, expected dir'.format(path))
1691*134e1779SJakub Wojciech Klama        self.remove(fid, ignore_error=force)
1692*134e1779SJakub Wojciech Klama
1693*134e1779SJakub Wojciech Klama    def _rm_file_by_dfid(self, dfid, name, force=False):
1694*134e1779SJakub Wojciech Klama        """
1695*134e1779SJakub Wojciech Klama        Remove a file whose name is <name> (no path, just a component
1696*134e1779SJakub Wojciech Klama        name) whose parent directory is <dfid>.  We may assume that the
1697*134e1779SJakub Wojciech Klama        file really is a file (or a socket, or fifo, or some such, but
1698*134e1779SJakub Wojciech Klama        definitely not a directory).
1699*134e1779SJakub Wojciech Klama
1700*134e1779SJakub Wojciech Klama        If force is set, ignore failures.
1701*134e1779SJakub Wojciech Klama        """
1702*134e1779SJakub Wojciech Klama        # If we have unlinkat, that's the fast way.  But it may
1703*134e1779SJakub Wojciech Klama        # return an ENOTSUP error.  If it does we shouldn't bother
1704*134e1779SJakub Wojciech Klama        # doing this again.
1705*134e1779SJakub Wojciech Klama        if self.supports(protocol.td.Tunlinkat):
1706*134e1779SJakub Wojciech Klama            try:
1707*134e1779SJakub Wojciech Klama                self.unlinkat(dfid, name, 0)
1708*134e1779SJakub Wojciech Klama                return
1709*134e1779SJakub Wojciech Klama            except RemoteError as err:
1710*134e1779SJakub Wojciech Klama                if not err.is_ENOTSUP():
1711*134e1779SJakub Wojciech Klama                    raise
1712*134e1779SJakub Wojciech Klama                self.unsupported(protocol.td.Tunlinkat)
1713*134e1779SJakub Wojciech Klama                # fall through to remove() op
1714*134e1779SJakub Wojciech Klama        # Fall back to lookup + remove.
1715*134e1779SJakub Wojciech Klama        try:
1716*134e1779SJakub Wojciech Klama            fid, qid = self.lookup_last(dfid, [name])
1717*134e1779SJakub Wojciech Klama        except RemoteError:
1718*134e1779SJakub Wojciech Klama            # If this has an errno we could tell ENOENT from EPERM,
1719*134e1779SJakub Wojciech Klama            # and actually raise an error for the latter.  Should we?
1720*134e1779SJakub Wojciech Klama            return
1721*134e1779SJakub Wojciech Klama        self.remove(fid, ignore_error=force)
1722*134e1779SJakub Wojciech Klama
1723*134e1779SJakub Wojciech Klama    def _rm_recursive(self, dfid, filetype, force):
1724*134e1779SJakub Wojciech Klama        """
1725*134e1779SJakub Wojciech Klama        Recursively remove a directory.  filetype is probably None,
1726*134e1779SJakub Wojciech Klama        but if it's 'dir' we fail if the directory contains non-dir
1727*134e1779SJakub Wojciech Klama        files.
1728*134e1779SJakub Wojciech Klama
1729*134e1779SJakub Wojciech Klama        If force is set, ignore failures.
1730*134e1779SJakub Wojciech Klama
1731*134e1779SJakub Wojciech Klama        Although we open dfid (via the readdir.*_fid calls) we
1732*134e1779SJakub Wojciech Klama        do not clunk it here; that's the caller's job.
1733*134e1779SJakub Wojciech Klama        """
1734*134e1779SJakub Wojciech Klama        # first, remove contents
1735*134e1779SJakub Wojciech Klama        if self.supports_all(protocol.td.Tlopen, protocol.td.Treaddir):
1736*134e1779SJakub Wojciech Klama            for entry in self.uxreaddir_dotl_fid(dfid):
1737*134e1779SJakub Wojciech Klama                if entry.name in (b'.', b'..'):
1738*134e1779SJakub Wojciech Klama                    continue
1739*134e1779SJakub Wojciech Klama                fid, qid = self.lookup(dfid, [entry.name])
1740*134e1779SJakub Wojciech Klama                try:
1741*134e1779SJakub Wojciech Klama                    attrs = self.Tgetattr(fid, protocol.td.GETATTR_MODE)
1742*134e1779SJakub Wojciech Klama                    if stat.S_ISDIR(attrs.mode):
1743*134e1779SJakub Wojciech Klama                        self.uxremove(entry.name, dfid, filetype, force, True)
1744*134e1779SJakub Wojciech Klama                    else:
1745*134e1779SJakub Wojciech Klama                        self.remove(fid)
1746*134e1779SJakub Wojciech Klama                        fid = None
1747*134e1779SJakub Wojciech Klama                finally:
1748*134e1779SJakub Wojciech Klama                    if fid is not None:
1749*134e1779SJakub Wojciech Klama                        self.clunk(fid, ignore_error=True)
1750*134e1779SJakub Wojciech Klama        else:
1751*134e1779SJakub Wojciech Klama            for statobj in self.uxreaddir_stat_fid(dfid):
1752*134e1779SJakub Wojciech Klama                # skip . and ..
1753*134e1779SJakub Wojciech Klama                name = statobj.name
1754*134e1779SJakub Wojciech Klama                if name in (b'.', b'..'):
1755*134e1779SJakub Wojciech Klama                    continue
1756*134e1779SJakub Wojciech Klama                if statobj.qid.type == protocol.td.QTDIR:
1757*134e1779SJakub Wojciech Klama                    self.uxremove(name, dfid, filetype, force, True)
1758*134e1779SJakub Wojciech Klama                else:
1759*134e1779SJakub Wojciech Klama                    self._rm_file_by_dfid(dfid, name, force)
1760*134e1779SJakub Wojciech Klama
1761*134e1779SJakub Wojciech Klamadef _wrong_file_type(qid):
1762*134e1779SJakub Wojciech Klama    "return EISDIR or ENOTDIR for passing to OSError"
1763*134e1779SJakub Wojciech Klama    if qid.type == protocol.td.QTDIR:
1764*134e1779SJakub Wojciech Klama        return errno.EISDIR
1765*134e1779SJakub Wojciech Klama    return errno.ENOTDIR
1766*134e1779SJakub Wojciech Klama
1767*134e1779SJakub Wojciech Klamadef flags_to_linux_flags(flags):
1768*134e1779SJakub Wojciech Klama    """
1769*134e1779SJakub Wojciech Klama    Convert OS flags (O_CREAT etc) to Linux flags (protocol.td.L_O_CREAT etc).
1770*134e1779SJakub Wojciech Klama    """
1771*134e1779SJakub Wojciech Klama    flagmap = {
1772*134e1779SJakub Wojciech Klama        os.O_CREAT: protocol.td.L_O_CREAT,
1773*134e1779SJakub Wojciech Klama        os.O_EXCL: protocol.td.L_O_EXCL,
1774*134e1779SJakub Wojciech Klama        os.O_NOCTTY: protocol.td.L_O_NOCTTY,
1775*134e1779SJakub Wojciech Klama        os.O_TRUNC: protocol.td.L_O_TRUNC,
1776*134e1779SJakub Wojciech Klama        os.O_APPEND: protocol.td.L_O_APPEND,
1777*134e1779SJakub Wojciech Klama        os.O_DIRECTORY: protocol.td.L_O_DIRECTORY,
1778*134e1779SJakub Wojciech Klama    }
1779*134e1779SJakub Wojciech Klama
1780*134e1779SJakub Wojciech Klama    result = flags & os.O_RDWR
1781*134e1779SJakub Wojciech Klama    flags &= ~os.O_RDWR
1782*134e1779SJakub Wojciech Klama    for key, value in flagmap.iteritems():
1783*134e1779SJakub Wojciech Klama        if flags & key:
1784*134e1779SJakub Wojciech Klama            result |= value
1785*134e1779SJakub Wojciech Klama            flags &= ~key
1786*134e1779SJakub Wojciech Klama    if flags:
1787*134e1779SJakub Wojciech Klama        raise ValueError('untranslated bits 0x{0:x} in os flags'.format(flags))
1788*134e1779SJakub Wojciech Klama    return result
1789