rstyx 0.1.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/lib/rstyx/client.rb +499 -0
- data/lib/rstyx/errors.rb +40 -0
- data/lib/rstyx/messages.rb +1429 -0
- data/lib/rstyx/version.rb +37 -0
- data/lib/rstyx.rb +23 -0
- data/tests/tc_message.rb +1288 -0
- metadata +52 -0
data/lib/rstyx/client.rb
ADDED
@@ -0,0 +1,499 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
#
|
3
|
+
# Copyright (C) 2005 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., 59 Temple Place, Suite 330, Boston, MA
|
19
|
+
# 02111-1307 USA.
|
20
|
+
#
|
21
|
+
# Styx Client
|
22
|
+
#
|
23
|
+
# Author:: Rafael R. Sevilla (mailto:dido@imperium.ph)
|
24
|
+
# Copyright:: Copyright (c) 2005 Rafael R. Sevilla
|
25
|
+
# License:: GNU Lesser General Public License
|
26
|
+
#
|
27
|
+
# $Id: client.rb,v 1.5 2005/09/30 08:07:46 dido Exp $
|
28
|
+
#
|
29
|
+
|
30
|
+
require 'socket'
|
31
|
+
require 'thread'
|
32
|
+
require 'timeout'
|
33
|
+
require 'rstyx/errors'
|
34
|
+
require 'rstyx/messages'
|
35
|
+
|
36
|
+
module RStyx
|
37
|
+
|
38
|
+
module Client
|
39
|
+
|
40
|
+
##
|
41
|
+
# Utility functions
|
42
|
+
#
|
43
|
+
class Util
|
44
|
+
##
|
45
|
+
# Gets the next unused tag or fid.
|
46
|
+
#
|
47
|
+
# +limit+:: [Fixnum] maximum value
|
48
|
+
# +func+:: [Proc] predicate that determines if tag or fid i is in use
|
49
|
+
# +thing+:: [String] what we're trying to get
|
50
|
+
# return:: an unused value
|
51
|
+
#
|
52
|
+
def Util.get_first_unused(limit, func, thing)
|
53
|
+
found = false
|
54
|
+
val = nil
|
55
|
+
0.upto(limit-1) do |i|
|
56
|
+
if func.call(i)
|
57
|
+
i += 1
|
58
|
+
else
|
59
|
+
val = i
|
60
|
+
break
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if val.nil?
|
65
|
+
# this should probably be impossible
|
66
|
+
raise StyxException.new(sprintf("No more free %s!", thing))
|
67
|
+
end
|
68
|
+
return(val)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# An abstract class for Styx connections. DO NOT INSTANTIATE THIS
|
74
|
+
# CLASS DIRECTLY!
|
75
|
+
#
|
76
|
+
class Connection
|
77
|
+
attr_reader :msize, :version, :rootfid, :user
|
78
|
+
|
79
|
+
TIMEOUT = 60
|
80
|
+
|
81
|
+
##
|
82
|
+
# Create a new Styx connection. If a block is passed, yield the
|
83
|
+
# new connection to the block and do a disconnect when the block
|
84
|
+
# finishes.
|
85
|
+
#
|
86
|
+
# +user+:: [String] the user to connect as
|
87
|
+
#
|
88
|
+
def initialize(user="")
|
89
|
+
@lock = Mutex.new # used to synchronize threads
|
90
|
+
@condvar = ConditionVariable.new
|
91
|
+
@rmessages = Hash.new # filled with rmessages that have arrived
|
92
|
+
@usedfids = Array.new # list of fids currently in use
|
93
|
+
@pendingclunks = Hash.new # outstanding Tclunk messages (tags and fids)
|
94
|
+
@conn = self.startconn # get the connection
|
95
|
+
|
96
|
+
# start message receiving thread
|
97
|
+
@receiver = Thread.new { receive_messages }
|
98
|
+
|
99
|
+
tv = Message::Tversion.new
|
100
|
+
rver = send_message(tv)
|
101
|
+
@msize = rver.msize
|
102
|
+
@version = rver.version
|
103
|
+
@rootfid = get_free_fid # the fid representing the serv. root
|
104
|
+
rattach = send_message(Message::Tattach.new(@rootfid, user))
|
105
|
+
@user = user
|
106
|
+
|
107
|
+
if block_given?
|
108
|
+
begin
|
109
|
+
yield self
|
110
|
+
ensure
|
111
|
+
self.disconnect
|
112
|
+
end
|
113
|
+
else
|
114
|
+
return(self)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
##
|
121
|
+
# This method should be overridden by subclasses, and should return
|
122
|
+
# a subclass of IO (e.g. a TCPSocket) on which the read and write
|
123
|
+
# methods can be used.
|
124
|
+
#
|
125
|
+
def startconn
|
126
|
+
return(nil) # Override me!
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Thread that listens for incoming messages on the socket.
|
131
|
+
#
|
132
|
+
# FIXME: Frankly, I think this is quite ugly...
|
133
|
+
#
|
134
|
+
def receive_messages
|
135
|
+
buffer = ""
|
136
|
+
data = nil
|
137
|
+
done = false
|
138
|
+
loop do
|
139
|
+
begin
|
140
|
+
data = @conn.read(1)
|
141
|
+
rescue Exception => e
|
142
|
+
done = true
|
143
|
+
raise StyxException.new("error reading from socket: #{e.message}")
|
144
|
+
end
|
145
|
+
|
146
|
+
# There may be more than one message (or a partial message)
|
147
|
+
buffer << data
|
148
|
+
if buffer.length >= 4
|
149
|
+
length = buffer[0..3].unpack("V")[0]
|
150
|
+
if buffer.length >= length
|
151
|
+
message = buffer[0..(length-1)]
|
152
|
+
buffer = buffer[length..(buffer.length)]
|
153
|
+
styxmsg = Message::StyxMessage.decode(message)
|
154
|
+
|
155
|
+
# let everyone know there's a new message available
|
156
|
+
@lock.synchronize do
|
157
|
+
@rmessages[styxmsg.tag] = styxmsg
|
158
|
+
@condvar.signal
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# Get a new tag for use in new messages.
|
167
|
+
#
|
168
|
+
def get_free_tag
|
169
|
+
return(Util.get_first_unused(0xffff,
|
170
|
+
lambda {|i| @rmessages.has_key?(i) },
|
171
|
+
"tags"))
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# Send a message and return immediately, returning the message's tag
|
176
|
+
#
|
177
|
+
# +message+:: [StyxMessage] the message to be sent
|
178
|
+
# return:: [Fixnum] the tag number used
|
179
|
+
#
|
180
|
+
def send_message_async(message)
|
181
|
+
if (message.tag.nil?)
|
182
|
+
message.tag = get_free_tag
|
183
|
+
end
|
184
|
+
|
185
|
+
# If this is a Tclunk message we are trying to close the file.
|
186
|
+
# Remember the fid so we can free it from the list of used fids
|
187
|
+
# when we get the Rclunk corresponding to it.
|
188
|
+
if message.mtype == Message::StyxMessage::TCLUNK
|
189
|
+
@pendingclunks[message.tag] = message.fid
|
190
|
+
end
|
191
|
+
|
192
|
+
# puts "sending message #{message.to_s}"
|
193
|
+
@conn.write(message.to_bytes)
|
194
|
+
return(message.tag)
|
195
|
+
end
|
196
|
+
|
197
|
+
##
|
198
|
+
# Waits for the message with the given tag to arrive, then returns it.
|
199
|
+
#
|
200
|
+
# +tag+:: [Fixnum] Tag to wait for
|
201
|
+
# return:: [Message::StyxMessage] the message received
|
202
|
+
#
|
203
|
+
def get_reply(tag)
|
204
|
+
msg = nil
|
205
|
+
@lock.synchronize do
|
206
|
+
# wait for a new message to arrive
|
207
|
+
while @rmessages[tag].nil?
|
208
|
+
@condvar.wait(@lock)
|
209
|
+
end
|
210
|
+
msg = @rmessages[tag]
|
211
|
+
@rmessages.delete(tag)
|
212
|
+
end
|
213
|
+
|
214
|
+
# puts "Received message #{msg.to_s}"
|
215
|
+
# Special things to do for certain messages
|
216
|
+
case msg.mtype
|
217
|
+
when Message::StyxMessage::RERROR
|
218
|
+
# raise an exception when we receive an Rerror
|
219
|
+
raise StyxException.new(msg.ename)
|
220
|
+
when Message::StyxMessage::RCLUNK
|
221
|
+
# find the fid corresponding to the original Tclunk and free it
|
222
|
+
fid = @pendingclunks[tag]
|
223
|
+
@pendingclunks.delete(tag)
|
224
|
+
return_fid(fid)
|
225
|
+
end
|
226
|
+
return(msg)
|
227
|
+
end
|
228
|
+
|
229
|
+
public
|
230
|
+
|
231
|
+
##
|
232
|
+
# Get a new fid where it's needed.
|
233
|
+
#
|
234
|
+
def get_free_fid
|
235
|
+
fid = Util.get_first_unused(0xffffffff,
|
236
|
+
lambda {|i| @usedfids.include?(i) },
|
237
|
+
"fids")
|
238
|
+
@usedfids << fid
|
239
|
+
return(fid)
|
240
|
+
end
|
241
|
+
|
242
|
+
##
|
243
|
+
# returns a fid after we are finished with it.
|
244
|
+
#
|
245
|
+
# +fid+:: [Fixnum] the fid to return
|
246
|
+
#
|
247
|
+
def return_fid(fid)
|
248
|
+
@usedfids.delete(fid)
|
249
|
+
end
|
250
|
+
|
251
|
+
##
|
252
|
+
# Send a message and wait for the reply
|
253
|
+
#
|
254
|
+
# +message+:: [StyxMessage] the message to be sent
|
255
|
+
# return:: [StyxMessage] the reply to our message
|
256
|
+
#
|
257
|
+
def send_message(message)
|
258
|
+
tag = send_message_async(message)
|
259
|
+
return(get_reply(tag))
|
260
|
+
end
|
261
|
+
|
262
|
+
##
|
263
|
+
# Disconnect from the remote server.
|
264
|
+
#
|
265
|
+
def disconnect
|
266
|
+
# Clunk all outstanding fids in reverse order so root fid gets
|
267
|
+
# clunked last.
|
268
|
+
while (@usedfids.length > 0)
|
269
|
+
# The fid is automatically removed from @usedfids when the
|
270
|
+
# Rclunk message is received
|
271
|
+
rclunk = send_message(Message::Tclunk.new(@usedfids[-1]))
|
272
|
+
end
|
273
|
+
|
274
|
+
# Flush all outstanding messages
|
275
|
+
@rmessages.each_key do |tag|
|
276
|
+
rflush = send_message(Message::Tflush.new(tag))
|
277
|
+
end
|
278
|
+
# kill the receiver thread (FIXME: there has got to be a cleaner way!)
|
279
|
+
@receiver.kill
|
280
|
+
@conn.close
|
281
|
+
end
|
282
|
+
|
283
|
+
##
|
284
|
+
# Open a file on the connection. Throws a StyxException if it doesn't
|
285
|
+
# exist. If a block is specified along with the file, it is passed
|
286
|
+
# the new StyxFile, and the StyxFile is closed when the block
|
287
|
+
# terminates.
|
288
|
+
#
|
289
|
+
# +path+:: the path to the Styx file to be opened
|
290
|
+
# +mode+:: the mode to open the file (which can be r, w, r+, or e)
|
291
|
+
#
|
292
|
+
# FIXME: This doesn't deal with the case where MAXWELEM is reached yet.
|
293
|
+
#
|
294
|
+
def open(path, mode='r')
|
295
|
+
# get a new fid for the file
|
296
|
+
fid = get_free_fid
|
297
|
+
# Walk the fid to the given file
|
298
|
+
begin
|
299
|
+
twalk = Message::Twalk.new(@rootfid, fid, path)
|
300
|
+
rwalk = send_message(twalk)
|
301
|
+
# if the rwalk has some other length than the number of path
|
302
|
+
# elements in the original twalk, we have failed.
|
303
|
+
if rwalk.qids.length != twalk.path_elements.length
|
304
|
+
raise StyxException.new(sprintf("%s: no such file or directory", twalk.path_elements[rwalk.qids.length]))
|
305
|
+
end
|
306
|
+
# TODO: if the file is a directory?
|
307
|
+
# Convert the mode string into a mode number
|
308
|
+
mval = case mode
|
309
|
+
when 'r': Message::Topen::OREAD
|
310
|
+
when 'w': Message::Topen::OWRITE
|
311
|
+
when 'r+': Message::Topen::ORDWR
|
312
|
+
when 'e': Message::Topen::OEXEC
|
313
|
+
end
|
314
|
+
# Create, open, and return a StyxIO object
|
315
|
+
ropen = send_message(Message::Topen.new(fid, mval))
|
316
|
+
fp = StyxIO.new(self, path, fid, ropen.iounit, mode)
|
317
|
+
if block_given?
|
318
|
+
begin
|
319
|
+
yield fp
|
320
|
+
ensure
|
321
|
+
fp.close
|
322
|
+
end
|
323
|
+
else
|
324
|
+
return(fp)
|
325
|
+
end
|
326
|
+
rescue StyxException => se
|
327
|
+
return_fid(fid)
|
328
|
+
raise se # reraise the exception
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
end
|
333
|
+
|
334
|
+
##
|
335
|
+
# TCP connection instance of a Styx connection
|
336
|
+
#
|
337
|
+
class TCPConnection < Connection
|
338
|
+
attr_reader :host, :port
|
339
|
+
|
340
|
+
##
|
341
|
+
# Create a Styx connection over TCP
|
342
|
+
#
|
343
|
+
# +host+:: [String] the host to connect to
|
344
|
+
# +port+:: [Fixnum] the port on which to connect
|
345
|
+
# +user+:: [String] the user to connect as
|
346
|
+
#
|
347
|
+
def initialize(host, port, user="")
|
348
|
+
@host = host
|
349
|
+
@port = port
|
350
|
+
super(user)
|
351
|
+
end
|
352
|
+
|
353
|
+
##
|
354
|
+
# TCP connection version of startconn
|
355
|
+
#
|
356
|
+
def startconn
|
357
|
+
sock = TCPSocket.new(@host, @port)
|
358
|
+
return(sock)
|
359
|
+
end
|
360
|
+
|
361
|
+
end
|
362
|
+
|
363
|
+
##
|
364
|
+
# A file or directory on a Styx server. This should probably not
|
365
|
+
# be instantiated directly, but only by the use of Connection#open.
|
366
|
+
#
|
367
|
+
# TODO: This should later become a true subclass of IO.
|
368
|
+
#
|
369
|
+
class StyxIO
|
370
|
+
attr_reader :name, :mode, :closed, :offset
|
371
|
+
|
372
|
+
def initialize(conn, name, fid, iounit, mode="r")
|
373
|
+
@conn = conn
|
374
|
+
@name = name
|
375
|
+
@mode = mode
|
376
|
+
@closed = false # closed flag
|
377
|
+
@fid = fid
|
378
|
+
@iounit = iounit # maximum payload of a Twrite or Rread message
|
379
|
+
@offset = 0 # file offset
|
380
|
+
end
|
381
|
+
|
382
|
+
##
|
383
|
+
# closes the file. Once this has been done, no more read or write
|
384
|
+
# operations should be possible.
|
385
|
+
#
|
386
|
+
def close
|
387
|
+
flush # flush any bytes buffered (later)
|
388
|
+
rclunk = @conn.send_message(Message::Tclunk.new(@fid))
|
389
|
+
# FIXME: abort all outstanding messages with flushes
|
390
|
+
end
|
391
|
+
|
392
|
+
##
|
393
|
+
# Read at most +size+ bytes from the current file position.
|
394
|
+
# If the size argument is negative or omitted, read until EOF.
|
395
|
+
#
|
396
|
+
# +size+:: number of bytes to read from the file
|
397
|
+
#
|
398
|
+
def read(size=-1)
|
399
|
+
contents = ""
|
400
|
+
bytes_to_read = size
|
401
|
+
loop do
|
402
|
+
if size < 0 || bytes_to_read > @iounit
|
403
|
+
n = @iounit
|
404
|
+
elsif bytes_to_read <= 0
|
405
|
+
break
|
406
|
+
else
|
407
|
+
n = bytes_to_read
|
408
|
+
end
|
409
|
+
rread = @conn.send_message(Message::Tread.new(@fid, @offset, n))
|
410
|
+
if rread.data.length == 0
|
411
|
+
break # EOF
|
412
|
+
end
|
413
|
+
@offset += rread.data.length
|
414
|
+
contents << rread.data
|
415
|
+
if size >= 0
|
416
|
+
bytes_to_read -= rread.data.length
|
417
|
+
end
|
418
|
+
end
|
419
|
+
return(contents)
|
420
|
+
end
|
421
|
+
|
422
|
+
##
|
423
|
+
#
|
424
|
+
# Write data to the file at the current offset. No buffering is
|
425
|
+
# performed (yet).
|
426
|
+
#
|
427
|
+
# +str+:: data to be written
|
428
|
+
#
|
429
|
+
def write(str)
|
430
|
+
pos = 0
|
431
|
+
loop do
|
432
|
+
bytes_left = str.length - pos
|
433
|
+
if bytes_left <= 0
|
434
|
+
break
|
435
|
+
elsif bytes_left > @iounit
|
436
|
+
n = @iounit
|
437
|
+
else
|
438
|
+
n = bytes_left
|
439
|
+
end
|
440
|
+
rwrite = @conn.send_message(Message::Twrite.new(@fid, @offset,
|
441
|
+
str[pos..(pos+n)]))
|
442
|
+
if rwrite.count != n
|
443
|
+
raise StyxException.new("error writing data")
|
444
|
+
end
|
445
|
+
pos += n
|
446
|
+
@offset += n
|
447
|
+
end
|
448
|
+
|
449
|
+
end
|
450
|
+
|
451
|
+
##
|
452
|
+
# Flushes any buffered data
|
453
|
+
#
|
454
|
+
def flush
|
455
|
+
# TODO
|
456
|
+
end
|
457
|
+
|
458
|
+
##
|
459
|
+
# Move to a new seek position
|
460
|
+
#
|
461
|
+
def seek(offset, whence=:seek_set)
|
462
|
+
case whence
|
463
|
+
when :seek_set: @offset = offset
|
464
|
+
when :seek_cur: @offset += offset
|
465
|
+
# FIXME: do :seek_end
|
466
|
+
when :seek_end: raise StyxException.new("Can't seek from end of file")
|
467
|
+
else raise StyxException.new("whence must be :seek_set, :seek_cur, or :seek_end")
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
##
|
472
|
+
# Return the current file position
|
473
|
+
#
|
474
|
+
def tell
|
475
|
+
return @offset
|
476
|
+
end
|
477
|
+
|
478
|
+
##
|
479
|
+
# Give the DirEntry corresponding to this file
|
480
|
+
#
|
481
|
+
def stat
|
482
|
+
rstat = @conn.send_message(Message::Tstat.new(@fid))
|
483
|
+
return(rstat.stat)
|
484
|
+
end
|
485
|
+
|
486
|
+
end
|
487
|
+
|
488
|
+
end # module Client
|
489
|
+
|
490
|
+
end # module RStyx
|
491
|
+
|
492
|
+
if $0 == __FILE__
|
493
|
+
RStyx::Client::TCPConnection.new("127.0.0.1", 1234) do |c|
|
494
|
+
c.open("hello.txt", "r+") do |fp|
|
495
|
+
puts fp.stat.to_s
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
data/lib/rstyx/errors.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
#
|
3
|
+
# Copyright (C) 2005 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., 59 Temple Place, Suite 330, Boston, MA
|
19
|
+
# 02111-1307 USA.
|
20
|
+
#
|
21
|
+
# Styx Exception classes.
|
22
|
+
#
|
23
|
+
# Author:: Rafael R. Sevilla (mailto:dido@imperium.ph)
|
24
|
+
# Copyright:: Copyright (c) 2005 Rafael R. Sevilla
|
25
|
+
# License:: GNU Lesser General Public License
|
26
|
+
#
|
27
|
+
# $Id: errors.rb,v 1.1 2005/09/28 15:25:11 dido Exp $
|
28
|
+
#
|
29
|
+
|
30
|
+
module RStyx
|
31
|
+
|
32
|
+
##
|
33
|
+
# Exception class for Styx errors. Same as standard Exception
|
34
|
+
# class for now, but here so we can distinguish Styx errors and
|
35
|
+
# for later extension.
|
36
|
+
#
|
37
|
+
class StyxException < Exception
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|