1789 lines
71 KiB
Python
1789 lines
71 KiB
Python
|
#! /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
|