rstyx 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +482 -0
- data/ChangeLog +4 -0
- data/Manifest.txt +17 -0
- data/NEWS +2 -0
- data/README +41 -0
- data/Rakefile +55 -0
- data/examples/readstyxfile.rb +48 -0
- data/lib/rstyx/authmodules.rb +90 -0
- data/lib/rstyx/client.rb +709 -693
- data/lib/rstyx/common.rb +71 -0
- data/lib/rstyx/errors.rb +35 -2
- data/lib/rstyx/messages.rb +377 -1113
- data/lib/rstyx/server.rb +1305 -0
- data/lib/rstyx/version.rb +2 -2
- data/lib/rstyx.rb +11 -4
- data/tests/tc_client.rb +260 -45
- data/tests/tc_message.rb +245 -388
- data/tests/tc_styxservproto.rb +596 -0
- metadata +51 -26
data/lib/rstyx/server.rb
ADDED
@@ -0,0 +1,1305 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
#
|
3
|
+
# Copyright (C) 2005-2007 Rafael Sevilla
|
4
|
+
# This file is part of RStyx
|
5
|
+
#
|
6
|
+
# RStyx is free software; you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Lesser General Public License as
|
8
|
+
# published by the Free Software Foundation; either version 2.1
|
9
|
+
# of the License, or (at your option) any later version.
|
10
|
+
#
|
11
|
+
# RStyx is distributed in the hope that it will be useful, but
|
12
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Lesser General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Lesser General Public
|
17
|
+
# License along with RStyx; if not, write to the Free Software
|
18
|
+
# Foundation, Inc., 51 Franklin St., Fifth Floor, Boston, MA
|
19
|
+
# 02110-1301 USA.
|
20
|
+
#
|
21
|
+
# Styx Server
|
22
|
+
#
|
23
|
+
# To create a Styx server, one has to create an SDirectory object that
|
24
|
+
# acts as the server root, e.g.:
|
25
|
+
#
|
26
|
+
# sd = RStyx::Server::SDirectory.new("/")
|
27
|
+
# sf = RStyx::Server::InMemoryFile.new("test.file")
|
28
|
+
# sf.contents = "hello"
|
29
|
+
# sd << sf
|
30
|
+
# serv = RStyx::Server::TCPServer.new(:bindaddr => "0.0.0.0",
|
31
|
+
# :port => 9876,
|
32
|
+
# :root => sd)
|
33
|
+
# serv.run.join
|
34
|
+
#
|
35
|
+
# Author:: Rafael R. Sevilla (mailto:dido@imperium.ph)
|
36
|
+
# Copyright:: Copyright (c) 2005-2007 Rafael R. Sevilla
|
37
|
+
# License:: GNU Lesser General Public License
|
38
|
+
#
|
39
|
+
# $Id: server.rb 231 2007-08-10 08:43:57Z dido $
|
40
|
+
#
|
41
|
+
require 'thread'
|
42
|
+
require 'monitor'
|
43
|
+
require 'timeout'
|
44
|
+
require 'rubygems'
|
45
|
+
require 'eventmachine'
|
46
|
+
require 'logger'
|
47
|
+
require 'socket'
|
48
|
+
require 'rstyx/common'
|
49
|
+
require 'rstyx/messages'
|
50
|
+
require 'rstyx/errors'
|
51
|
+
|
52
|
+
|
53
|
+
module RStyx
|
54
|
+
module Server
|
55
|
+
|
56
|
+
##
|
57
|
+
# Message receiving module for the Styx server. The server will
|
58
|
+
# assemble all inbound messages
|
59
|
+
#
|
60
|
+
module StyxServerProtocol
|
61
|
+
attr_accessor :sentmessages, :msize, :log, :root
|
62
|
+
|
63
|
+
DEFAULT_MSIZE = 8216
|
64
|
+
|
65
|
+
def post_init
|
66
|
+
@msize = DEFAULT_MSIZE
|
67
|
+
# Buffer for messages received from the client
|
68
|
+
@msgbuffer = ""
|
69
|
+
# Session object for this session
|
70
|
+
@session = Session.new(self)
|
71
|
+
# Conveniences to allow the logger and root to be
|
72
|
+
# more easily accessible from within the mixin.
|
73
|
+
# Try to get the peername if available
|
74
|
+
pname = get_peername()
|
75
|
+
# XXX - We should be using unpack_sockaddr_un for
|
76
|
+
# Unix domain sockets...
|
77
|
+
if pname.nil?
|
78
|
+
@peername = "(unknown peer)"
|
79
|
+
else
|
80
|
+
port, host = Socket.unpack_sockaddr_in(pname)
|
81
|
+
@peername = "#{host}:#{port}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def unbind
|
86
|
+
@log.info("#{@peername} session closed")
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Handle version messages. This handles the version negotiation.
|
91
|
+
# At this point, the only version of the protocol supported is
|
92
|
+
# 9P2000: all other version strings result in an error being
|
93
|
+
# returned to the client. A successful Tversion/Rversion
|
94
|
+
# negotiation results in the protocol_negotiated flag in the
|
95
|
+
# current session becoming true, and all other outstanding I/O
|
96
|
+
# on the session (e.g. opened fids and the like) all removed.
|
97
|
+
#
|
98
|
+
# External methods used:
|
99
|
+
#
|
100
|
+
# Session#reset_session *
|
101
|
+
#
|
102
|
+
def tversion(msg)
|
103
|
+
@cversion = msg.version
|
104
|
+
@cmsize = msg.msize
|
105
|
+
if @cversion != "9P2000"
|
106
|
+
# Unsupported protocol version
|
107
|
+
return(Message::Rerror.new(:ename => "Unsupported protocol version #{@cversion} (must be 9P2000)"))
|
108
|
+
end
|
109
|
+
# Reset the session, which also causes the protocol negotiated
|
110
|
+
# flag in the session to be set to true.
|
111
|
+
@session.reset_session(@cmsize)
|
112
|
+
return(Message::Rversion.new(:version => "9P2000", :msize => @msize))
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Handle auth messages. This should be filled in later,
|
117
|
+
# depending on the auth methods that we decide to support.
|
118
|
+
#
|
119
|
+
def tauth(msg)
|
120
|
+
return(Message::Rerror.new(:ename => "Authentication methods through auth messages are not used."))
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Handle attach messages. Internally, this will result in the
|
125
|
+
# fid passed by the client being associated with the root of the
|
126
|
+
# Styx server's file system. Possible error conditions here:
|
127
|
+
#
|
128
|
+
# 1. The client has not done a version negotiation yet.
|
129
|
+
# 2. The client has provided a fid which it is already using
|
130
|
+
# for something else.
|
131
|
+
#
|
132
|
+
# External methods used:
|
133
|
+
#
|
134
|
+
# Session#version_negotiated? *
|
135
|
+
# Session#has_fid? *
|
136
|
+
# Session#[]= *
|
137
|
+
# SFile#qid (root) *
|
138
|
+
#
|
139
|
+
def tattach(msg)
|
140
|
+
# Do not allow attaches without version negotiation
|
141
|
+
unless @session.version_negotiated?
|
142
|
+
raise StyxException.new("Tversion not seen")
|
143
|
+
end
|
144
|
+
# Check that the supplied fid isn't already used.
|
145
|
+
if @session.has_fid?(msg.fid)
|
146
|
+
raise StyxException.new("fid already in use")
|
147
|
+
end
|
148
|
+
# Associate the fid with the root of the server.
|
149
|
+
@session[msg.fid] = @root
|
150
|
+
return(Message::Rattach.new(:qid => @root.qid))
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# Handle flush messages. The only result of this message
|
155
|
+
# is it causes the server to forget about the tag passed:
|
156
|
+
# any I/O already in progress when the flush message is
|
157
|
+
# received is not actually aborted. This is also the way
|
158
|
+
# JStyx handles it. Unfortunately, these semantics are wrong
|
159
|
+
# from the Inferno manual, viz. flush(5):
|
160
|
+
#
|
161
|
+
# If no response is received before the Rflush, the
|
162
|
+
# flushed transaction is considered to have been cancelled,
|
163
|
+
# and should be treated as though it had never been sent.
|
164
|
+
#
|
165
|
+
# XXX - The current implementation doesn't do this. If a
|
166
|
+
# Twrite is flushed, the write will still occur, but no
|
167
|
+
# response will be sent back (except for some clients, such
|
168
|
+
# as JStyx and RStyx which send the Rflush back to the
|
169
|
+
# flushed transaction). Some means, possibly a session-wide
|
170
|
+
# global transaction lock on server internal state changes
|
171
|
+
# may be necessary to allow flushes of this kind to work.
|
172
|
+
#
|
173
|
+
# External methods used:
|
174
|
+
#
|
175
|
+
# Session#flush_tag *
|
176
|
+
#
|
177
|
+
def tflush(msg)
|
178
|
+
@session.flush_tag(msg.oldtag)
|
179
|
+
return(Message::Rflush.new)
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# Handle walk messages.
|
184
|
+
#
|
185
|
+
# Possible error conditions:
|
186
|
+
#
|
187
|
+
# 1. The client specified more than MAXWELEM path elements in the
|
188
|
+
# walk message.
|
189
|
+
# 2. The client tried to walk to a fid that was already previously
|
190
|
+
# opened.
|
191
|
+
# 3. The client used a newfid not the same as fid, where newfid
|
192
|
+
# is a fid already assigned to some other file on the server.
|
193
|
+
# 4. The client tried to walk to a file which is not a directory.
|
194
|
+
# 5. The client tried to descend the directory tree to a directory
|
195
|
+
# to which execute permission is not available.
|
196
|
+
# 6. The client was unable to walk beyond the root to the file
|
197
|
+
# specified.
|
198
|
+
#
|
199
|
+
# Note that if several parts of the walk managed to succeed, this
|
200
|
+
# method will still return an Rwalk response, but it will NOT
|
201
|
+
# associate newfid with anything.
|
202
|
+
#
|
203
|
+
# External methods used:
|
204
|
+
#
|
205
|
+
# Session#[] *
|
206
|
+
# Session#[]= *
|
207
|
+
# Session#has_fid? *
|
208
|
+
#
|
209
|
+
# SFile#client
|
210
|
+
# SFile#directory?
|
211
|
+
# SFile#name
|
212
|
+
# SFile#atime=
|
213
|
+
# SFile#[]
|
214
|
+
# SFile#qid
|
215
|
+
#
|
216
|
+
#
|
217
|
+
def twalk(msg)
|
218
|
+
if msg.wnames.length > MAXWELEM
|
219
|
+
raise StyxException.new("Too many path elements in Twalk message")
|
220
|
+
end
|
221
|
+
fid = msg.fid
|
222
|
+
# Check that the fid has not already been opened by the client
|
223
|
+
sf = @session[fid]
|
224
|
+
clnt = sf.client(@session, fid)
|
225
|
+
unless clnt.nil?
|
226
|
+
raise StyxException.new("cannot walk to an open fid")
|
227
|
+
end
|
228
|
+
nfid = msg.newfid
|
229
|
+
if nfid != fid
|
230
|
+
# if the original and new fids are different, check that
|
231
|
+
# the new fid isn't already in use.
|
232
|
+
if (@session.has_fid?(nfid))
|
233
|
+
raise StyxException.new("fid already in use")
|
234
|
+
end
|
235
|
+
end
|
236
|
+
rwalk = Message::Rwalk.new(:qids => [])
|
237
|
+
num = 0
|
238
|
+
msg.wnames.each do |n|
|
239
|
+
unless sf.directory?
|
240
|
+
raise StyxException.new("#{sf.name} is not a directory")
|
241
|
+
end
|
242
|
+
# Check file permissions if we're descending
|
243
|
+
if n == ".." && !@session.execute?(sf)
|
244
|
+
raise StyxException.new("#{sf.name}: permission denied")
|
245
|
+
end
|
246
|
+
sf.atime = Time.now
|
247
|
+
sf = sf[n]
|
248
|
+
if sf.nil?
|
249
|
+
# Send an error response if the number of walked elements is 0
|
250
|
+
if num == 0
|
251
|
+
raise StyxException.new("file does not exist")
|
252
|
+
end
|
253
|
+
break
|
254
|
+
end
|
255
|
+
# This allows a client to get a fid representing the directory
|
256
|
+
# at the end of the walk, even if the client does not have
|
257
|
+
# execute permissions on that directory. Therefore, in Inferno,
|
258
|
+
# a client could cd into a directory but be unable to read
|
259
|
+
# any of its contents.
|
260
|
+
rwalk.qids << sf.qid
|
261
|
+
sf.refresh
|
262
|
+
num += 1
|
263
|
+
end
|
264
|
+
|
265
|
+
if rwalk.qids.length == msg.wnames.length
|
266
|
+
# The whole walk operation was successful. Associate
|
267
|
+
# the new fid with the returned file.
|
268
|
+
@session[nfid] = sf
|
269
|
+
end
|
270
|
+
|
271
|
+
return(rwalk)
|
272
|
+
end
|
273
|
+
|
274
|
+
##
|
275
|
+
# Handle open messages.
|
276
|
+
#
|
277
|
+
# External methods used:
|
278
|
+
#
|
279
|
+
# Session#[]
|
280
|
+
# Session#confirm_open
|
281
|
+
# SFile#add_client
|
282
|
+
# SFile#set_mtime
|
283
|
+
# SFile#qid
|
284
|
+
# Session#iounit
|
285
|
+
# Session#user
|
286
|
+
#
|
287
|
+
def topen(msg)
|
288
|
+
sf = @session[msg.fid]
|
289
|
+
mode = msg.mode
|
290
|
+
@session.confirm_open(sf, mode)
|
291
|
+
sf.add_client(SFileClient.new(@session, msg.fid, mode))
|
292
|
+
if mode & OTRUNC == OTRUNC
|
293
|
+
sf.set_mtime(Time.now, @session.user)
|
294
|
+
end
|
295
|
+
return(Message::Ropen.new(:qid => sf.qid, :iounit => @session.iounit))
|
296
|
+
end
|
297
|
+
|
298
|
+
##
|
299
|
+
# Handle tcreate messages
|
300
|
+
def tcreate(msg)
|
301
|
+
dir = @session[msg.fid]
|
302
|
+
unless dir.directory?
|
303
|
+
raise StyxException.new("can't create a file inside another file")
|
304
|
+
end
|
305
|
+
|
306
|
+
unless @session.writable?(dir)
|
307
|
+
raise StyxException.new("permission denied, no write permissions to parent directory")
|
308
|
+
end
|
309
|
+
|
310
|
+
# Create the file in the directory. Note that SDirectory#newfile
|
311
|
+
# has to do all of the permission checking and all that.
|
312
|
+
new_file = dir.newfile(msg.name, msg.perm)
|
313
|
+
dir << new_file
|
314
|
+
@session[msg.fid] = new_file
|
315
|
+
new_file.add_client(SFileClient.new(@session, msg.fid, msg.mode))
|
316
|
+
return(Message::Rcreate.new(:qid => new_file.qid,
|
317
|
+
:iounit => @session.iounit))
|
318
|
+
end
|
319
|
+
|
320
|
+
##
|
321
|
+
# Handle reads
|
322
|
+
#
|
323
|
+
def tread(msg)
|
324
|
+
sf = @session[msg.fid]
|
325
|
+
# Check if the file is open for reading
|
326
|
+
clnt = sf.client(@session, msg.fid)
|
327
|
+
if clnt.nil? || !clnt.readable?
|
328
|
+
raise StyxException.new("file is not open for reading")
|
329
|
+
end
|
330
|
+
|
331
|
+
if msg.count > @session.iounit
|
332
|
+
raise StyxException.new("cannot request more than #{@session.iounit} bytes in a single read")
|
333
|
+
end
|
334
|
+
|
335
|
+
return(sf.read(clnt, msg.offset, msg.count))
|
336
|
+
end
|
337
|
+
|
338
|
+
##
|
339
|
+
# Handle writes
|
340
|
+
#
|
341
|
+
def twrite(msg)
|
342
|
+
sf = @session[msg.fid]
|
343
|
+
# Check that the file is open for writing
|
344
|
+
clnt = sf.client(@session, msg.fid)
|
345
|
+
if (clnt.nil? || !clnt.writable?)
|
346
|
+
raise StyxException.new("file is not open for writing")
|
347
|
+
end
|
348
|
+
if msg.data.length > @session.iounit
|
349
|
+
raise StyxException.new("cannot write more than #{@session.iounit} bytes in a single operation")
|
350
|
+
end
|
351
|
+
truncate = clnt.truncate?
|
352
|
+
ofs = msg.offset
|
353
|
+
# If this is an append-only file we ignore the specified offset
|
354
|
+
# and just write to the end of the file, without truncation.
|
355
|
+
# This relies on the SFile#length method returning an accurate
|
356
|
+
# value.
|
357
|
+
if sf.appendonly?
|
358
|
+
ofs = sf.length
|
359
|
+
truncate = false
|
360
|
+
end
|
361
|
+
|
362
|
+
return(sf.write(clnt, ofs, msg.data, truncate))
|
363
|
+
end
|
364
|
+
|
365
|
+
##
|
366
|
+
# Handle clunk messages.
|
367
|
+
#
|
368
|
+
def tclunk(msg)
|
369
|
+
@session.clunk(msg.fid)
|
370
|
+
return(Message::Rclunk.new)
|
371
|
+
end
|
372
|
+
|
373
|
+
##
|
374
|
+
# Handle remove messages.
|
375
|
+
#
|
376
|
+
def tremove(msg)
|
377
|
+
# A remove is just like a clunk with the side effect of
|
378
|
+
# removing the file if the permissions allow.
|
379
|
+
sf = @session[msg.fid]
|
380
|
+
sf.lock do
|
381
|
+
@session.clunk(msg.fid)
|
382
|
+
parent = sf.parent
|
383
|
+
if @session.writable?(parent)
|
384
|
+
raise StyxException.new("permission denied")
|
385
|
+
end
|
386
|
+
|
387
|
+
if sf.instance_of?(SDirectory) && sf.child_count != 0
|
388
|
+
raise StyxException.new("directory not empty")
|
389
|
+
end
|
390
|
+
sf.remove
|
391
|
+
end
|
392
|
+
parent.set_mtime(Time.now, @session.user)
|
393
|
+
return(Message::Rremove.new)
|
394
|
+
end
|
395
|
+
|
396
|
+
##
|
397
|
+
# Handle stat messages
|
398
|
+
#
|
399
|
+
def tstat(msg)
|
400
|
+
sf = @session[msg.fid]
|
401
|
+
# Stat requests require no special permissions
|
402
|
+
return(Message::Rstat.new(:stat => sf.stat))
|
403
|
+
end
|
404
|
+
|
405
|
+
##
|
406
|
+
# Handle wstat messages
|
407
|
+
#
|
408
|
+
def twstat(msg)
|
409
|
+
nstat = msg.stat
|
410
|
+
sf = @session[msg.fid]
|
411
|
+
sf.lock do
|
412
|
+
# Check if we are changing the file's name
|
413
|
+
unless nstat.name.empty?
|
414
|
+
dir = sf.parent
|
415
|
+
unless @session.writable?(dir)
|
416
|
+
raise StyxException.new("write permissions required on parent directory to change file name")
|
417
|
+
end
|
418
|
+
unless dir.has_child?(nstat.name)
|
419
|
+
raise StyxException.new("cannot rename file to the name of an existing file")
|
420
|
+
end
|
421
|
+
sf.can_setname?(nstat.name)
|
422
|
+
end
|
423
|
+
|
424
|
+
# Check if we are changing the length of a file
|
425
|
+
if nstat.size != -1
|
426
|
+
# Check if we have write permission on the file
|
427
|
+
if @session.writable?(sf)
|
428
|
+
raise StyxException.new("write permissions required to change file length")
|
429
|
+
end
|
430
|
+
sf.can_setlength?(sf.size)
|
431
|
+
end
|
432
|
+
|
433
|
+
# Check if we are changing the mode of a file
|
434
|
+
if nstat.mode != MAXUINT
|
435
|
+
# Must be the file owner to change the file mode
|
436
|
+
if sf.uid != @session.user
|
437
|
+
raise StyxException.new("must be owner to change file mode")
|
438
|
+
end
|
439
|
+
|
440
|
+
# Can't change the directory bit
|
441
|
+
if ((nstat.mode & DMDIR == DMDIR) != sf.directory?)
|
442
|
+
raise StyxException.new("can't change a file to a directory")
|
443
|
+
end
|
444
|
+
sf.can_setmode?(nstat.mode)
|
445
|
+
end
|
446
|
+
|
447
|
+
# Check if we are changing the last modification time of a file
|
448
|
+
if nstat.mtime != MAXUINT
|
449
|
+
# Must be owner
|
450
|
+
if sf.uid != @session.user
|
451
|
+
raise StyxException.new("must be owner to change mtime")
|
452
|
+
end
|
453
|
+
sf.can_setmtime?(nstat.mtime)
|
454
|
+
end
|
455
|
+
|
456
|
+
# Check if we are changing the gid of a file
|
457
|
+
unless nstat.gid.empty?
|
458
|
+
# Disallowed for now
|
459
|
+
raise StyxException.new("can't change gid on this server")
|
460
|
+
end
|
461
|
+
|
462
|
+
# No other types are possible for now
|
463
|
+
unless nstat.dtype == 0xffff
|
464
|
+
raise StyxException.new("can't change type")
|
465
|
+
end
|
466
|
+
|
467
|
+
unless nstat.dev == 0xffffffff
|
468
|
+
raise StyxException.new("can't change dev")
|
469
|
+
end
|
470
|
+
|
471
|
+
unless nstat.qid == Message::Qid.new(0xff, 0xffffffff,
|
472
|
+
0xffffffffffffffff)
|
473
|
+
raise StyxException.new("can't change qid")
|
474
|
+
end
|
475
|
+
|
476
|
+
unless nstat.atime == 0xffffffff
|
477
|
+
raise StyxException.new("can't change atime directly")
|
478
|
+
end
|
479
|
+
|
480
|
+
unless nstat.uid.empty?
|
481
|
+
raise StyxException.new("can't change uid")
|
482
|
+
end
|
483
|
+
|
484
|
+
unless nstat.muid.empty?
|
485
|
+
raise StyxException.new("can't change user who last modified file directly")
|
486
|
+
end
|
487
|
+
|
488
|
+
# Now, all the permissions have been checked, we can actually go
|
489
|
+
# ahead with all the changes
|
490
|
+
unless nstat.name.empty?
|
491
|
+
sf.name = nstat.name
|
492
|
+
end
|
493
|
+
|
494
|
+
if nstat.size != -1
|
495
|
+
sf.length = nstat.length
|
496
|
+
end
|
497
|
+
|
498
|
+
if nstat.mode != MAXUINT
|
499
|
+
sf.mode = nstat.mode
|
500
|
+
end
|
501
|
+
|
502
|
+
if nstat.mtime != MAXUINT
|
503
|
+
sf.mtime = nstat.mtime
|
504
|
+
end
|
505
|
+
|
506
|
+
end
|
507
|
+
|
508
|
+
return(Message::Rwstat.new)
|
509
|
+
end
|
510
|
+
|
511
|
+
##
|
512
|
+
# Send a reply back to the peer
|
513
|
+
def reply(msg, tag)
|
514
|
+
# Check if the tag is still available. If it has been
|
515
|
+
# flushed, don't send the reply.
|
516
|
+
if @session.has_tag?(tag)
|
517
|
+
msg.tag = tag
|
518
|
+
@log.debug("#{@peername} << #{msg.to_s}")
|
519
|
+
send_data(msg.to_bytes)
|
520
|
+
@session.release_tag(tag)
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
##
|
525
|
+
# Process a StyxMessage.
|
526
|
+
#
|
527
|
+
def process_styxmsg(msg)
|
528
|
+
begin
|
529
|
+
tag = msg.tag
|
530
|
+
@session.add_tag(tag)
|
531
|
+
# call the appropriate handler method based on the name
|
532
|
+
# of the StyxMessage subclass. These methods should either
|
533
|
+
# return a normal response, or raise an exception of
|
534
|
+
# some sort that (usually) gets turned by this block into
|
535
|
+
# an Rerror response based on the exception's message.
|
536
|
+
pname = msg.class.name.split("::")[-1].downcase.intern
|
537
|
+
resp = self.send(pname, msg)
|
538
|
+
if resp.nil?
|
539
|
+
raise StyxException.new("internal error: empty reply")
|
540
|
+
end
|
541
|
+
reply(resp, tag)
|
542
|
+
rescue TagInUseException => e
|
543
|
+
# In this case, we can't reply with an error to the client,
|
544
|
+
# since the tag used was invalid! If debug level is high
|
545
|
+
# enough, simply print out an error.
|
546
|
+
@log.error("#{@peername} #{e.to_s} #{msg.to_s}")
|
547
|
+
rescue FidNotFoundException => e
|
548
|
+
@log.error("#{@peername} unknown fid in message #{msg.to_s}")
|
549
|
+
reply(Message::Rerror.new(:ename => "Unknown fid #{e.fid}"), tag)
|
550
|
+
rescue StyxException => e
|
551
|
+
@log.error("#{@peername} styx exception #{e.message} in #{msg.to_s}")
|
552
|
+
reply(Message::Rerror.new(:ename => "Error: #{e.message}"), tag)
|
553
|
+
end
|
554
|
+
|
555
|
+
end
|
556
|
+
|
557
|
+
|
558
|
+
##
|
559
|
+
# Receive data from the network connection, called by EventMachine.
|
560
|
+
#
|
561
|
+
def receive_data(data)
|
562
|
+
@msgbuffer << data
|
563
|
+
# self.class.log.debug(" << #{data.unpack("H*").inspect}")
|
564
|
+
while @msgbuffer.length > 4
|
565
|
+
length = @msgbuffer.unpack("V")[0]
|
566
|
+
# Break out if there is not enough data in the message
|
567
|
+
# buffer to construct a message.
|
568
|
+
if @msgbuffer.length < length
|
569
|
+
break
|
570
|
+
end
|
571
|
+
|
572
|
+
# Decode the received data
|
573
|
+
message, @msgbuffer = @msgbuffer.unpack("a#{length}a*")
|
574
|
+
styxmsg = Message::StyxMessage.from_bytes(message)
|
575
|
+
@log.debug("#{@peername} >> #{styxmsg.to_s}")
|
576
|
+
process_styxmsg(styxmsg)
|
577
|
+
|
578
|
+
# after all this is done, there may still be enough data in
|
579
|
+
# the message buffer for more messages so keep looping.
|
580
|
+
end
|
581
|
+
# If we get here, we don't have enough data in the buffer to
|
582
|
+
# build a new message, so we just have to wait until there is
|
583
|
+
# enough.
|
584
|
+
end
|
585
|
+
|
586
|
+
end
|
587
|
+
|
588
|
+
##
|
589
|
+
# Session state of a Styx connection.
|
590
|
+
#
|
591
|
+
class Session < Monitor
|
592
|
+
|
593
|
+
attr_accessor :msize, :auth, :fids, :tags, :version_negotiated, :user
|
594
|
+
attr_accessor :iounit
|
595
|
+
|
596
|
+
def initialize(conn)
|
597
|
+
@conn = conn
|
598
|
+
@version_negotiated = false
|
599
|
+
@msize = 0
|
600
|
+
@user = nil
|
601
|
+
@auth = false
|
602
|
+
@fids = {}
|
603
|
+
@tags = []
|
604
|
+
end
|
605
|
+
|
606
|
+
def version_negotiated?
|
607
|
+
return(@version_negotiated)
|
608
|
+
end
|
609
|
+
|
610
|
+
def reset_session(msize)
|
611
|
+
# XXX: clunk all outstanding fids and release all outstanding tags
|
612
|
+
@version_negotiated = true
|
613
|
+
@iounit = msize
|
614
|
+
end
|
615
|
+
|
616
|
+
##
|
617
|
+
# Associates a FID with a file. The FID passed must be checked before
|
618
|
+
# using this or the old FID will be forgotten.
|
619
|
+
#
|
620
|
+
def []=(fid, file)
|
621
|
+
@fids.delete(fid)
|
622
|
+
@fids[fid] = file
|
623
|
+
end
|
624
|
+
|
625
|
+
##
|
626
|
+
# Gets the file associated with the indexed FID. Raises a
|
627
|
+
# FidNotFoundException if the fid is not present.
|
628
|
+
#
|
629
|
+
def [](fid)
|
630
|
+
unless has_fid?(fid)
|
631
|
+
raise FidNotFoundException.new(fid)
|
632
|
+
end
|
633
|
+
return(@fids[fid])
|
634
|
+
end
|
635
|
+
|
636
|
+
def has_fid?(fid)
|
637
|
+
return(@fids.has_key?(fid))
|
638
|
+
end
|
639
|
+
|
640
|
+
def clunk(fid)
|
641
|
+
unless @fids.has_key?(fid)
|
642
|
+
raise FidNotFoundException.new(fid)
|
643
|
+
end
|
644
|
+
sf = self[fid]
|
645
|
+
sf.synchronize do
|
646
|
+
# Get the client using this fid, and see whether the file
|
647
|
+
# is requested to be deleted on clunk.
|
648
|
+
sfc = sf.client(self, fid)
|
649
|
+
if (!sfc.nil? && sfc.orclose?)
|
650
|
+
begin
|
651
|
+
sf.remove
|
652
|
+
rescue Exception => e
|
653
|
+
# if there was a problem removing the file, ignore it
|
654
|
+
end
|
655
|
+
sf.remove_client(sfc)
|
656
|
+
end
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
660
|
+
def clunk_all
|
661
|
+
@fids.each_key do |k|
|
662
|
+
begin
|
663
|
+
clunk(k)
|
664
|
+
rescue FidNotFoundException => e
|
665
|
+
# ignore this as we are closing down anyway...
|
666
|
+
end
|
667
|
+
end
|
668
|
+
end
|
669
|
+
|
670
|
+
def has_tag?(tag)
|
671
|
+
return(!@tags.index(tag).nil?)
|
672
|
+
end
|
673
|
+
|
674
|
+
##
|
675
|
+
# Adds the given tag to the list of tags in use, first checking to
|
676
|
+
# see if it is already in use. Raises a TagInUseException if the
|
677
|
+
# tag is already in use.
|
678
|
+
#
|
679
|
+
def add_tag(tag)
|
680
|
+
if has_tag?(tag)
|
681
|
+
raise TagInUseException.new(tag)
|
682
|
+
end
|
683
|
+
@tags << tag
|
684
|
+
end
|
685
|
+
|
686
|
+
alias << add_tag
|
687
|
+
|
688
|
+
##
|
689
|
+
# Called when a message is replied to, releasing the tag.
|
690
|
+
def release_tag(tag)
|
691
|
+
@tags.delete(tag)
|
692
|
+
end
|
693
|
+
|
694
|
+
alias flush_tag release_tag
|
695
|
+
|
696
|
+
def flush_all
|
697
|
+
@tags.each do |f|
|
698
|
+
flush_tag(t)
|
699
|
+
end
|
700
|
+
end
|
701
|
+
|
702
|
+
##
|
703
|
+
# Check the permissions for a given mode
|
704
|
+
# +sf+ the file to check against
|
705
|
+
# +mode+ the mode to check (OEXEC, OWRITE, or OREAD)
|
706
|
+
#
|
707
|
+
# XXX: the permissions are only for anonymous access at the
|
708
|
+
# moment, so only the world permissions are ever checked.
|
709
|
+
#
|
710
|
+
def permission?(sf, mode)
|
711
|
+
if mode < 0 || mode > 2
|
712
|
+
raise "Internal error: mode should be 0, 1, or 2"
|
713
|
+
end
|
714
|
+
# We bit shift the permissions value so that the mode is
|
715
|
+
# represented by the last bit (all) the fourth to last bit
|
716
|
+
# (group), and the seventh-to-last bit (user).
|
717
|
+
perms = sf.permissions >> mode
|
718
|
+
# Check permissions for 'all' -- the low-order bit
|
719
|
+
if perms & 0b000_000_001 == 1
|
720
|
+
return(true)
|
721
|
+
end
|
722
|
+
|
723
|
+
# XXX: this has to be something more useful!
|
724
|
+
return(false)
|
725
|
+
end
|
726
|
+
|
727
|
+
##
|
728
|
+
# Check for executable permission for the styx file
|
729
|
+
#
|
730
|
+
def execute?(sf)
|
731
|
+
end
|
732
|
+
|
733
|
+
##
|
734
|
+
# Checks that the given file can be opened with the given mode.
|
735
|
+
# Raises a StyxException if this is not possible.
|
736
|
+
#
|
737
|
+
def confirm_open(sf, mode)
|
738
|
+
if sf.exclusive? && sf.num_clients != 0
|
739
|
+
raise StyxException.new("can't open locked file")
|
740
|
+
end
|
741
|
+
openmode = mode & 0x03
|
742
|
+
# XXX: Needs to be filled out further...
|
743
|
+
end
|
744
|
+
|
745
|
+
end # class Session
|
746
|
+
|
747
|
+
class Server
|
748
|
+
def initialize(config)
|
749
|
+
@root = config[:root]
|
750
|
+
@log = config[:log] || Logger.new(STDERR)
|
751
|
+
@log.level = config[:debug] || Logger::WARN
|
752
|
+
end
|
753
|
+
|
754
|
+
protected
|
755
|
+
|
756
|
+
def start_server
|
757
|
+
end
|
758
|
+
|
759
|
+
public
|
760
|
+
|
761
|
+
##
|
762
|
+
# Start the Styx server, returning the thread of the
|
763
|
+
# running Styx server instance.
|
764
|
+
def run
|
765
|
+
t = Thread.new do
|
766
|
+
@log.info("starting")
|
767
|
+
start_server
|
768
|
+
end
|
769
|
+
return(t)
|
770
|
+
end
|
771
|
+
|
772
|
+
end # class Server
|
773
|
+
|
774
|
+
class TCPServer < Server
|
775
|
+
def initialize(config)
|
776
|
+
@bindaddr = config[:bindaddr]
|
777
|
+
@port = config[:port]
|
778
|
+
super(config)
|
779
|
+
end
|
780
|
+
|
781
|
+
protected
|
782
|
+
def start_server
|
783
|
+
EventMachine::run do
|
784
|
+
@log.info("TCP server on #{@bindaddr}:#{@port}")
|
785
|
+
EventMachine::start_server(@bindaddr, @port,
|
786
|
+
StyxServerProtocol) do |conn|
|
787
|
+
conn.root = @root
|
788
|
+
conn.log = @log
|
789
|
+
end
|
790
|
+
end
|
791
|
+
end
|
792
|
+
end
|
793
|
+
|
794
|
+
|
795
|
+
##
|
796
|
+
# Server's representation of the client of an SFile, created when
|
797
|
+
# a client opens a file.
|
798
|
+
#
|
799
|
+
class SFileClient
|
800
|
+
attr_reader :session, :fid, :mode
|
801
|
+
attr_accessor :offset, :next_file_to_read
|
802
|
+
|
803
|
+
##
|
804
|
+
# Create a new SFileClient.
|
805
|
+
#
|
806
|
+
# +session+:: The session object associated with the client.
|
807
|
+
# +fid+:: The client's handle to the file. Note that clients may
|
808
|
+
# use many fids opened representing the same file.
|
809
|
+
# +mode+:: The mode field as received from the client's Topen
|
810
|
+
# message (including the OTRUNC and ORCLOSE bits)
|
811
|
+
#
|
812
|
+
def initialize(session, fid, mode)
|
813
|
+
@session = session
|
814
|
+
@fid = fid
|
815
|
+
@truncate = ((mode & OTRUNC) == OTRUNC)
|
816
|
+
@orclose = ((mode & ORCLOSE) == ORCLOSE)
|
817
|
+
@mode = mode & 0x03 # mask off all but the last two bits
|
818
|
+
# When a client reads from or writes to file, this records the
|
819
|
+
# new offset
|
820
|
+
@offset = 0
|
821
|
+
# Used when reading a directory: stores the index of the next
|
822
|
+
# child of an SFile to include in an RreadMessage.
|
823
|
+
@next_file_to_read = 0
|
824
|
+
end
|
825
|
+
|
826
|
+
def truncate?
|
827
|
+
return(@truncate)
|
828
|
+
end
|
829
|
+
|
830
|
+
def orclose?
|
831
|
+
return(@orclose)
|
832
|
+
end
|
833
|
+
|
834
|
+
alias delete_on_clunk? orclose?
|
835
|
+
|
836
|
+
##
|
837
|
+
# Check to see if the client can read the file (i.e. the client
|
838
|
+
# opened it with read access mode)
|
839
|
+
#
|
840
|
+
def readable?
|
841
|
+
return(mode == OREAD || mode == ORDWR)
|
842
|
+
end
|
843
|
+
|
844
|
+
##
|
845
|
+
# Check to see if the client can write to the file (i.e. the client
|
846
|
+
# opened it with write access).
|
847
|
+
#
|
848
|
+
def writable?
|
849
|
+
return(mode == OWRITE || mode == ORDWR)
|
850
|
+
end
|
851
|
+
|
852
|
+
end
|
853
|
+
|
854
|
+
##
|
855
|
+
# Class representing a file (or directory) on a Styx server. There
|
856
|
+
# may be different types of file: a file might map directly to a file
|
857
|
+
# on disk, or it may be a synthetic file representing a program
|
858
|
+
# interface. This class creates a Styx file which does nothing useful:
|
859
|
+
# returning errors when reading from or writing to it. Subclasses
|
860
|
+
# should override the SFile#read, SFile#write and SFile#length methods
|
861
|
+
# to implement the desired behavior. Each Styx file has exactly one
|
862
|
+
# parent, the directory which contains it, thus symbolic links on the
|
863
|
+
# underlying operating system cannot be represented.
|
864
|
+
#
|
865
|
+
class SFile < Monitor
|
866
|
+
|
867
|
+
attr_reader :name, :uid, :gid, :muid, :mtime
|
868
|
+
attr_accessor :permissions, :atime, :parent
|
869
|
+
|
870
|
+
##
|
871
|
+
# Create a new file object with the given permissions.
|
872
|
+
# This accepts a hash with the following keys:
|
873
|
+
#
|
874
|
+
# name:: the name of the file
|
875
|
+
# permissions:: The permissions of the file (e.g. 0755 in octal)
|
876
|
+
# apponly:: true if the file is append only
|
877
|
+
# excl:: true if the file is for exclusive use, i.e. only one
|
878
|
+
# client at a time may open.
|
879
|
+
# user:: the username of the owner of the file. If not specified
|
880
|
+
# gets the value from an environment variable.
|
881
|
+
# group:: the group name of the owner of the file. If not specified
|
882
|
+
# gets the value from an environment variable.
|
883
|
+
#
|
884
|
+
#
|
885
|
+
def initialize(name, argv={ :permissions => 0666, :apponly => false,
|
886
|
+
:excl => false, :uid => ENV["USER"],
|
887
|
+
:gid => ENV["GROUP"] })
|
888
|
+
super()
|
889
|
+
if name == "" || name == "." || name == ".."
|
890
|
+
raise StyxException.new("Illegal file name")
|
891
|
+
end
|
892
|
+
# The parent directory of the file.
|
893
|
+
@parent = nil
|
894
|
+
# The name of the file.
|
895
|
+
@name = name
|
896
|
+
# True if this is a directory
|
897
|
+
@directory = false
|
898
|
+
# True if this is an append-only file
|
899
|
+
@appendonly = argv[:apponly]
|
900
|
+
# True if this file may be opened by only one client at a time
|
901
|
+
@exclusive = argv[:excl]
|
902
|
+
# True if this is a file to be used by the authentication mechanism (normally false)
|
903
|
+
@auth = false
|
904
|
+
# Permissions represented as a number, e.g. 0755 in octal
|
905
|
+
@permissions = argv[:permissions]
|
906
|
+
# Version number of the file, incremented whenever the file is
|
907
|
+
# modified
|
908
|
+
@version = 0
|
909
|
+
# Time of creation
|
910
|
+
@ctime = Time.now
|
911
|
+
# Last access time
|
912
|
+
@atime = Time.now
|
913
|
+
# Last modification time
|
914
|
+
@mtime = Time.now
|
915
|
+
# Owner name
|
916
|
+
@uid = argv[:user]
|
917
|
+
# Group name
|
918
|
+
@gid = argv[:group]
|
919
|
+
# User who last modified the file
|
920
|
+
@muid = ""
|
921
|
+
# The clients who have a connection to the file
|
922
|
+
@clients = []
|
923
|
+
@clients.extend(MonitorMixin)
|
924
|
+
end
|
925
|
+
|
926
|
+
##
|
927
|
+
# Check if the name may be changed. Raises a StyxException
|
928
|
+
# if this is not possible.
|
929
|
+
#
|
930
|
+
def can_setname?(name)
|
931
|
+
end
|
932
|
+
|
933
|
+
##
|
934
|
+
# Check if the File is a directory (should always be the same as
|
935
|
+
# Object#instance_of?(Directory).
|
936
|
+
#
|
937
|
+
def directory?
|
938
|
+
return(@directory)
|
939
|
+
end
|
940
|
+
|
941
|
+
##
|
942
|
+
# Check if the file is append-only
|
943
|
+
#
|
944
|
+
def appendonly?
|
945
|
+
return(@appendonly)
|
946
|
+
end
|
947
|
+
|
948
|
+
##
|
949
|
+
# Check if the file is marked as exclusive use
|
950
|
+
#
|
951
|
+
def exclusive?
|
952
|
+
return(@exclusive)
|
953
|
+
end
|
954
|
+
|
955
|
+
##
|
956
|
+
# Check if the file is an authenticator
|
957
|
+
#
|
958
|
+
def auth?
|
959
|
+
return(@auth)
|
960
|
+
end
|
961
|
+
|
962
|
+
##
|
963
|
+
# Get the full path relative to the root of the filesystem.
|
964
|
+
#
|
965
|
+
def full_path
|
966
|
+
if auth? || @parent.nil?
|
967
|
+
return(@name)
|
968
|
+
end
|
969
|
+
return(@parent.full_path + @name)
|
970
|
+
end
|
971
|
+
|
972
|
+
##
|
973
|
+
# Get the length of the file. This default implementation returns
|
974
|
+
# zero: subclasses must override this method.
|
975
|
+
#
|
976
|
+
def length
|
977
|
+
return(0)
|
978
|
+
end
|
979
|
+
|
980
|
+
##
|
981
|
+
# Gets the type of the file as a number representing the OR of DMDIR,
|
982
|
+
# DMAPPEND, DMEXCL, and DMAUTH as appropriate, used to create the Qid.
|
983
|
+
#
|
984
|
+
def filetype
|
985
|
+
type = 0
|
986
|
+
if @directory
|
987
|
+
type |= DMDIR
|
988
|
+
end
|
989
|
+
|
990
|
+
if @appendonly
|
991
|
+
type |= DMAPPEND
|
992
|
+
end
|
993
|
+
|
994
|
+
if @exclusive
|
995
|
+
type |= DMEXCL
|
996
|
+
end
|
997
|
+
|
998
|
+
if @auth
|
999
|
+
type |= DMAUTH
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
return(type)
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
##
|
1006
|
+
# Gets the mode of the file (permissions and flags)
|
1007
|
+
#
|
1008
|
+
def mode
|
1009
|
+
return(self.filetype | @permissions)
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
##
|
1013
|
+
# Checks to see if this file allows the mode (permissions and flags)
|
1014
|
+
# of the file to be changed. This is called when the server receives
|
1015
|
+
# a Twstat message. This default implementation does nothing.
|
1016
|
+
#
|
1017
|
+
# +newmode+: the new mode of the file (permissions plus any other flags
|
1018
|
+
# such as DMDIR, etc.)
|
1019
|
+
#
|
1020
|
+
def can_setmode?(newmode)
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
##
|
1024
|
+
# Sets the mode of the file (permissions plus other flags). Must check
|
1025
|
+
# all the relevant permissions and call SFile#can_setmode? before
|
1026
|
+
# calling this method, as the assumption is that this method will
|
1027
|
+
# always succeed.
|
1028
|
+
def mode=(newmode)
|
1029
|
+
@appendonly = (newmode & DMAPPEND == DMAPPEND)
|
1030
|
+
@exclusive = (newmode & DMEXCL == DMEXCL)
|
1031
|
+
@auth = (newmode & DMAUTH == DMAUTH)
|
1032
|
+
@permissions = newmode & 0x03fff
|
1033
|
+
return(newmode)
|
1034
|
+
end
|
1035
|
+
|
1036
|
+
def qid
|
1037
|
+
t = filetype() >> 24 & 0xff
|
1038
|
+
q = Message::Qid.new(t, @version, self.uuid)
|
1039
|
+
return(q)
|
1040
|
+
end
|
1041
|
+
|
1042
|
+
def stat
|
1043
|
+
s = Message::Stat.new
|
1044
|
+
s.dtype = s.dev = 0
|
1045
|
+
s.qid = self.qid
|
1046
|
+
s.mode = self.mode
|
1047
|
+
s.atime = @atime.to_i
|
1048
|
+
s.mtime = @mtime.to_i
|
1049
|
+
s.length = self.length
|
1050
|
+
s.name = @name
|
1051
|
+
s.uid = @uid
|
1052
|
+
s.gid = @gid
|
1053
|
+
s.muid = @muid
|
1054
|
+
return(s)
|
1055
|
+
end
|
1056
|
+
|
1057
|
+
##
|
1058
|
+
# Check to see if the length of this file can be changed to the
|
1059
|
+
# given value. If this does not throw an exception then
|
1060
|
+
# SFile#length= should always succeed. The default implementation
|
1061
|
+
# always throws an exception; subclasses should override this method
|
1062
|
+
# if they want the length of the file to be changeable.
|
1063
|
+
#
|
1064
|
+
def can_setlength?(newlength)
|
1065
|
+
raise StyxException.new("Cannot change the length of this file directly")
|
1066
|
+
end
|
1067
|
+
|
1068
|
+
##
|
1069
|
+
# Sets the length of the file. The usual disclaimers about permissions
|
1070
|
+
# and SFile#can_setlength? apply. Default implementation does nothing
|
1071
|
+
# and it should be overriden by subclasses.
|
1072
|
+
#
|
1073
|
+
def length=(newlength)
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
def can_setmtime?(nmtime)
|
1077
|
+
end
|
1078
|
+
|
1079
|
+
def set_mtime(nmtime, uid)
|
1080
|
+
@mtime = nmtime
|
1081
|
+
@muid = uid
|
1082
|
+
end
|
1083
|
+
|
1084
|
+
def rename(newname)
|
1085
|
+
if @parent == nil
|
1086
|
+
raise StyxException.new("Cannot change the name of the root directory")
|
1087
|
+
end
|
1088
|
+
if @parent.has_child?(newname)
|
1089
|
+
raise StyxException.new("A file with name #{newname} already exists in this directory")
|
1090
|
+
end
|
1091
|
+
@name = newname
|
1092
|
+
end
|
1093
|
+
|
1094
|
+
# Gets the unique numeric ID for the path of this file (generated from
|
1095
|
+
# the low-order bytes of the creation time and the hashcode of the full
|
1096
|
+
# path). If the file is deleted and re-created the unique ID will
|
1097
|
+
# change (except for the extremely unlikely case in which the low-order
|
1098
|
+
# bytes of the creation time happen to be the same in the new file and
|
1099
|
+
# the old file).
|
1100
|
+
def uuid
|
1101
|
+
tbytes = @ctime.to_i & 0xffffffff
|
1102
|
+
return((self.full_path.hash << 32) | tbytes)
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
##
|
1106
|
+
# Reads data from this file. This method should be overridden by
|
1107
|
+
# subclasses and should return an Rread with the data read. This
|
1108
|
+
# default implementation simply throws a StyxException, which
|
1109
|
+
# results in an Rerror being returned to the client. Subclasses
|
1110
|
+
# should override this to provide the desired behavior when the
|
1111
|
+
# file is read.
|
1112
|
+
#
|
1113
|
+
def read(client, offset, count)
|
1114
|
+
raise StyxException.new("Cannot read from this file")
|
1115
|
+
end
|
1116
|
+
|
1117
|
+
|
1118
|
+
##
|
1119
|
+
# Writes data to this file. This method should be overriden by
|
1120
|
+
# subclasses to provide the desired behavior when the file is
|
1121
|
+
# written to. It should return the number of bytes actually
|
1122
|
+
# "written".
|
1123
|
+
#
|
1124
|
+
def write(client, offset, count, data, truncate)
|
1125
|
+
raise StyxException.new("Cannot write to this file")
|
1126
|
+
end
|
1127
|
+
|
1128
|
+
#
|
1129
|
+
# Remove the file from the Styx server
|
1130
|
+
def remove
|
1131
|
+
self.delete
|
1132
|
+
self.parent.remove_child(self)
|
1133
|
+
end
|
1134
|
+
|
1135
|
+
def delete
|
1136
|
+
end
|
1137
|
+
|
1138
|
+
def add_client(cl)
|
1139
|
+
@clients.synchronize { @clients << cl }
|
1140
|
+
self.client_connected(cl)
|
1141
|
+
end
|
1142
|
+
|
1143
|
+
def client_connected(cl)
|
1144
|
+
end
|
1145
|
+
|
1146
|
+
##
|
1147
|
+
# Get the client connection to this file
|
1148
|
+
#
|
1149
|
+
def client(sess, fid)
|
1150
|
+
@clients.synchronize do
|
1151
|
+
@clients.each do |cl|
|
1152
|
+
if cl.session == sess && cl.fid == fid
|
1153
|
+
return(cl)
|
1154
|
+
end
|
1155
|
+
end
|
1156
|
+
end
|
1157
|
+
return(nil)
|
1158
|
+
end
|
1159
|
+
|
1160
|
+
def num_clients
|
1161
|
+
@clients.synchronize do
|
1162
|
+
remove_dead_clients
|
1163
|
+
return(@clients.length)
|
1164
|
+
end
|
1165
|
+
end
|
1166
|
+
|
1167
|
+
def remove_dead_clients
|
1168
|
+
@clients.synchronize do
|
1169
|
+
@clients.each do |clnt|
|
1170
|
+
if clnt.session.nil? || !clnt.session.connected?
|
1171
|
+
remove_client(clnt)
|
1172
|
+
end
|
1173
|
+
end
|
1174
|
+
end
|
1175
|
+
end
|
1176
|
+
|
1177
|
+
def remove_client(cl)
|
1178
|
+
unless cl.nil?
|
1179
|
+
@clients.delete(cl)
|
1180
|
+
client_disconnected(cl)
|
1181
|
+
end
|
1182
|
+
end
|
1183
|
+
|
1184
|
+
def client_disconnected(cl)
|
1185
|
+
end
|
1186
|
+
|
1187
|
+
def contents_changed
|
1188
|
+
version_incr
|
1189
|
+
end
|
1190
|
+
|
1191
|
+
def version_incr
|
1192
|
+
self.version = ((self.version + 1) & 0xffffffffffffffff)
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
def refresh
|
1196
|
+
end
|
1197
|
+
end # class SFile
|
1198
|
+
|
1199
|
+
class SDirectory < SFile
|
1200
|
+
def initialize(name, argv={ :permissions => 777, :uid => ENV["USER"],
|
1201
|
+
:gid => ENV["GROUP"] })
|
1202
|
+
# directories cannot be append-only, exclusive, or auth files
|
1203
|
+
argv.merge({:apponly => false, :excl => false})
|
1204
|
+
super(name, argv)
|
1205
|
+
@directory = true
|
1206
|
+
@children = []
|
1207
|
+
@children.extend(MonitorMixin)
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
def child_exists?(name)
|
1211
|
+
@children.synchronize do
|
1212
|
+
@children.each do |c|
|
1213
|
+
if c.name == name
|
1214
|
+
return(true)
|
1215
|
+
end
|
1216
|
+
end
|
1217
|
+
return(false)
|
1218
|
+
end
|
1219
|
+
end
|
1220
|
+
|
1221
|
+
##
|
1222
|
+
# Add a child to this directory. If a file with the same name
|
1223
|
+
# already exists, throws a FileExists exception.
|
1224
|
+
#
|
1225
|
+
def <<(child)
|
1226
|
+
@children.synchronize do
|
1227
|
+
if child_exists?(child.name)
|
1228
|
+
raise FileExists("#{sf.name} already exists")
|
1229
|
+
end
|
1230
|
+
child.parent = self
|
1231
|
+
@children << child
|
1232
|
+
end
|
1233
|
+
return(child)
|
1234
|
+
end
|
1235
|
+
|
1236
|
+
##
|
1237
|
+
# Get the child with the name +name+, or nil if no such file
|
1238
|
+
#
|
1239
|
+
def [](name)
|
1240
|
+
if name == "."
|
1241
|
+
return(self)
|
1242
|
+
end
|
1243
|
+
@children.synchronize do
|
1244
|
+
@children.each do |c|
|
1245
|
+
if c.name == name
|
1246
|
+
return(c)
|
1247
|
+
end
|
1248
|
+
end
|
1249
|
+
return(nil)
|
1250
|
+
end
|
1251
|
+
end
|
1252
|
+
|
1253
|
+
##
|
1254
|
+
# Get the number of children this directory has
|
1255
|
+
#
|
1256
|
+
def child_count
|
1257
|
+
return(@children.length)
|
1258
|
+
end
|
1259
|
+
|
1260
|
+
##
|
1261
|
+
# Read the contents of the directory
|
1262
|
+
#
|
1263
|
+
def read(client, offset, count)
|
1264
|
+
# Check that the offset is valid; zero offsets are always valid,
|
1265
|
+
# but non-zero offsets are only valid if this client has read part
|
1266
|
+
# of the contents of the directory before.
|
1267
|
+
if (offset != 0 && offset != client.offset)
|
1268
|
+
raise StyxException.new("invalid offset when reading directory")
|
1269
|
+
end
|
1270
|
+
|
1271
|
+
# Create a string to store the serialized stat representations
|
1272
|
+
# of the directory's contents.
|
1273
|
+
str = ""
|
1274
|
+
nextfile = (offset == 0) ? 0 : client.next_file_to_read
|
1275
|
+
while (nextfile < @children.length)
|
1276
|
+
sf = @children[nextfile]
|
1277
|
+
s = sf.stat.to_bytes
|
1278
|
+
if (s.length + str.length) > count
|
1279
|
+
break
|
1280
|
+
end
|
1281
|
+
# Add the serialized stat to the buffer
|
1282
|
+
str << s
|
1283
|
+
nextfile += 1
|
1284
|
+
end
|
1285
|
+
client.next_file_to_read = nextfile
|
1286
|
+
client.offset += str.length
|
1287
|
+
return(Message::Rread.new(:data => str))
|
1288
|
+
|
1289
|
+
end
|
1290
|
+
|
1291
|
+
end # class SDirectory
|
1292
|
+
|
1293
|
+
class InMemoryFile < SFile
|
1294
|
+
attr_accessor :contents
|
1295
|
+
|
1296
|
+
def read(client, offset, count)
|
1297
|
+
data = @contents[offset..(offset+count)]
|
1298
|
+
data ||= ""
|
1299
|
+
return(Message::Rread.new(:data => data))
|
1300
|
+
end
|
1301
|
+
end
|
1302
|
+
|
1303
|
+
end # module Server
|
1304
|
+
|
1305
|
+
end # module RStyx
|