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