Changeset View
Changeset View
Standalone View
Standalone View
contrib/lib9p/pytest/p9conn.py
- This file was added.
#! /usr/bin/env python | |||||
""" | |||||
handle plan9 server <-> client connections | |||||
(We can act as either server or client.) | |||||
This code needs some doctests or other unit tests... | |||||
""" | |||||
import collections | |||||
import errno | |||||
import logging | |||||
import math | |||||
import os | |||||
import socket | |||||
import stat | |||||
import struct | |||||
import sys | |||||
import threading | |||||
import time | |||||
import lerrno | |||||
import numalloc | |||||
import p9err | |||||
import pfod | |||||
import protocol | |||||
# Timespec based timestamps, if present, have | |||||
# both seconds and nanoseconds. | |||||
Timespec = collections.namedtuple('Timespec', 'sec nsec') | |||||
# File attributes from Tgetattr, or given to Tsetattr. | |||||
# (move to protocol.py?) We use pfod here instead of | |||||
# namedtuple so that we can create instances with all-None | |||||
# fields easily. | |||||
Fileattrs = pfod.pfod('Fileattrs', | |||||
'ino mode uid gid nlink rdev size blksize blocks ' | |||||
'atime mtime ctime btime gen data_version') | |||||
qt2n = protocol.qid_type2name | |||||
STD_P9_PORT=564 | |||||
class P9Error(Exception): | |||||
pass | |||||
class RemoteError(P9Error): | |||||
""" | |||||
Used when the remote returns an error. We track the client | |||||
(connection instance), the operation being attempted, the | |||||
message, and an error number and type. The message may be | |||||
from the Rerror reply, or from converting the errno in a dot-L | |||||
or dot-u Rerror reply. The error number may be None if the | |||||
type is 'Rerror' rather than 'Rlerror'. The message may be | |||||
None or empty string if a non-None errno supplies the error | |||||
instead. | |||||
""" | |||||
def __init__(self, client, op, msg, etype, errno): | |||||
self.client = str(client) | |||||
self.op = op | |||||
self.msg = msg | |||||
self.etype = etype # 'Rerror' or 'Rlerror' | |||||
self.errno = errno # may be None | |||||
self.message = self._get_message() | |||||
super(RemoteError, self).__init__(self, self.message) | |||||
def __repr__(self): | |||||
return ('{0!r}({1}, {2}, {3}, {4}, ' | |||||
'{5})'.format(self.__class__.__name__, self.client, self.op, | |||||
self.msg, self.errno, self.etype)) | |||||
def __str__(self): | |||||
prefix = '{0}: {1}: '.format(self.client, self.op) | |||||
if self.errno: # check for "is not None", or just non-false-y? | |||||
name = {'Rerror': '.u', 'Rlerror': 'Linux'}[self.etype] | |||||
middle = '[{0} error {1}] '.format(name, self.errno) | |||||
else: | |||||
middle = '' | |||||
return '{0}{1}{2}'.format(prefix, middle, self.message) | |||||
def is_ENOTSUP(self): | |||||
if self.etype == 'Rlerror': | |||||
return self.errno == lerrno.EOPNOTSUPP | |||||
return self.errno == errno.EOPNOTSUPP | |||||
def _get_message(self): | |||||
"get message based on self.msg or self.errno" | |||||
if self.errno is not None: | |||||
return { | |||||
'Rlerror': p9err.dotl_strerror, | |||||
'Rerror' : p9err.dotu_strerror, | |||||
}[self.etype](self.errno) | |||||
return self.msg | |||||
class LocalError(P9Error): | |||||
pass | |||||
class TEError(LocalError): | |||||
pass | |||||
class P9SockIO(object): | |||||
""" | |||||
Common base for server and client, handle send and | |||||
receive to communications channel. Note that this | |||||
need not set up the channel initially, only the logger. | |||||
The channel is typically connected later. However, you | |||||
can provide one initially. | |||||
""" | |||||
def __init__(self, logger, name=None, server=None, port=STD_P9_PORT): | |||||
self.logger = logger | |||||
self.channel = None | |||||
self.name = name | |||||
self.maxio = None | |||||
self.size_coder = struct.Struct('<I') | |||||
if server is not None: | |||||
self.connect(server, port) | |||||
self.max_payload = 2**32 - self.size_coder.size | |||||
def __str__(self): | |||||
if self.name: | |||||
return self.name | |||||
return repr(self) | |||||
def get_recommended_maxio(self): | |||||
"suggest a max I/O size, for when self.maxio is 0 / unset" | |||||
return 16 * 4096 | |||||
def min_maxio(self): | |||||
"return a minimum size below which we refuse to work" | |||||
return self.size_coder.size + 100 | |||||
def connect(self, server, port=STD_P9_PORT): | |||||
""" | |||||
Connect to given server name / IP address. | |||||
If self.name was none, sets self.name to ip:port on success. | |||||
""" | |||||
if self.is_connected(): | |||||
raise LocalError('already connected') | |||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) | |||||
sock.connect((server, port)) | |||||
if self.name is None: | |||||
if port == STD_P9_PORT: | |||||
name = server | |||||
else: | |||||
name = '{0}:{1}'.format(server, port) | |||||
else: | |||||
name = None | |||||
self.declare_connected(sock, name, None) | |||||
def is_connected(self): | |||||
"predicate: are we connected?" | |||||
return self.channel != None | |||||
def declare_connected(self, chan, name, maxio): | |||||
""" | |||||
Now available for normal protocol (size-prefixed) I/O. | |||||
Replaces chan and name and adjusts maxio, if those | |||||
parameters are not None. | |||||
""" | |||||
if maxio: | |||||
minio = self.min_maxio() | |||||
if maxio < minio: | |||||
raise LocalError('maxio={0} < minimum {1}'.format(maxio, minio)) | |||||
if chan is not None: | |||||
self.channel = chan | |||||
if name is not None: | |||||
self.name = name | |||||
if maxio is not None: | |||||
self.maxio = maxio | |||||
self.max_payload = maxio - self.size_coder.size | |||||
def reduce_maxio(self, maxio): | |||||
"Reduce maximum I/O size per other-side request" | |||||
minio = self.min_maxio() | |||||
if maxio < minio: | |||||
raise LocalError('new maxio={0} < minimum {1}'.format(maxio, minio)) | |||||
if maxio > self.maxio: | |||||
raise LocalError('new maxio={0} > current {1}'.format(maxio, | |||||
self.maxio)) | |||||
self.maxio = maxio | |||||
self.max_payload = maxio - self.size_coder.size | |||||
def declare_disconnected(self): | |||||
"Declare comm channel dead (note: leaves self.name set!)" | |||||
self.channel = None | |||||
self.maxio = None | |||||
def shutwrite(self): | |||||
"Do a SHUT_WR on the outbound channel - can't send more" | |||||
chan = self.channel | |||||
# we're racing other threads here | |||||
try: | |||||
chan.shutdown(socket.SHUT_WR) | |||||
except (OSError, AttributeError): | |||||
pass | |||||
def shutdown(self): | |||||
"Shut down comm channel" | |||||
if self.channel: | |||||
try: | |||||
self.channel.shutdown(socket.SHUT_RDWR) | |||||
except socket.error: | |||||
pass | |||||
self.channel.close() | |||||
self.declare_disconnected() | |||||
def read(self): | |||||
""" | |||||
Try to read a complete packet. | |||||
Returns '' for EOF, as read() usually does. | |||||
If we can't even get the size, this still returns ''. | |||||
If we get a sensible size but are missing some data, | |||||
we can return a short packet. Since we know if we did | |||||
this, we also return a boolean: True means "really got a | |||||
complete packet." | |||||
Note that '' EOF always returns False: EOF is never a | |||||
complete packet. | |||||
""" | |||||
if self.channel is None: | |||||
return b'', False | |||||
size_field = self.xread(self.size_coder.size) | |||||
if len(size_field) < self.size_coder.size: | |||||
if len(size_field) == 0: | |||||
self.logger.log(logging.INFO, '%s: normal EOF', self) | |||||
else: | |||||
self.logger.log(logging.ERROR, | |||||
'%s: EOF while reading size (got %d bytes)', | |||||
self, len(size_field)) | |||||
# should we raise an error here? | |||||
return b'', False | |||||
size = self.size_coder.unpack(size_field)[0] - self.size_coder.size | |||||
if size <= 0 or size > self.max_payload: | |||||
self.logger.log(logging.ERROR, | |||||
'%s: incoming size %d is insane ' | |||||
'(max payload is %d)', | |||||
self, size, self.max_payload) | |||||
# indicate EOF - should we raise an error instead, here? | |||||
return b'', False | |||||
data = self.xread(size) | |||||
return data, len(data) == size | |||||
def xread(self, nbytes): | |||||
""" | |||||
Read nbytes bytes, looping if necessary. Return '' for | |||||
EOF; may return a short count if we get some data, then | |||||
EOF. | |||||
""" | |||||
assert nbytes > 0 | |||||
# Try to get everything at once (should usually succeed). | |||||
# Return immediately for EOF or got-all-data. | |||||
data = self.channel.recv(nbytes) | |||||
if data == b'' or len(data) == nbytes: | |||||
return data | |||||
# Gather data fragments into an array, then join it all at | |||||
# the end. | |||||
count = len(data) | |||||
data = [data] | |||||
while count < nbytes: | |||||
more = self.channel.recv(nbytes - count) | |||||
if more == b'': | |||||
break | |||||
count += len(more) | |||||
data.append(more) | |||||
return b''.join(data) | |||||
def write(self, data): | |||||
""" | |||||
Write all the data, in the usual encoding. Note that | |||||
the length of the data, including the length of the length | |||||
itself, is already encoded in the first 4 bytes of the | |||||
data. | |||||
Raises IOError if we can't write everything. | |||||
Raises LocalError if len(data) exceeds max_payload. | |||||
""" | |||||
size = len(data) | |||||
assert size >= 4 | |||||
if size > self.max_payload: | |||||
raise LocalError('data length {0} exceeds ' | |||||
'maximum {1}'.format(size, self.max_payload)) | |||||
self.channel.sendall(data) | |||||
def _pathcat(prefix, suffix): | |||||
""" | |||||
Concatenate paths we are using on the server side. This is | |||||
basically just prefix + / + suffix, with two complications: | |||||
It's possible we don't have a prefix path, in which case | |||||
we want the suffix without a leading slash. | |||||
It's possible that the prefix is just b'/', in which case we | |||||
want prefix + suffix. | |||||
""" | |||||
if prefix: | |||||
if prefix == b'/': # or prefix.endswith(b'/')? | |||||
return prefix + suffix | |||||
return prefix + b'/' + suffix | |||||
return suffix | |||||
class P9Client(P9SockIO): | |||||
""" | |||||
Act as client. | |||||
We need the a logger (see logging), a timeout, and a protocol | |||||
version to request. By default, we will downgrade to a lower | |||||
version if asked. | |||||
If server and port are supplied, they are remembered and become | |||||
the default for .connect() (which is still deferred). | |||||
Note that we keep a table of fid-to-path in self.live_fids, | |||||
but at any time (except while holding the lock) a fid can | |||||
be deleted entirely, and the table entry may just be True | |||||
if we have no path name. In general, we update the name | |||||
when we can. | |||||
""" | |||||
def __init__(self, logger, timeout, version, may_downgrade=True, | |||||
server=None, port=None): | |||||
super(P9Client, self).__init__(logger) | |||||
self.timeout = timeout | |||||
self.iproto = protocol.p9_version(version) | |||||
self.may_downgrade = may_downgrade | |||||
self.tagalloc = numalloc.NumAlloc(0, 65534) | |||||
self.tagstate = {} | |||||
# The next bit is slighlty dirty: perhaps we should just | |||||
# allocate NOFID out of the 2**32-1 range, so as to avoid | |||||
# "knowing" that it's 2**32-1. | |||||
self.fidalloc = numalloc.NumAlloc(0, protocol.td.NOFID - 1) | |||||
self.live_fids = {} | |||||
self.rootfid = None | |||||
self.rootqid = None | |||||
self.rthread = None | |||||
self.lock = threading.Lock() | |||||
self.new_replies = threading.Condition(self.lock) | |||||
self._monkeywrench = {} | |||||
self._server = server | |||||
self._port = port | |||||
self._unsup = {} | |||||
def get_monkey(self, what): | |||||
"check for a monkey-wrench" | |||||
with self.lock: | |||||
wrench = self._monkeywrench.get(what) | |||||
if wrench is None: | |||||
return None | |||||
if isinstance(wrench, list): | |||||
# repeats wrench[0] times, or forever if that's 0 | |||||
ret = wrench[1] | |||||
if wrench[0] > 0: | |||||
wrench[0] -= 1 | |||||
if wrench[0] == 0: | |||||
del self._monkeywrench[what] | |||||
else: | |||||
ret = wrench | |||||
del self._monkeywrench[what] | |||||
return ret | |||||
def set_monkey(self, what, how, repeat=None): | |||||
""" | |||||
Set a monkey-wrench. If repeat is not None it is the number of | |||||
times the wrench is applied (0 means forever, or until you call | |||||
set again with how=None). What is what to monkey-wrench, which | |||||
depends on the op. How is generally a replacement value. | |||||
""" | |||||
if how is None: | |||||
with self.lock: | |||||
try: | |||||
del self._monkeywrench[what] | |||||
except KeyError: | |||||
pass | |||||
return | |||||
if repeat is not None: | |||||
how = [repeat, how] | |||||
with self.lock: | |||||
self._monkeywrench[what] = how | |||||
def get_tag(self, for_Tversion=False): | |||||
"get next available tag ID" | |||||
with self.lock: | |||||
if for_Tversion: | |||||
tag = 65535 | |||||
else: | |||||
tag = self.tagalloc.alloc() | |||||
if tag is None: | |||||
raise LocalError('all tags in use') | |||||
self.tagstate[tag] = True # ie, in use, still waiting | |||||
return tag | |||||
def set_tag(self, tag, reply): | |||||
"set the reply info for the given tag" | |||||
assert tag >= 0 and tag < 65536 | |||||
with self.lock: | |||||
# check whether we're still waiting for the tag | |||||
state = self.tagstate.get(tag) | |||||
if state is True: | |||||
self.tagstate[tag] = reply # i.e., here's the answer | |||||
self.new_replies.notify_all() | |||||
return | |||||
# state must be one of these... | |||||
if state is False: | |||||
# We gave up on this tag. Reply came anyway. | |||||
self.logger.log(logging.INFO, | |||||
'%s: got tag %d = %r after timing out on it', | |||||
self, tag, reply) | |||||
self.retire_tag_locked(tag) | |||||
return | |||||
if state is None: | |||||
# We got a tag back from the server that was not | |||||
# outstanding! | |||||
self.logger.log(logging.WARNING, | |||||
'%s: got tag %d = %r when tag %d not in use!', | |||||
self, tag, reply, tag) | |||||
return | |||||
# We got a second reply before handling the first reply! | |||||
self.logger.log(logging.WARNING, | |||||
'%s: got tag %d = %r when tag %d = %r!', | |||||
self, tag, reply, tag, state) | |||||
return | |||||
def retire_tag(self, tag): | |||||
"retire the given tag - only used by the thread that handled the result" | |||||
if tag == 65535: | |||||
return | |||||
assert tag >= 0 and tag < 65535 | |||||
with self.lock: | |||||
self.retire_tag_locked(tag) | |||||
def retire_tag_locked(self, tag): | |||||
"retire the given tag while holding self.lock" | |||||
# must check "in tagstate" because we can race | |||||
# with retire_all_tags. | |||||
if tag in self.tagstate: | |||||
del self.tagstate[tag] | |||||
self.tagalloc.free(tag) | |||||
def retire_all_tags(self): | |||||
"retire all tags, after connection drop" | |||||
with self.lock: | |||||
# release all tags in any state (waiting, answered, timedout) | |||||
self.tagalloc.free_multi(self.tagstate.keys()) | |||||
self.tagstate = {} | |||||
self.new_replies.notify_all() | |||||
def alloc_fid(self): | |||||
"allocate new fid" | |||||
with self.lock: | |||||
fid = self.fidalloc.alloc() | |||||
self.live_fids[fid] = True | |||||
return fid | |||||
def getpath(self, fid): | |||||
"get path from fid, or return None if no path known, or not valid" | |||||
with self.lock: | |||||
path = self.live_fids.get(fid) | |||||
if path is True: | |||||
path = None | |||||
return path | |||||
def getpathX(self, fid): | |||||
""" | |||||
Much like getpath, but return <fid N, unknown path> if necessary. | |||||
If we do have a path, return its repr(). | |||||
""" | |||||
path = self.getpath(fid) | |||||
if path is None: | |||||
return '<fid {0}, unknown path>'.format(fid) | |||||
return repr(path) | |||||
def setpath(self, fid, path): | |||||
"associate fid with new path (possibly from another fid)" | |||||
with self.lock: | |||||
if isinstance(path, int): | |||||
path = self.live_fids.get(path) | |||||
# path might now be None (not a live fid after all), or | |||||
# True (we have no path name), or potentially even the | |||||
# empty string (invalid for our purposes). Treat all of | |||||
# those as True, meaning "no known path". | |||||
if not path: | |||||
path = True | |||||
if self.live_fids.get(fid): | |||||
# Existing fid maps to either True or its old path. | |||||
# Set the new path (which may be just a placeholder). | |||||
self.live_fids[fid] = path | |||||
def did_rename(self, fid, ncomp, newdir=None): | |||||
""" | |||||
Announce that we renamed using a fid - we'll try to update | |||||
other fids based on this (we can't really do it perfectly). | |||||
NOTE: caller must provide a final-component. | |||||
The caller can supply the new path (and should | |||||
do so if the rename is not based on the retained path | |||||
for the supplied fid, i.e., for rename ops where fid | |||||
can move across directories). The rules: | |||||
- If newdir is None (default), we use stored path. | |||||
- Otherwise, newdir provides the best approximation | |||||
we have to the path that needs ncomp appended. | |||||
(This is based on the fact that renames happen via Twstat | |||||
or Trename, or Trenameat, which change just one tail component, | |||||
but the path names vary.) | |||||
""" | |||||
if ncomp is None: | |||||
return | |||||
opath = self.getpath(fid) | |||||
if newdir is None: | |||||
if opath is None: | |||||
return | |||||
ocomps = opath.split(b'/') | |||||
ncomps = ocomps[0:-1] | |||||
else: | |||||
ocomps = None # well, none yet anyway | |||||
ncomps = newdir.split(b'/') | |||||
ncomps.append(ncomp) | |||||
if opath is None or opath[0] != '/': | |||||
# We don't have enough information to fix anything else. | |||||
# Just store the new path and return. We have at least | |||||
# a partial path now, which is no worse than before. | |||||
npath = b'/'.join(ncomps) | |||||
with self.lock: | |||||
if fid in self.live_fids: | |||||
self.live_fids[fid] = npath | |||||
return | |||||
if ocomps is None: | |||||
ocomps = opath.split(b'/') | |||||
olen = len(ocomps) | |||||
ofinal = ocomps[olen - 1] | |||||
# Old paths is full path. Find any other fids that start | |||||
# with some or all the components in ocomps. Note that if | |||||
# we renamed /one/two/three to /four/five this winds up | |||||
# renaming files /one/a to /four/a, /one/two/b to /four/five/b, | |||||
# and so on. | |||||
with self.lock: | |||||
for fid2, path2 in self.live_fids.iteritems(): | |||||
# Skip fids without byte-string paths | |||||
if not isinstance(path2, bytes): | |||||
continue | |||||
# Before splitting (which is a bit expensive), try | |||||
# a straightforward prefix match. This might give | |||||
# some false hits, e.g., prefix /one/two/threepenny | |||||
# starts with /one/two/three, but it quickly eliminates | |||||
# /raz/baz/mataz and the like. | |||||
if not path2.startswith(opath): | |||||
continue | |||||
# Split up the path, and use that to make sure that | |||||
# the final component is a full match. | |||||
parts2 = path2.split(b'/') | |||||
if parts2[olen - 1] != ofinal: | |||||
continue | |||||
# OK, path2 starts with the old (renamed) sequence. | |||||
# Replace the old components with the new ones. | |||||
# This updates the renamed fid when we come across | |||||
# it! It also handles a change in the number of | |||||
# components, thanks to Python's slice assignment. | |||||
parts2[0:olen] = ncomps | |||||
self.live_fids[fid2] = b'/'.join(parts2) | |||||
def retire_fid(self, fid): | |||||
"retire one fid" | |||||
with self.lock: | |||||
self.fidalloc.free(fid) | |||||
del self.live_fids[fid] | |||||
def retire_all_fids(self): | |||||
"return live fids to pool" | |||||
# this is useful for debugging fid leaks: | |||||
#for fid in self.live_fids: | |||||
# print 'retiring', fid, self.getpathX(fid) | |||||
with self.lock: | |||||
self.fidalloc.free_multi(self.live_fids.keys()) | |||||
self.live_fids = {} | |||||
def read_responses(self): | |||||
"Read responses. This gets spun off as a thread." | |||||
while self.is_connected(): | |||||
pkt, is_full = super(P9Client, self).read() | |||||
if pkt == b'': | |||||
self.shutwrite() | |||||
self.retire_all_tags() | |||||
return | |||||
if not is_full: | |||||
self.logger.log(logging.WARNING, '%s: got short packet', self) | |||||
try: | |||||
# We have one special case: if we're not yet connected | |||||
# with a version, we must unpack *as if* it's a plain | |||||
# 9P2000 response. | |||||
if self.have_version: | |||||
resp = self.proto.unpack(pkt) | |||||
else: | |||||
resp = protocol.plain.unpack(pkt) | |||||
except protocol.SequenceError as err: | |||||
self.logger.log(logging.ERROR, '%s: bad response: %s', | |||||
self, err) | |||||
try: | |||||
resp = self.proto.unpack(pkt, noerror=True) | |||||
except protocol.SequenceError: | |||||
header = self.proto.unpack_header(pkt, noerror=True) | |||||
self.logger.log(logging.ERROR, | |||||
'%s: (not even raw-decodable)', self) | |||||
self.logger.log(logging.ERROR, | |||||
'%s: header decode produced %r', | |||||
self, header) | |||||
else: | |||||
self.logger.log(logging.ERROR, | |||||
'%s: raw decode produced %r', | |||||
self, resp) | |||||
# after this kind of problem, probably need to | |||||
# shut down, but let's leave that out for a bit | |||||
else: | |||||
# NB: all protocol responses have a "tag", | |||||
# so resp['tag'] always exists. | |||||
self.logger.log(logging.DEBUG, "read_resp: tag %d resp %r", resp.tag, resp) | |||||
self.set_tag(resp.tag, resp) | |||||
def wait_for(self, tag): | |||||
""" | |||||
Wait for a response to the given tag. Return the response, | |||||
releasing the tag. If self.timeout is not None, wait at most | |||||
that long (and release the tag even if there's no reply), else | |||||
wait forever. | |||||
If this returns None, either the tag was bad initially, or | |||||
a timeout occurred, or the connection got shut down. | |||||
""" | |||||
self.logger.log(logging.DEBUG, "wait_for: tag %d", tag) | |||||
if self.timeout is None: | |||||
deadline = None | |||||
else: | |||||
deadline = time.time() + self.timeout | |||||
with self.lock: | |||||
while True: | |||||
# tagstate is True (waiting) or False (timedout) or | |||||
# a valid response, or None if we've reset the tag | |||||
# states (retire_all_tags, after connection drop). | |||||
resp = self.tagstate.get(tag, None) | |||||
if resp is None: | |||||
# out of sync, exit loop | |||||
break | |||||
if resp is True: | |||||
# still waiting for a response - wait some more | |||||
self.new_replies.wait(self.timeout) | |||||
if deadline and time.time() > deadline: | |||||
# Halt the waiting, but go around once more. | |||||
# Note we may have killed the tag by now though. | |||||
if tag in self.tagstate: | |||||
self.tagstate[tag] = False | |||||
continue | |||||
# resp is either False (timeout) or a reply. | |||||
# If resp is False, change it to None; the tag | |||||
# is now dead until we get a reply (then we | |||||
# just toss the reply). | |||||
# Otherwise, we're done with the tag: free it. | |||||
# In either case, stop now. | |||||
if resp is False: | |||||
resp = None | |||||
else: | |||||
self.tagalloc.free(tag) | |||||
del self.tagstate[tag] | |||||
break | |||||
return resp | |||||
def badresp(self, req, resp): | |||||
""" | |||||
Complain that a response was not something expected. | |||||
""" | |||||
if resp is None: | |||||
self.shutdown() | |||||
raise TEError('{0}: {1}: timeout or EOF'.format(self, req)) | |||||
if isinstance(resp, protocol.rrd.Rlerror): | |||||
raise RemoteError(self, req, None, 'Rlerror', resp.ecode) | |||||
if isinstance(resp, protocol.rrd.Rerror): | |||||
if resp.errnum is None: | |||||
raise RemoteError(self, req, resp.errstr, 'Rerror', None) | |||||
raise RemoteError(self, req, None, 'Rerror', resp.errnum) | |||||
raise LocalError('{0}: {1} got response {2!r}'.format(self, req, resp)) | |||||
def supports(self, req_code): | |||||
""" | |||||
Test self.proto.support(req_code) unless we've recorded that | |||||
while the protocol supports it, the client does not. | |||||
""" | |||||
return req_code not in self._unsup and self.proto.supports(req_code) | |||||
def supports_all(self, *req_codes): | |||||
"basically just all(supports(...))" | |||||
return all(self.supports(code) for code in req_codes) | |||||
def unsupported(self, req_code): | |||||
""" | |||||
Record an ENOTSUP (RemoteError was ENOTSUP) for a request. | |||||
Must be called from the op, this does not happen automatically. | |||||
(It's just an optimization.) | |||||
""" | |||||
self._unsup[req_code] = True | |||||
def connect(self, server=None, port=None): | |||||
""" | |||||
Connect to given server/port pair. | |||||
The server and port are remembered. If given as None, | |||||
the last remembered values are used. The initial | |||||
remembered values are from the creation of this client | |||||
instance. | |||||
New values are only remembered here on a *successful* | |||||
connect, however. | |||||
""" | |||||
if server is None: | |||||
server = self._server | |||||
if server is None: | |||||
raise LocalError('connect: no server specified and no default') | |||||
if port is None: | |||||
port = self._port | |||||
if port is None: | |||||
port = STD_P9_PORT | |||||
self.name = None # wipe out previous name, if any | |||||
super(P9Client, self).connect(server, port) | |||||
maxio = self.get_recommended_maxio() | |||||
self.declare_connected(None, None, maxio) | |||||
self.proto = self.iproto # revert to initial protocol | |||||
self.have_version = False | |||||
self.rthread = threading.Thread(target=self.read_responses) | |||||
self.rthread.start() | |||||
tag = self.get_tag(for_Tversion=True) | |||||
req = protocol.rrd.Tversion(tag=tag, msize=maxio, | |||||
version=self.get_monkey('version')) | |||||
super(P9Client, self).write(self.proto.pack_from(req)) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rversion): | |||||
self.shutdown() | |||||
if isinstance(resp, protocol.rrd.Rerror): | |||||
version = req.version or self.proto.get_version() | |||||
# for python3, we need to convert version to string | |||||
if not isinstance(version, str): | |||||
version = version.decode('utf-8', 'surrogateescape') | |||||
raise RemoteError(self, 'version ' + version, | |||||
resp.errstr, 'Rerror', None) | |||||
self.badresp('version', resp) | |||||
their_maxio = resp.msize | |||||
try: | |||||
self.reduce_maxio(their_maxio) | |||||
except LocalError as err: | |||||
raise LocalError('{0}: sent maxio={1}, they tried {2}: ' | |||||
'{3}'.format(self, maxio, their_maxio, | |||||
err.args[0])) | |||||
if resp.version != self.proto.get_version(): | |||||
if not self.may_downgrade: | |||||
self.shutdown() | |||||
raise LocalError('{0}: they only support ' | |||||
'version {1!r}'.format(self, resp.version)) | |||||
# raises LocalError if the version is bad | |||||
# (should we wrap it with a connect-to-{0} msg?) | |||||
self.proto = self.proto.downgrade_to(resp.version) | |||||
self._server = server | |||||
self._port = port | |||||
self.have_version = True | |||||
def attach(self, afid, uname, aname, n_uname): | |||||
""" | |||||
Attach. | |||||
Currently we don't know how to do authentication, | |||||
but we'll pass any provided afid through. | |||||
""" | |||||
if afid is None: | |||||
afid = protocol.td.NOFID | |||||
if uname is None: | |||||
uname = '' | |||||
if aname is None: | |||||
aname = '' | |||||
if n_uname is None: | |||||
n_uname = protocol.td.NONUNAME | |||||
tag = self.get_tag() | |||||
fid = self.alloc_fid() | |||||
pkt = self.proto.Tattach(tag=tag, fid=fid, afid=afid, | |||||
uname=uname, aname=aname, | |||||
n_uname=n_uname) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rattach): | |||||
self.retire_fid(fid) | |||||
self.badresp('attach', resp) | |||||
# probably should check resp.qid | |||||
self.rootfid = fid | |||||
self.rootqid = resp.qid | |||||
self.setpath(fid, b'/') | |||||
def shutdown(self): | |||||
"disconnect from server" | |||||
if self.rootfid is not None: | |||||
self.clunk(self.rootfid, ignore_error=True) | |||||
self.retire_all_tags() | |||||
self.retire_all_fids() | |||||
self.rootfid = None | |||||
self.rootqid = None | |||||
super(P9Client, self).shutdown() | |||||
if self.rthread: | |||||
self.rthread.join() | |||||
self.rthread = None | |||||
def dupfid(self, fid): | |||||
""" | |||||
Copy existing fid to a new fid. | |||||
""" | |||||
tag = self.get_tag() | |||||
newfid = self.alloc_fid() | |||||
pkt = self.proto.Twalk(tag=tag, fid=fid, newfid=newfid, nwname=0, | |||||
wname=[]) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rwalk): | |||||
self.retire_fid(newfid) | |||||
self.badresp('walk {0}'.format(self.getpathX(fid)), resp) | |||||
# Copy path too | |||||
self.setpath(newfid, fid) | |||||
return newfid | |||||
def lookup(self, fid, components): | |||||
""" | |||||
Do Twalk. Caller must provide a starting fid, which should | |||||
be rootfid to look up from '/' - we do not do / vs . here. | |||||
Caller must also provide a component-ized path (on purpose, | |||||
so that caller can provide invalid components like '' or '/'). | |||||
The components must be byte-strings as well, for the same | |||||
reason. | |||||
We do allocate the new fid ourselves here, though. | |||||
There's no logic here to split up long walks (yet?). | |||||
""" | |||||
# these are too easy to screw up, so check | |||||
if self.rootfid is None: | |||||
raise LocalError('{0}: not attached'.format(self)) | |||||
if (isinstance(components, (str, bytes) or | |||||
not all(isinstance(i, bytes) for i in components))): | |||||
raise LocalError('{0}: lookup: invalid ' | |||||
'components {1!r}'.format(self, components)) | |||||
tag = self.get_tag() | |||||
newfid = self.alloc_fid() | |||||
startpath = self.getpath(fid) | |||||
pkt = self.proto.Twalk(tag=tag, fid=fid, newfid=newfid, | |||||
nwname=len(components), wname=components) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rwalk): | |||||
self.retire_fid(newfid) | |||||
self.badresp('walk {0} in ' | |||||
'{1}'.format(components, self.getpathX(fid)), | |||||
resp) | |||||
# Just because we got Rwalk does not mean we got ALL the | |||||
# way down the path. Raise OSError(ENOENT) if we're short. | |||||
if resp.nwqid > len(components): | |||||
# ??? this should be impossible. Local error? Remote error? | |||||
# OS Error? | |||||
self.clunk(newfid, ignore_error=True) | |||||
raise LocalError('{0}: walk {1} in {2} returned {3} ' | |||||
'items'.format(self, components, | |||||
self.getpathX(fid), resp.nwqid)) | |||||
if resp.nwqid < len(components): | |||||
self.clunk(newfid, ignore_error=True) | |||||
# Looking up a/b/c and got just a/b, c is what's missing. | |||||
# Looking up a/b/c and got just a, b is what's missing. | |||||
missing = components[resp.nwqid] | |||||
within = _pathcat(startpath, b'/'.join(components[:resp.nwqid])) | |||||
raise OSError(errno.ENOENT, | |||||
'{0}: {1} in {2}'.format(os.strerror(errno.ENOENT), | |||||
missing, within)) | |||||
self.setpath(newfid, _pathcat(startpath, b'/'.join(components))) | |||||
return newfid, resp.wqid | |||||
def lookup_last(self, fid, components): | |||||
""" | |||||
Like lookup, but return only the last component's qid. | |||||
As a special case, if components is an empty list, we | |||||
handle that. | |||||
""" | |||||
rfid, wqid = self.lookup(fid, components) | |||||
if len(wqid): | |||||
return rfid, wqid[-1] | |||||
if fid == self.rootfid: # usually true, if we get here at all | |||||
return rfid, self.rootqid | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Tstat(tag=tag, fid=rfid) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rstat): | |||||
self.badresp('stat {0}'.format(self.getpathX(fid)), resp) | |||||
statval = self.proto.unpack_wirestat(resp.data) | |||||
return rfid, statval.qid | |||||
def clunk(self, fid, ignore_error=False): | |||||
"issue clunk(fid)" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Tclunk(tag=tag, fid=fid) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rclunk): | |||||
if ignore_error: | |||||
return | |||||
self.badresp('clunk {0}'.format(self.getpathX(fid)), resp) | |||||
self.retire_fid(fid) | |||||
def remove(self, fid, ignore_error=False): | |||||
"issue remove (old style), which also clunks fid" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Tremove(tag=tag, fid=fid) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rremove): | |||||
if ignore_error: | |||||
# remove failed: still need to clunk the fid | |||||
self.clunk(fid, True) | |||||
return | |||||
self.badresp('remove {0}'.format(self.getpathX(fid)), resp) | |||||
self.retire_fid(fid) | |||||
def create(self, fid, name, perm, mode, filetype=None, extension=b''): | |||||
""" | |||||
Issue create op (note that this may be mkdir, symlink, etc). | |||||
fid is the directory in which the create happens, and for | |||||
regular files, it becomes, on success, a fid referring to | |||||
the now-open file. perm is, e.g., 0644, 0755, etc., | |||||
optionally with additional high bits. mode is a mode | |||||
byte (e.g., protocol.td.ORDWR, or OWRONLY|OTRUNC, etc.). | |||||
As a service to callers, we take two optional arguments | |||||
specifying the file type ('dir', 'symlink', 'device', | |||||
'fifo', or 'socket') and additional info if needed. | |||||
The additional info for a symlink is the target of the | |||||
link (a byte string), and the additional info for a device | |||||
is a byte string with "b <major> <minor>" or "c <major> <minor>". | |||||
Otherwise, callers can leave filetype=None and encode the bits | |||||
into the mode (caller must still provide extension if needed). | |||||
We do NOT check whether the extension matches extra DM bits, | |||||
or that there's only one DM bit set, or whatever, since this | |||||
is a testing setup. | |||||
""" | |||||
tag = self.get_tag() | |||||
if filetype is not None: | |||||
perm |= { | |||||
'dir': protocol.td.DMDIR, | |||||
'symlink': protocol.td.DMSYMLINK, | |||||
'device': protocol.td.DMDEVICE, | |||||
'fifo': protocol.td.DMNAMEDPIPE, | |||||
'socket': protocol.td.DMSOCKET, | |||||
}[filetype] | |||||
pkt = self.proto.Tcreate(tag=tag, fid=fid, name=name, | |||||
perm=perm, mode=mode, extension=extension) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rcreate): | |||||
self.badresp('create {0} in {1}'.format(name, self.getpathX(fid)), | |||||
resp) | |||||
if resp.qid.type == protocol.td.QTFILE: | |||||
# Creating a regular file opens the file, | |||||
# thus changing the fid's path. | |||||
self.setpath(fid, _pathcat(self.getpath(fid), name)) | |||||
return resp.qid, resp.iounit | |||||
def open(self, fid, mode): | |||||
"use Topen to open file or directory fid (mode is 1 byte)" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Topen(tag=tag, fid=fid, mode=mode) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Ropen): | |||||
self.badresp('open {0}'.format(self.getpathX(fid)), resp) | |||||
return resp.qid, resp.iounit | |||||
def lopen(self, fid, flags): | |||||
"use Tlopen to open file or directory fid (flags from L_O_*)" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Tlopen(tag=tag, fid=fid, flags=flags) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rlopen): | |||||
self.badresp('lopen {0}'.format(self.getpathX(fid)), resp) | |||||
return resp.qid, resp.iounit | |||||
def read(self, fid, offset, count): | |||||
"read (up to) count bytes from offset, given open fid" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Tread(tag=tag, fid=fid, offset=offset, count=count) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rread): | |||||
self.badresp('read {0} bytes at offset {1} in ' | |||||
'{2}'.format(count, offset, self.getpathX(fid)), | |||||
resp) | |||||
return resp.data | |||||
def write(self, fid, offset, data): | |||||
"write (up to) count bytes to offset, given open fid" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Twrite(tag=tag, fid=fid, offset=offset, | |||||
count=len(data), data=data) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rwrite): | |||||
self.badresp('write {0} bytes at offset {1} in ' | |||||
'{2}'.format(len(data), offset, self.getpathX(fid)), | |||||
resp) | |||||
return resp.count | |||||
# Caller may | |||||
# - pass an actual stat object, or | |||||
# - pass in all the individual to-set items by keyword, or | |||||
# - mix and match a bit: get an existing stat, then use | |||||
# keywords to override fields. | |||||
# We convert "None"s to the internal "do not change" values, | |||||
# and for diagnostic purposes, can turn "do not change" back | |||||
# to None at the end, too. | |||||
def wstat(self, fid, statobj=None, **kwargs): | |||||
if statobj is None: | |||||
statobj = protocol.td.stat() | |||||
else: | |||||
statobj = statobj._copy() | |||||
# Fields in stat that you can't send as a wstat: the | |||||
# type and qid are informative. Similarly, the | |||||
# 'extension' is an input when creating a file but | |||||
# read-only when stat-ing. | |||||
# | |||||
# It's not clear what it means to set dev, but we'll leave | |||||
# it in as an optional parameter here. fs/backend.c just | |||||
# errors out on an attempt to change it. | |||||
if self.proto == protocol.plain: | |||||
forbid = ('type', 'qid', 'extension', | |||||
'n_uid', 'n_gid', 'n_muid') | |||||
else: | |||||
forbid = ('type', 'qid', 'extension') | |||||
nochange = { | |||||
'type': 0, | |||||
'qid': protocol.td.qid(0, 0, 0), | |||||
'dev': 2**32 - 1, | |||||
'mode': 2**32 - 1, | |||||
'atime': 2**32 - 1, | |||||
'mtime': 2**32 - 1, | |||||
'length': 2**64 - 1, | |||||
'name': b'', | |||||
'uid': b'', | |||||
'gid': b'', | |||||
'muid': b'', | |||||
'extension': b'', | |||||
'n_uid': 2**32 - 1, | |||||
'n_gid': 2**32 - 1, | |||||
'n_muid': 2**32 - 1, | |||||
} | |||||
for field in statobj._fields: | |||||
if field in kwargs: | |||||
if field in forbid: | |||||
raise ValueError('cannot wstat a stat.{0}'.format(field)) | |||||
statobj[field] = kwargs.pop(field) | |||||
else: | |||||
if field in forbid or statobj[field] is None: | |||||
statobj[field] = nochange[field] | |||||
if kwargs: | |||||
raise TypeError('wstat() got an unexpected keyword argument ' | |||||
'{0!r}'.format(kwargs.popitem())) | |||||
data = self.proto.pack_wirestat(statobj) | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Twstat(tag=tag, fid=fid, data=data) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rwstat): | |||||
# For error viewing, switch all the do-not-change | |||||
# and can't-change fields to None. | |||||
statobj.qid = None | |||||
for field in statobj._fields: | |||||
if field in forbid: | |||||
statobj[field] = None | |||||
elif field in nochange and statobj[field] == nochange[field]: | |||||
statobj[field] = None | |||||
self.badresp('wstat {0}={1}'.format(self.getpathX(fid), statobj), | |||||
resp) | |||||
# wstat worked - change path names if needed | |||||
if statobj.name != b'': | |||||
self.did_rename(fid, statobj.name) | |||||
def readdir(self, fid, offset, count): | |||||
"read (up to) count bytes of dir data from offset, given open fid" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Treaddir(tag=tag, fid=fid, offset=offset, count=count) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rreaddir): | |||||
self.badresp('readdir {0} bytes at offset {1} in ' | |||||
'{2}'.format(count, offset, self.getpathX(fid)), | |||||
resp) | |||||
return resp.data | |||||
def rename(self, fid, dfid, name): | |||||
"invoke Trename: rename file <fid> to <dfid>/name" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Trename(tag=tag, fid=fid, dfid=dfid, name=name) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rrename): | |||||
self.badresp('rename {0} to {2} in ' | |||||
'{1}'.format(self.getpathX(fid), | |||||
self.getpathX(dfid), name), | |||||
resp) | |||||
self.did_rename(fid, name, self.getpath(dfid)) | |||||
def renameat(self, olddirfid, oldname, newdirfid, newname): | |||||
"invoke Trenameat: rename <olddirfid>/oldname to <newdirfid>/newname" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Trenameat(tag=tag, | |||||
olddirfid=olddirfid, oldname=oldname, | |||||
newdirfid=newdirfid, newname=newname) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rrenameat): | |||||
self.badresp('rename {1} in {0} to {3} in ' | |||||
'{2}'.format(oldname, self.getpathX(olddirfid), | |||||
newname, self.getpathX(newdirdfid)), | |||||
resp) | |||||
# There's no renamed *fid*, just a renamed file! So no | |||||
# call to self.did_rename(). | |||||
def unlinkat(self, dirfd, name, flags): | |||||
"invoke Tunlinkat - flags should be 0 or protocol.td.AT_REMOVEDIR" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Tunlinkat(tag=tag, dirfd=dirfd, | |||||
name=name, flags=flags) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Runlinkat): | |||||
self.badresp('unlinkat {0} in ' | |||||
'{1}'.format(name, self.getpathX(dirfd)), resp) | |||||
def decode_stat_objects(self, bstring, noerror=False): | |||||
""" | |||||
Read on a directory returns an array of stat objects. | |||||
Note that for .u these encode extra data. | |||||
It's possible for this to produce a SequenceError, if | |||||
the data are incorrect, unless you pass noerror=True. | |||||
""" | |||||
objlist = [] | |||||
offset = 0 | |||||
while offset < len(bstring): | |||||
obj, offset = self.proto.unpack_wirestat(bstring, offset, noerror) | |||||
objlist.append(obj) | |||||
return objlist | |||||
def decode_readdir_dirents(self, bstring, noerror=False): | |||||
""" | |||||
Readdir on a directory returns an array of dirent objects. | |||||
It's possible for this to produce a SequenceError, if | |||||
the data are incorrect, unless you pass noerror=True. | |||||
""" | |||||
objlist = [] | |||||
offset = 0 | |||||
while offset < len(bstring): | |||||
obj, offset = self.proto.unpack_dirent(bstring, offset, noerror) | |||||
objlist.append(obj) | |||||
return objlist | |||||
def lcreate(self, fid, name, lflags, mode, gid): | |||||
"issue lcreate (.L)" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Tlcreate(tag=tag, fid=fid, name=name, | |||||
flags=lflags, mode=mode, gid=gid) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rlcreate): | |||||
self.badresp('create {0} in ' | |||||
'{1}'.format(name, self.getpathX(fid)), resp) | |||||
# Creating a file opens the file, | |||||
# thus changing the fid's path. | |||||
self.setpath(fid, _pathcat(self.getpath(fid), name)) | |||||
return resp.qid, resp.iounit | |||||
def mkdir(self, dfid, name, mode, gid): | |||||
"issue mkdir (.L)" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Tmkdir(tag=tag, dfid=dfid, name=name, | |||||
mode=mode, gid=gid) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rmkdir): | |||||
self.badresp('mkdir {0} in ' | |||||
'{1}'.format(name, self.getpathX(dfid)), resp) | |||||
return resp.qid | |||||
# We don't call this getattr(), for the obvious reason. | |||||
def Tgetattr(self, fid, request_mask=protocol.td.GETATTR_ALL): | |||||
"issue Tgetattr.L - get what you ask for, or everything by default" | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Tgetattr(tag=tag, fid=fid, request_mask=request_mask) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rgetattr): | |||||
self.badresp('Tgetattr {0} of ' | |||||
'{1}'.format(request_mask, self.getpathX(fid)), resp) | |||||
attrs = Fileattrs() | |||||
# Handle the simplest valid-bit tests: | |||||
for name in ('mode', 'nlink', 'uid', 'gid', 'rdev', | |||||
'size', 'blocks', 'gen', 'data_version'): | |||||
bit = getattr(protocol.td, 'GETATTR_' + name.upper()) | |||||
if resp.valid & bit: | |||||
attrs[name] = resp[name] | |||||
# Handle the timestamps, which are timespec pairs | |||||
for name in ('atime', 'mtime', 'ctime', 'btime'): | |||||
bit = getattr(protocol.td, 'GETATTR_' + name.upper()) | |||||
if resp.valid & bit: | |||||
attrs[name] = Timespec(sec=resp[name + '_sec'], | |||||
nsec=resp[name + '_nsec']) | |||||
# There is no control bit for blksize; qemu and Linux always | |||||
# provide one. | |||||
attrs.blksize = resp.blksize | |||||
# Handle ino, which comes out of qid.path | |||||
if resp.valid & protocol.td.GETATTR_INO: | |||||
attrs.ino = resp.qid.path | |||||
return attrs | |||||
# We don't call this setattr(), for the obvious reason. | |||||
# See wstat for usage. Note that time fields can be set | |||||
# with either second or nanosecond resolutions, and some | |||||
# can be set without supplying an actual timestamp, so | |||||
# this is all pretty ad-hoc. | |||||
# | |||||
# There's also one keyword-only argument, ctime=<anything>, | |||||
# which means "set SETATTR_CTIME". This has the same effect | |||||
# as supplying valid=protocol.td.SETATTR_CTIME. | |||||
def Tsetattr(self, fid, valid=0, attrs=None, **kwargs): | |||||
if attrs is None: | |||||
attrs = Fileattrs() | |||||
else: | |||||
attrs = attrs._copy() | |||||
# Start with an empty (all-zero) Tsetattr instance. We | |||||
# don't really need to zero out tag and fid, but it doesn't | |||||
# hurt. Note that if caller says, e.g., valid=SETATTR_SIZE | |||||
# but does not supply an incoming size (via "attrs" or a size= | |||||
# argument), we'll ask to set that field to 0. | |||||
attrobj = protocol.rrd.Tsetattr() | |||||
for field in attrobj._fields: | |||||
attrobj[field] = 0 | |||||
# In this case, forbid means "only as kwargs": these values | |||||
# in an incoming attrs object are merely ignored. | |||||
forbid = ('ino', 'nlink', 'rdev', 'blksize', 'blocks', 'btime', | |||||
'gen', 'data_version') | |||||
for field in attrs._fields: | |||||
if field in kwargs: | |||||
if field in forbid: | |||||
raise ValueError('cannot Tsetattr {0}'.format(field)) | |||||
attrs[field] = kwargs.pop(field) | |||||
elif attrs[field] is None: | |||||
continue | |||||
# OK, we're setting this attribute. Many are just | |||||
# numeric - if that's the case, we're good, set the | |||||
# field and the appropriate bit. | |||||
bitname = 'SETATTR_' + field.upper() | |||||
bit = getattr(protocol.td, bitname) | |||||
if field in ('mode', 'uid', 'gid', 'size'): | |||||
valid |= bit | |||||
attrobj[field] = attrs[field] | |||||
continue | |||||
# Timestamps are special: The value may be given as | |||||
# an integer (seconds), or as a float (we convert to | |||||
# (we convert to sec+nsec), or as a timespec (sec+nsec). | |||||
# If specified as 0, we mean "we are not providing the | |||||
# actual time, use the server's time." | |||||
# | |||||
# The ctime field's value, if any, is *ignored*. | |||||
if field in ('atime', 'mtime'): | |||||
value = attrs[field] | |||||
if hasattr(value, '__len__'): | |||||
if len(value) != 2: | |||||
raise ValueError('invalid {0}={1!r}'.format(field, | |||||
value)) | |||||
sec = value[0] | |||||
nsec = value[1] | |||||
else: | |||||
sec = value | |||||
if isinstance(sec, float): | |||||
nsec, sec = math.modf(sec) | |||||
nsec = int(round(nsec * 1000000000)) | |||||
else: | |||||
nsec = 0 | |||||
valid |= bit | |||||
attrobj[field + '_sec'] = sec | |||||
attrobj[field + '_nsec'] = nsec | |||||
if sec != 0 or nsec != 0: | |||||
# Add SETATTR_ATIME_SET or SETATTR_MTIME_SET | |||||
# as appropriate, to tell the server to *this | |||||
# specific* time, instead of just "server now". | |||||
bit = getattr(protocol.td, bitname + '_SET') | |||||
valid |= bit | |||||
if 'ctime' in kwargs: | |||||
kwargs.pop('ctime') | |||||
valid |= protocol.td.SETATTR_CTIME | |||||
if kwargs: | |||||
raise TypeError('Tsetattr() got an unexpected keyword argument ' | |||||
'{0!r}'.format(kwargs.popitem())) | |||||
tag = self.get_tag() | |||||
attrobj.valid = valid | |||||
attrobj.tag = tag | |||||
attrobj.fid = fid | |||||
pkt = self.proto.pack(attrobj) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rsetattr): | |||||
self.badresp('Tsetattr {0} {1} of ' | |||||
'{2}'.format(valid, attrs, self.getpathX(fid)), resp) | |||||
def xattrwalk(self, fid, name=None): | |||||
"walk one name or all names: caller should read() the returned fid" | |||||
tag = self.get_tag() | |||||
newfid = self.alloc_fid() | |||||
pkt = self.proto.Txattrwalk(tag=tag, fid=fid, newfid=newfid, | |||||
name=name or '') | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rxattrwalk): | |||||
self.retire_fid(newfid) | |||||
self.badresp('Txattrwalk {0} of ' | |||||
'{1}'.format(name, self.getpathX(fid)), resp) | |||||
if name: | |||||
self.setpath(newfid, 'xattr:' + name) | |||||
else: | |||||
self.setpath(newfid, 'xattr') | |||||
return newfid, resp.size | |||||
def _pathsplit(self, path, startdir, allow_empty=False): | |||||
"common code for uxlookup and uxopen" | |||||
if self.rootfid is None: | |||||
raise LocalError('{0}: not attached'.format(self)) | |||||
if path.startswith(b'/') or startdir is None: | |||||
startdir = self.rootfid | |||||
components = [i for i in path.split(b'/') if i != b''] | |||||
if len(components) == 0 and not allow_empty: | |||||
raise LocalError('{0}: {1!r}: empty path'.format(self, path)) | |||||
return components, startdir | |||||
def uxlookup(self, path, startdir=None): | |||||
""" | |||||
Unix-style lookup. That is, lookup('/foo/bar') or | |||||
lookup('foo/bar'). If startdir is not None and the | |||||
path does not start with '/' we look up from there. | |||||
""" | |||||
components, startdir = self._pathsplit(path, startdir, allow_empty=True) | |||||
return self.lookup_last(startdir, components) | |||||
def uxopen(self, path, oflags=0, perm=None, gid=None, | |||||
startdir=None, filetype=None): | |||||
""" | |||||
Unix-style open()-with-option-to-create, or mkdir(). | |||||
oflags is 0/1/2 with optional os.O_CREAT, perm defaults | |||||
to 0o666 (files) or 0o777 (directories). If we use | |||||
a Linux create or mkdir op, we will need a gid, but it's | |||||
not required if you are opening an existing file. | |||||
Adds a final boolean value for "did we actually create". | |||||
Raises OSError if you ask for a directory but it's a file, | |||||
or vice versa. (??? reconsider this later) | |||||
Note that this does not handle other file types, only | |||||
directories. | |||||
""" | |||||
needtype = { | |||||
'dir': protocol.td.QTDIR, | |||||
None: protocol.td.QTFILE, | |||||
}[filetype] | |||||
omode_byte = oflags & 3 # cheating | |||||
# allow looking up /, but not creating / | |||||
allow_empty = (oflags & os.O_CREAT) == 0 | |||||
components, startdir = self._pathsplit(path, startdir, | |||||
allow_empty=allow_empty) | |||||
if not (oflags & os.O_CREAT): | |||||
# Not creating, i.e., just look up and open existing file/dir. | |||||
fid, qid = self.lookup_last(startdir, components) | |||||
# If we got this far, use Topen on the fid; we did not | |||||
# create the file. | |||||
return self._uxopen2(path, needtype, fid, qid, omode_byte, False) | |||||
# Only used if using dot-L, but make sure it's always provided | |||||
# since this is generic. | |||||
if gid is None: | |||||
raise ValueError('gid is required when creating file or dir') | |||||
if len(components) > 1: | |||||
# Look up all but last component; this part must succeed. | |||||
fid, _ = self.lookup(startdir, components[:-1]) | |||||
# Now proceed with the final component, using fid | |||||
# as the start dir. Remember to clunk it! | |||||
startdir = fid | |||||
clunk_startdir = True | |||||
components = components[-1:] | |||||
else: | |||||
# Use startdir as the start dir, and get a new fid. | |||||
# Do not clunk startdir! | |||||
clunk_startdir = False | |||||
fid = self.alloc_fid() | |||||
# Now look up the (single) component. If this fails, | |||||
# assume the file or directory needs to be created. | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Twalk(tag=tag, fid=startdir, newfid=fid, | |||||
nwname=1, wname=components) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if isinstance(resp, protocol.rrd.Rwalk): | |||||
if clunk_startdir: | |||||
self.clunk(startdir, ignore_error=True) | |||||
# fid successfully walked to refer to final component. | |||||
# Just need to actually open the file. | |||||
self.setpath(fid, _pathcat(self.getpath(startdir), components[0])) | |||||
qid = resp.wqid[0] | |||||
return self._uxopen2(needtype, fid, qid, omode_byte, False) | |||||
# Walk failed. If we allocated a fid, retire it. Then set | |||||
# up a fid that points to the parent directory in which to | |||||
# create the file or directory. Note that if we're creating | |||||
# a file, this fid will get changed so that it points to the | |||||
# file instead of the directory, but if we're creating a | |||||
# directory, it will be unchanged. | |||||
if fid != startdir: | |||||
self.retire_fid(fid) | |||||
fid = self.dupfid(startdir) | |||||
try: | |||||
qid, iounit = self._uxcreate(filetype, fid, components[0], | |||||
oflags, omode_byte, perm, gid) | |||||
# Success. If we created an ordinary file, we have everything | |||||
# now as create alters the incoming (dir) fid to open the file. | |||||
# Otherwise (mkdir), we need to open the file, as with | |||||
# a successful lookup. | |||||
# | |||||
# Note that qid type should match "needtype". | |||||
if filetype != 'dir': | |||||
if qid.type == needtype: | |||||
return fid, qid, iounit, True | |||||
self.clunk(fid, ignore_error=True) | |||||
raise OSError(_wrong_file_type(qid), | |||||
'{0}: server told to create {1} but ' | |||||
'created {2} instead'.format(path, | |||||
qt2n(needtype), | |||||
qt2n(qid.type))) | |||||
# Success: created dir; but now need to walk to and open it. | |||||
fid = self.alloc_fid() | |||||
tag = self.get_tag() | |||||
pkt = self.proto.Twalk(tag=tag, fid=startdir, newfid=fid, | |||||
nwname=1, wname=components) | |||||
super(P9Client, self).write(pkt) | |||||
resp = self.wait_for(tag) | |||||
if not isinstance(resp, protocol.rrd.Rwalk): | |||||
self.clunk(fid, ignore_error=True) | |||||
raise OSError(errno.ENOENT, | |||||
'{0}: server made dir but then failed to ' | |||||
'find it again'.format(path)) | |||||
self.setpath(fid, _pathcat(self.getpath(fid), components[0])) | |||||
return self._uxopen2(needtype, fid, qid, omode_byte, True) | |||||
finally: | |||||
# Regardless of success/failure/exception, make sure | |||||
# we clunk startdir if needed. | |||||
if clunk_startdir: | |||||
self.clunk(startdir, ignore_error=True) | |||||
def _uxcreate(self, filetype, fid, name, oflags, omode_byte, perm, gid): | |||||
""" | |||||
Helper for creating dir-or-file. The fid argument is the | |||||
parent directory on input, but will point to the file (if | |||||
we're creating a file) on return. oflags only applies if | |||||
we're creating a file (even then we use omode_byte if we | |||||
are using the plan9 create op). | |||||
""" | |||||
# Try to create or mkdir as appropriate. | |||||
if self.supports_all(protocol.td.Tlcreate, protocol.td.Tmkdir): | |||||
# Use Linux style create / mkdir. | |||||
if filetype == 'dir': | |||||
if perm is None: | |||||
perm = 0o777 | |||||
return self.mkdir(startdir, name, perm, gid), None | |||||
if perm is None: | |||||
perm = 0o666 | |||||
lflags = flags_to_linux_flags(oflags) | |||||
return self.lcreate(fid, name, lflags, perm, gid) | |||||
if filetype == 'dir': | |||||
if perm is None: | |||||
perm = protocol.td.DMDIR | 0o777 | |||||
else: | |||||
perm |= protocol.td.DMDIR | |||||
else: | |||||
if perm is None: | |||||
perm = 0o666 | |||||
return self.create(fid, name, perm, omode_byte) | |||||
def _uxopen2(self, needtype, fid, qid, omode_byte, didcreate): | |||||
"common code for finishing up uxopen" | |||||
if qid.type != needtype: | |||||
self.clunk(fid, ignore_error=True) | |||||
raise OSError(_wrong_file_type(qid), | |||||
'{0}: is {1}, expected ' | |||||
'{2}'.format(path, qt2n(qid.type), qt2n(needtype))) | |||||
qid, iounit = self.open(fid, omode_byte) | |||||
# ? should we re-check qid? it should not have changed | |||||
return fid, qid, iounit, didcreate | |||||
def uxmkdir(self, path, perm, gid, startdir=None): | |||||
""" | |||||
Unix-style mkdir. | |||||
The gid is only applied if we are using .L style mkdir. | |||||
""" | |||||
components, startdir = self._pathsplit(path, startdir) | |||||
clunkme = None | |||||
if len(components) > 1: | |||||
fid, _ = self.lookup(startdir, components[:-1]) | |||||
startdir = fid | |||||
clunkme = fid | |||||
components = components[-1:] | |||||
try: | |||||
if self.supports(protocol.td.Tmkdir): | |||||
qid = self.mkdir(startdir, components[0], perm, gid) | |||||
else: | |||||
qid, _ = self.create(startdir, components[0], | |||||
protocol.td.DMDIR | perm, | |||||
protocol.td.OREAD) | |||||
# Should we chown/chgrp the dir? | |||||
finally: | |||||
if clunkme: | |||||
self.clunk(clunkme, ignore_error=True) | |||||
return qid | |||||
def uxreaddir(self, path, startdir=None, no_dotl=False): | |||||
""" | |||||
Read a directory to get a list of names (which may or may not | |||||
include '.' and '..'). | |||||
If no_dotl is True (or anything non-false-y), this uses the | |||||
plain or .u readdir format, otherwise it uses dot-L readdir | |||||
if possible. | |||||
""" | |||||
components, startdir = self._pathsplit(path, startdir, allow_empty=True) | |||||
fid, qid = self.lookup_last(startdir, components) | |||||
try: | |||||
if qid.type != protocol.td.QTDIR: | |||||
raise OSError(errno.ENOTDIR, | |||||
'{0}: {1}'.format(self.getpathX(fid), | |||||
os.strerror(errno.ENOTDIR))) | |||||
# We need both Tlopen and Treaddir to use Treaddir. | |||||
if not self.supports_all(protocol.td.Tlopen, protocol.td.Treaddir): | |||||
no_dotl = True | |||||
if no_dotl: | |||||
statvals = self.uxreaddir_stat_fid(fid) | |||||
return [i.name for i in statvals] | |||||
dirents = self.uxreaddir_dotl_fid(fid) | |||||
return [dirent.name for dirent in dirents] | |||||
finally: | |||||
self.clunk(fid, ignore_error=True) | |||||
def uxreaddir_stat(self, path, startdir=None): | |||||
""" | |||||
Use directory read to get plan9 style stat data (plain or .u readdir). | |||||
Note that this gets a fid, then opens it, reads, then clunks | |||||
the fid. If you already have a fid, you may want to use | |||||
uxreaddir_stat_fid (but note that this opens, yet does not | |||||
clunk, the fid). | |||||
We return the qid plus the list of the contents. If the | |||||
target is not a directory, the qid will not have type QTDIR | |||||
and the contents list will be empty. | |||||
Raises OSError if this is applied to a non-directory. | |||||
""" | |||||
components, startdir = self._pathsplit(path, startdir) | |||||
fid, qid = self.lookup_last(startdir, components) | |||||
try: | |||||
if qid.type != protocol.td.QTDIR: | |||||
raise OSError(errno.ENOTDIR, | |||||
'{0}: {1}'.format(self.getpathX(fid), | |||||
os.strerror(errno.ENOTDIR))) | |||||
statvals = self.ux_readdir_stat_fid(fid) | |||||
return qid, statvals | |||||
finally: | |||||
self.clunk(fid, ignore_error=True) | |||||
def uxreaddir_stat_fid(self, fid): | |||||
""" | |||||
Implement readdir loop that extracts stat values. | |||||
This opens, but does not clunk, the given fid. | |||||
Unlike uxreaddir_stat(), if this is applied to a file, | |||||
rather than a directory, it just returns no entries. | |||||
""" | |||||
statvals = [] | |||||
qid, iounit = self.open(fid, protocol.td.OREAD) | |||||
# ?? is a zero iounit allowed? if so, what do we use here? | |||||
if qid.type == protocol.td.QTDIR: | |||||
if iounit <= 0: | |||||
iounit = 512 # probably good enough | |||||
offset = 0 | |||||
while True: | |||||
bstring = self.read(fid, offset, iounit) | |||||
if bstring == b'': | |||||
break | |||||
statvals.extend(self.decode_stat_objects(bstring)) | |||||
offset += len(bstring) | |||||
return statvals | |||||
def uxreaddir_dotl_fid(self, fid): | |||||
""" | |||||
Implement readdir loop that uses dot-L style dirents. | |||||
This opens, but does not clunk, the given fid. | |||||
If applied to a file, the lopen should fail, because of the | |||||
L_O_DIRECTORY flag. | |||||
""" | |||||
dirents = [] | |||||
qid, iounit = self.lopen(fid, protocol.td.OREAD | | |||||
protocol.td.L_O_DIRECTORY) | |||||
# ?? is a zero iounit allowed? if so, what do we use here? | |||||
# but, we want a minimum of over 256 anyway, let's go for 512 | |||||
if iounit < 512: | |||||
iounit = 512 | |||||
offset = 0 | |||||
while True: | |||||
bstring = self.readdir(fid, offset, iounit) | |||||
if bstring == b'': | |||||
break | |||||
ents = self.decode_readdir_dirents(bstring) | |||||
if len(ents) == 0: | |||||
break # ??? | |||||
dirents.extend(ents) | |||||
offset = ents[-1].offset | |||||
return dirents | |||||
def uxremove(self, path, startdir=None, filetype=None, | |||||
force=False, recurse=False): | |||||
""" | |||||
Implement rm / rmdir, with optional -rf. | |||||
if filetype is None, remove dir or file. If 'dir' or 'file' | |||||
remove only if it's one of those. If force is set, ignore | |||||
failures to remove. If recurse is True, remove contents of | |||||
directories (recursively). | |||||
File type mismatches (when filetype!=None) raise OSError (?). | |||||
""" | |||||
components, startdir = self._pathsplit(path, startdir, allow_empty=True) | |||||
# Look up all components. If | |||||
# we get an error we'll just assume the file does not | |||||
# exist (is this good?). | |||||
try: | |||||
fid, qid = self.lookup_last(startdir, components) | |||||
except RemoteError: | |||||
return | |||||
if qid.type == protocol.td.QTDIR: | |||||
# it's a directory, remove only if allowed. | |||||
# Note that we must check for "rm -r /" (len(components)==0). | |||||
if filetype == 'file': | |||||
self.clunk(fid, ignore_error=True) | |||||
raise OSError(_wrong_file_type(qid), | |||||
'{0}: is dir, expected file'.format(path)) | |||||
isroot = len(components) == 0 | |||||
closer = self.clunk if isroot else self.remove | |||||
if recurse: | |||||
# NB: _rm_recursive does not clunk fid | |||||
self._rm_recursive(fid, filetype, force) | |||||
# This will fail if the directory is non-empty, unless of | |||||
# course we tell it to ignore error. | |||||
closer(fid, ignore_error=force) | |||||
return | |||||
# Not a directory, call it a file (even if socket or fifo etc). | |||||
if filetype == 'dir': | |||||
self.clunk(fid, ignore_error=True) | |||||
raise OSError(_wrong_file_type(qid), | |||||
'{0}: is file, expected dir'.format(path)) | |||||
self.remove(fid, ignore_error=force) | |||||
def _rm_file_by_dfid(self, dfid, name, force=False): | |||||
""" | |||||
Remove a file whose name is <name> (no path, just a component | |||||
name) whose parent directory is <dfid>. We may assume that the | |||||
file really is a file (or a socket, or fifo, or some such, but | |||||
definitely not a directory). | |||||
If force is set, ignore failures. | |||||
""" | |||||
# If we have unlinkat, that's the fast way. But it may | |||||
# return an ENOTSUP error. If it does we shouldn't bother | |||||
# doing this again. | |||||
if self.supports(protocol.td.Tunlinkat): | |||||
try: | |||||
self.unlinkat(dfid, name, 0) | |||||
return | |||||
except RemoteError as err: | |||||
if not err.is_ENOTSUP(): | |||||
raise | |||||
self.unsupported(protocol.td.Tunlinkat) | |||||
# fall through to remove() op | |||||
# Fall back to lookup + remove. | |||||
try: | |||||
fid, qid = self.lookup_last(dfid, [name]) | |||||
except RemoteError: | |||||
# If this has an errno we could tell ENOENT from EPERM, | |||||
# and actually raise an error for the latter. Should we? | |||||
return | |||||
self.remove(fid, ignore_error=force) | |||||
def _rm_recursive(self, dfid, filetype, force): | |||||
""" | |||||
Recursively remove a directory. filetype is probably None, | |||||
but if it's 'dir' we fail if the directory contains non-dir | |||||
files. | |||||
If force is set, ignore failures. | |||||
Although we open dfid (via the readdir.*_fid calls) we | |||||
do not clunk it here; that's the caller's job. | |||||
""" | |||||
# first, remove contents | |||||
if self.supports_all(protocol.td.Tlopen, protocol.td.Treaddir): | |||||
for entry in self.uxreaddir_dotl_fid(dfid): | |||||
if entry.name in (b'.', b'..'): | |||||
continue | |||||
fid, qid = self.lookup(dfid, [entry.name]) | |||||
try: | |||||
attrs = self.Tgetattr(fid, protocol.td.GETATTR_MODE) | |||||
if stat.S_ISDIR(attrs.mode): | |||||
self.uxremove(entry.name, dfid, filetype, force, True) | |||||
else: | |||||
self.remove(fid) | |||||
fid = None | |||||
finally: | |||||
if fid is not None: | |||||
self.clunk(fid, ignore_error=True) | |||||
else: | |||||
for statobj in self.uxreaddir_stat_fid(dfid): | |||||
# skip . and .. | |||||
name = statobj.name | |||||
if name in (b'.', b'..'): | |||||
continue | |||||
if statobj.qid.type == protocol.td.QTDIR: | |||||
self.uxremove(name, dfid, filetype, force, True) | |||||
else: | |||||
self._rm_file_by_dfid(dfid, name, force) | |||||
def _wrong_file_type(qid): | |||||
"return EISDIR or ENOTDIR for passing to OSError" | |||||
if qid.type == protocol.td.QTDIR: | |||||
return errno.EISDIR | |||||
return errno.ENOTDIR | |||||
def flags_to_linux_flags(flags): | |||||
""" | |||||
Convert OS flags (O_CREAT etc) to Linux flags (protocol.td.L_O_CREAT etc). | |||||
""" | |||||
flagmap = { | |||||
os.O_CREAT: protocol.td.L_O_CREAT, | |||||
os.O_EXCL: protocol.td.L_O_EXCL, | |||||
os.O_NOCTTY: protocol.td.L_O_NOCTTY, | |||||
os.O_TRUNC: protocol.td.L_O_TRUNC, | |||||
os.O_APPEND: protocol.td.L_O_APPEND, | |||||
os.O_DIRECTORY: protocol.td.L_O_DIRECTORY, | |||||
} | |||||
result = flags & os.O_RDWR | |||||
flags &= ~os.O_RDWR | |||||
for key, value in flagmap.iteritems(): | |||||
if flags & key: | |||||
result |= value | |||||
flags &= ~key | |||||
if flags: | |||||
raise ValueError('untranslated bits 0x{0:x} in os flags'.format(flags)) | |||||
return result |