rstyx 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|