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.
@@ -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
+
@@ -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