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