rstyx 0.1.0

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