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/lib/rstyx/client.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/ruby
2
2
  #
3
- # Copyright (C) 2005 Rafael Sevilla
3
+ # Copyright (C) 2005-2007 Rafael Sevilla
4
4
  # This file is part of RStyx
5
5
  #
6
6
  # RStyx is free software; you can redistribute it and/or modify
@@ -15,646 +15,599 @@
15
15
  #
16
16
  # You should have received a copy of the GNU Lesser General Public
17
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.
18
+ # Foundation, Inc., 51 Franklin St., Fifth Floor, Boston, MA
19
+ # 02110-1301 USA.
20
20
  #
21
21
  # Styx Client
22
22
  #
23
23
  # Author:: Rafael R. Sevilla (mailto:dido@imperium.ph)
24
- # Copyright:: Copyright (c) 2005 Rafael R. Sevilla
24
+ # Copyright:: Copyright (c) 2005-2007 Rafael R. Sevilla
25
25
  # License:: GNU Lesser General Public License
26
26
  #
27
- # $Id: client.rb,v 1.10 2005/11/01 13:02:18 dido Exp $
27
+ # $Id: client.rb 201 2007-06-08 17:13:58Z dido $
28
28
  #
29
29
 
30
- require 'English'
31
- require 'socket'
32
30
  require 'thread'
33
31
  require 'timeout'
34
- require 'rstyx/errors'
35
- require 'rstyx/messages'
32
+ require 'rubygems'
33
+ require 'eventmachine'
36
34
 
37
35
  module RStyx
38
36
 
39
37
  module Client
40
38
 
41
39
  ##
42
- # Utility functions
40
+ # Message receiving module for the Styx client. The client will
41
+ # assemble all inbound messages.
43
42
  #
44
- class Util
43
+ module StyxClient
44
+ attr_accessor :sentmessages
45
+
46
+ def post_init
47
+ @msgbuffer = ""
48
+ @lock = Mutex.new
49
+ @sentmessages = {}
50
+ end
51
+
45
52
  ##
46
- # Gets the next unused tag or fid.
53
+ # Send a message asynchronously.
47
54
  #
48
- # +limit+:: [Fixnum] maximum value
49
- # +func+:: [Proc] predicate that determines if tag or fid i is in use
50
- # +thing+:: [String] what we're trying to get
51
- # return:: an unused value
55
+ # +message+:: [StyxMessage] the message to be sent
56
+ # +block+:: [Proc] the callback to use
57
+ # return:: [Fixnum] the tag number used.
52
58
  #
53
- def Util.get_first_unused(limit, func, thing)
54
- found = false
55
- val = nil
56
- 0.upto(limit-1) do |i|
57
- if func.call(i)
58
- i += 1
59
- else
60
- val = i
61
- break
59
+ def send_message_async(message, &block)
60
+ # store the message and callback indexed by tag
61
+ @lock.synchronize do
62
+ if message.tag.nil?
63
+ # If a tag has not been explicitly specified, get
64
+ # a new tag for the message. We use the current
65
+ # thread's object ID as the base and use what
66
+ # amounts to a linear probing algorithm to
67
+ # determine a new tag in case of collisions.
68
+ tag = Thread.current.object_id % MAX_TAG
69
+ while @sentmessages.has_key?(tag)
70
+ tag += 1
71
+ end
72
+ message.tag = tag
62
73
  end
74
+ @sentmessages[message.tag] = [message, block]
63
75
  end
64
76
 
65
- if val.nil?
66
- # this should probably be impossible
67
- raise StyxException.new(sprintf("No more free %s!", thing))
68
- end
69
- return(val)
77
+ DEBUG > 0 && puts(" >> #{message.to_s}")
78
+ DEBUG > 1 && puts(" >> #{message.to_bytes.unpack("H*").inspect}")
79
+ send_data(message.to_bytes)
80
+ return(message.tag)
70
81
  end
71
82
 
72
- end
73
-
74
- ##
75
- # An abstract class for Styx connections. DO NOT INSTANTIATE THIS
76
- # CLASS DIRECTLY!
77
- #
78
- # A +Connection+ subclass instance acts in the same way as the
79
- # +File+ class would under Ruby, and many of the class methods for
80
- # File are implemented as instance methods for Connection, and are
81
- # relevant to the Styx filesystem the Connection is connected to.
82
- # There are a few differences: for instance, new is not equivalent
83
- # to open, for obvious reasons.
84
- #
85
- class Connection
86
- attr_reader :msize, :version, :rootfid, :user
87
-
88
83
  ##
89
- # Create a new Styx connection. If a block is passed, yield the
90
- # new connection to the block and do a disconnect when the block
91
- # finishes.
84
+ # Send a message synchronously. If an error occurs, a
85
+ # StyxException is raised.
92
86
  #
93
- # +user+:: [String] the user to connect as
87
+ # +message+:: The Styx message to send.
88
+ # +timeout+:: optional timeout for receiving the response.
94
89
  #
95
- def initialize(user="")
96
- @lock = Mutex.new # used to synchronize threads
97
- @condvar = ConditionVariable.new
98
- @rmessages = Hash.new # filled with rmessages that have arrived
99
- @usedfids = Array.new # list of fids currently in use
100
- @pendingclunks = Hash.new # outstanding Tclunk messages (tags and fids)
101
- @conn = self.startconn # get the connection
102
-
103
- # start message receiving thread
104
- @receiver = Thread.new { receive_messages }
105
-
106
- tv = Message::Tversion.new
107
- rver = send_message(tv)
108
- @msize = rver.msize
109
- @version = rver.version
110
- @rootfid = get_free_fid # the fid representing the serv. root
111
- rattach = send_message(Message::Tattach.new(@rootfid, user))
112
- @user = user
90
+ def send_message(message, timeout=0)
91
+ # The queue here holds the response message, and is used
92
+ # to communicate with the receive_data thread, which ultimately
93
+ # calls the block passed to send_message_async.
94
+ queue = Queue.new
95
+ send_message_async(message) do |tx,rx|
96
+ # Enqueue the response message -- this runs in the
97
+ # receive_data thread
98
+ queue << rx
99
+ end
100
+ Timeout::timeout(timeout, StyxException.new("timeout waiting for a reply to #{message.to_s}")) do
101
+ # This will block until the queue contains something
102
+ resp = queue.shift
103
+ # Check the response to see if it is the response to
104
+ # the transmitted message.
105
+ if resp.class == Message::Rerror
106
+ raise StyxException.new("#{resp.ename}")
107
+ end
113
108
 
114
- if block_given?
115
- begin
116
- yield self
117
- ensure
118
- self.disconnect
109
+ if resp.ident != message.ident + 1
110
+ raise StyxException.new("Unexpected #{resp.to_s} received in response to #{message.to_s}")
119
111
  end
120
- else
121
- return(self)
112
+ return(resp)
122
113
  end
123
114
  end
124
115
 
125
- protected
126
-
127
116
  ##
128
- # This method should be overridden by subclasses, and should return
129
- # a subclass of IO (e.g. a TCPSocket) on which the read and write
130
- # methods can be used.
117
+ # Receive data from the network connection, called by EventMachine.
131
118
  #
132
- def startconn
133
- return(nil) # Override me!
134
- end
119
+ def receive_data(data)
120
+ @msgbuffer << data
121
+ DEBUG > 1 && puts(" << #{data.unpack("H*").inspect}")
122
+ while @msgbuffer.length > 4
123
+ length = @msgbuffer.unpack("V")[0]
124
+ # Break out if there is not enough data in the message
125
+ # buffer to construct a message.
126
+ if @msgbuffer.length < length
127
+ break
128
+ end
135
129
 
136
- ##
137
- # Thread that listens for incoming messages on the socket.
138
- #
139
- # FIXME: Frankly, I think this is quite ugly...
140
- #
141
- def receive_messages
142
- buffer = ""
143
- data = nil
144
- done = false
145
- loop do
146
- begin
147
- data = @conn.read(1)
148
- rescue Exception => e
149
- done = true
150
- raise StyxException.new("error reading from socket: #{e.message}")
130
+ # Decode the received data
131
+ message, @msgbuffer = @msgbuffer.unpack("a#{length}a*")
132
+ styxmsg = Message::StyxMessage.from_bytes(message)
133
+ DEBUG > 0 && puts(" << #{styxmsg.to_s}")
134
+ # and look for its tag in the hash of received messages
135
+ tmsg, cb = @lock.synchronize do
136
+ @sentmessages.delete(styxmsg.tag)
137
+ end
138
+
139
+ if tmsg.nil?
140
+ # Discard unrecognized messages.
141
+ next
151
142
  end
152
143
 
153
- # There may be more than one message (or a partial message)
154
- buffer << data
155
- if buffer.length >= 4
156
- length = buffer[0..3].unpack("V")[0]
157
- if buffer.length >= length
158
- message = buffer[0..(length-1)]
159
- buffer = buffer[length..(buffer.length)]
160
- styxmsg = Message::StyxMessage.decode(message)
161
-
162
- # let everyone know there's a new message available
163
- @lock.synchronize do
164
- @rmessages[styxmsg.tag] = styxmsg
165
- @condvar.signal
144
+ if styxmsg.class == Message::Rflush
145
+ # We need to delete the oldtag as well, and send the
146
+ # rflush to the original sender if possible, so they
147
+ # don't wait forever.
148
+ if tmsg.respond_to?(:oldtag)
149
+ otmsg, ocb = @lock.synchronize do
150
+ @sentmessages.delete(tmsg.oldtag)
166
151
  end
167
152
  end
153
+
154
+ if !otmsg.nil? && !ocb.nil?
155
+ ocb.call(otmsg, styxmsg)
156
+ end
168
157
  end
169
- end
170
- end
171
158
 
172
- ##
173
- # Get a new tag for use in new messages.
174
- #
175
- def get_free_tag
176
- return(Util.get_first_unused(0xffff,
177
- lambda {|i| @rmessages.has_key?(i) },
178
- "tags"))
159
+ # Now, activate the callback block.
160
+ if !(tmsg.nil? || cb.nil?)
161
+ cb.call(tmsg, styxmsg)
162
+ end
163
+
164
+ # after all this is done, there may still be enough data in
165
+ # the message buffer for more messages so keep looping.
166
+ end
167
+ # If we get here, we don't have enough data in the buffer to
168
+ # build a new message.
179
169
  end
180
170
 
181
171
  ##
182
- # Send a message and return immediately, returning the message's tag
183
- #
184
- # +message+:: [StyxMessage] the message to be sent
185
- # return:: [Fixnum] the tag number used
172
+ # Disconnect from the remote server.
186
173
  #
187
- def send_message_async(message)
188
- if (message.tag.nil?)
189
- message.tag = get_free_tag
174
+ def disconnect
175
+ # flush all outstanding messages before disconnect
176
+ sentmessages.each_key do |tag|
177
+ rflush = send_message(Message::Tflush.new(:oldtag => tag))
190
178
  end
191
179
 
192
- # If this is a Tclunk message we are trying to close the file.
193
- # Remember the fid so we can free it from the list of used fids
194
- # when we get the Rclunk corresponding to it.
195
- if message.mtype == Message::StyxMessage::TCLUNK
196
- @pendingclunks[message.tag] = message.fid
197
- end
180
+ EventMachine::stop_event_loop
181
+ end
198
182
 
199
- # puts "sending message #{message.to_s}"
200
- @conn.write(message.to_bytes)
201
- return(message.tag)
183
+ end # module StyxClient
184
+
185
+ class Connection
186
+ attr_accessor :usedfids, :pendingclunks, :umask
187
+ attr_reader :connectstate, :msize, :version
188
+ attr_reader :rootdirectory, :rootfid
189
+
190
+ def initialize(auth=nil)
191
+ @usedfids = []
192
+ @pendingclunks = {}
193
+ @rpendingclunks = {}
194
+ @conn = nil
195
+ @rootfid = nil
196
+ @eventthread = nil
197
+ @authenticator = auth
198
+ @clunklock = Mutex.new
199
+ @umask = ::File.umask
202
200
  end
203
201
 
204
202
  ##
205
- # Waits for the message with the given tag to arrive, then returns it.
206
- #
207
- # +tag+:: [Fixnum] Tag to wait for
208
- # return:: [Message::StyxMessage] the message received
203
+ # Get a new free FID.
209
204
  #
210
- def get_reply(tag)
211
- msg = nil
212
- @lock.synchronize do
213
- # wait for a new message to arrive
214
- while @rmessages[tag].nil?
215
- @condvar.wait(@lock)
205
+ def get_free_fid
206
+ found = false
207
+ val = nil
208
+ 0.upto(MAX_FID) do |i|
209
+ unless @usedfids.include?(i)
210
+ val = i
211
+ break
216
212
  end
217
- msg = @rmessages[tag]
218
- @rmessages.delete(tag)
219
- end
220
-
221
- # puts "Received message #{msg.to_s}"
222
- # Special things to do for certain messages
223
- case msg.mtype
224
- when Message::StyxMessage::RERROR
225
- # raise an exception when we receive an Rerror
226
- raise StyxException.new(msg.ename)
227
- when Message::StyxMessage::RCLUNK
228
- # find the fid corresponding to the original Tclunk and free it
229
- fid = @pendingclunks[tag]
230
- @pendingclunks.delete(tag)
231
- return_fid(fid)
232
213
  end
233
- return(msg)
234
- end
235
214
 
236
- public
237
-
238
- ##
239
- # Get a new fid where it's needed.
240
- #
241
- def get_free_fid
242
- fid = Util.get_first_unused(0xffffffff,
243
- lambda {|i| @usedfids.include?(i) },
244
- "fids")
245
- @usedfids << fid
246
- return(fid)
215
+ if val.nil?
216
+ raise StyxException.new("No more free fids")
217
+ end
218
+ @usedfids << val
219
+ return(val)
247
220
  end
248
221
 
249
222
  ##
250
- # returns a fid after we are finished with it.
251
- #
252
- # +fid+:: [Fixnum] the fid to return
223
+ # Returns a fid after we're done using it.
253
224
  #
254
225
  def return_fid(fid)
255
226
  @usedfids.delete(fid)
256
227
  end
257
228
 
258
- ##
259
- # Send a message and wait for the reply
260
- #
261
- # +message+:: [StyxMessage] the message to be sent
262
- # return:: [StyxMessage] the reply to our message
263
- #
264
- def send_message(message)
265
- tag = send_message_async(message)
266
- return(get_reply(tag))
267
- end
229
+ protected
268
230
 
269
231
  ##
270
- # Disconnect from the remote server.
232
+ # This method is used to prepare the connection, and should be
233
+ # defined by subclasses.
271
234
  #
272
- def disconnect
273
- # Clunk all outstanding fids in reverse order so root fid gets
274
- # clunked last.
275
- while (@usedfids.length > 0)
276
- # The fid is automatically removed from @usedfids when the
277
- # Rclunk message is received
278
- rclunk = send_message(Message::Tclunk.new(@usedfids[-1]))
279
- end
280
-
281
- # Flush all outstanding messages
282
- @rmessages.each_key do |tag|
283
- rflush = send_message(Message::Tflush.new(tag))
284
- end
285
- # kill the receiver thread (FIXME: there has got to be a cleaner way!)
286
- @receiver.kill
287
- @conn.close
235
+ def prepare_connection
236
+ raise StyxException.new("No prepare_connection method defined")
288
237
  end
289
238
 
239
+ public
240
+
290
241
  ##
291
- # Open a file on the connection. Throws a StyxException if it doesn't
292
- # exist. If a block is specified along with the file, it is passed
293
- # the new StyxFile, and the StyxFile is closed when the block
294
- # terminates.
295
- #
296
- # +path+:: the path to the Styx file to be opened
297
- # +mode+:: the mode to open the file (which can be r, w, r+, or e)
298
- # +perm+:: the (optional) permissions bit mask. This is only
299
- # relevant if the open creates a file.
300
- # +rfid+:: the root fid. If this is nil, use the root fid we got
301
- # when we connected.
302
- #
303
- # All parameters are optional. If no path is specified, it defaults
304
- # to reading the root directory on the Styx server.
305
- #
306
- # FIXME: This doesn't deal with the case where MAXWELEM is reached
307
- # yet.
308
- # TODO: Allow the caller to make use of the Styx file access mode
309
- # values (OREAD, OWRITE, ORDWR, OEXEC, OTRUNC, ORCLOSE).
242
+ # Connect to a remote Styx server. If a block is passed, yield
243
+ # self to the block and then do a disconnect when the block
244
+ # finishes.
310
245
  #
311
- def open(path="", mode='r', perm=0644, rfid=nil)
312
- # get a new fid for the file
313
- fid = get_free_fid
246
+ def connect(&block)
247
+ prepare_connection()
314
248
 
315
- if rfid.nil?
316
- rfid = @rootfid
317
- end
318
-
319
- #
320
- # The mode string is the standard Ruby mode string, viz.
321
- # "r" - read-only, start at beginning of file
322
- # "r+" - read/write, start at beginning of file
323
- # "w" - write only, truncate existing file or create it if it
324
- # doesn't exist.
325
- # "w+" - read/write, truncate existing file or create it if it
326
- # doesn't exist
327
- # "a" - write only, starts at end of file if it exists otherwise
328
- # creates a new file for writing.
329
- # "a+" - read/write, starts at end of file if it exists, otherwise
330
- # creates a new file for writing.
331
- #
332
- # There is also a mode specific to RStyx, not a standard mode
333
- # for Ruby:
249
+ # Connection has been established. Begin the handshaking process
250
+ # with the remote server.
334
251
  #
335
- # "e" - execute the file
252
+ # 1. Send a Tversion message and check the response from the
253
+ # remote Styx server.
336
254
  #
337
- # The "b" suffix on DOS/Windows is not recognized as valid.
338
- #
339
- read = true
340
- write = false
341
- create = false
342
- truncate = false
343
- append = false
344
- mval = Message::Topen::OREAD
345
- case mode
346
- when "r"
347
- # all default above
348
- when "r+"
349
- write = true
350
- mval = Message::Topen::ORDWR
351
- when "w"
352
- read = false
353
- write = true
354
- create = true
355
- truncate = true
356
- mval = Message::Topen::OWRITE
357
- when "w+"
358
- read = true
359
- write = true
360
- create = true
361
- truncate = true
362
- mval = Message::Topen::ORDWR
363
- when "a"
364
- read = false
365
- write = true
366
- create = true
367
- append = true
368
- mval = Message::Topen::OWRITE
369
- when "a+"
370
- read = true
371
- write = true
372
- create = true
373
- append = true
374
- mval = Message::Topen::ORDWR
375
- when "e"
376
- read = true
377
- write = true
378
- mval = Message::Topen::OEXEC
379
- else
380
- raise StyxException.new("Invalid mode string '#{mode}'")
381
- end
382
-
383
- if truncate
384
- mval |= Message::Topen::OTRUNC
255
+ rver = send_message(Message::Tversion.new(:msize => 8216,
256
+ :version => "9P2000"))
257
+ if (rver.version != "9P2000")
258
+ raise StyxException.new("Server uses unsupported Styx version #{rver.version}")
385
259
  end
386
-
387
- iclass = StyxIO
388
- begin
389
- twalk = Message::Twalk.new(rfid, fid, path)
390
- rwalk = send_message(twalk)
391
- # if the rwalk has some other length than the number of path
392
- # elements in the original twalk, we have failed.
393
- if rwalk.qids.length != twalk.path_elements.length
394
- raise StyxException.new(sprintf("%s: no such file or directory", twalk.path_elements[rwalk.qids.length]))
395
- end
396
- open_msg = Message::Topen.new(fid, mval)
397
- # Determine if the file is actually a directory. Such a file
398
- # would have its Qid.qtype high bit set to 1. In this case,
399
- # mode can only be 'r', and instead of a StyxIO, a StyxDir
400
- # instance is created instead.
260
+ @msize = rver.msize
261
+ @version = rver.version
262
+ rfid = nil
263
+ # 2. Attach to the remote server
264
+ if @auth.nil?
265
+ # unauthenticated connection
266
+ rfid = get_free_fid
267
+ rattach = send_message(Message::Tattach.new(:fid => rfid,
268
+ :afid => NOFID,
269
+ :uname => ENV['USER'],
270
+ :aname => ""))
271
+ else
401
272
  #
402
- # If the number of path elements involved is zero, assume a
403
- # directory, as the root fid is always one.
273
+ # 3. Perform authentication based on the passed authenticator
274
+ # object.
404
275
  #
405
- if twalk.path_elements.length == 0 ||
406
- rwalk.qids[twalk.path_elements.length-1].qtype & 0x80 != 0
407
- iclass = StyxDir
408
- end
409
-
410
- rescue StyxException => se
411
- # If the walk generated an error, see if the create flag
412
- # was true. If so, the error may be ignored, and the
413
- # open_msgclass set to Message::Tcreate. Otherwise, return
414
- # the fid and reraise the exception.
415
- if create
416
- # Walk to the directory of the file to be made, if possible
417
- begin
418
- rwalk = send_message(Message::Twalk.new(rfid, fid,
419
- File.dirname(path)))
420
- rescue StyxException => se
421
- return_fid(fid)
422
- raise se # reraise the exception
423
- end
424
- open_msg = Message::Tcreate.new(fid, File.basename(path),
425
- mval, perm)
426
- else
427
- return_fid(fid)
428
- raise se # reraise the exception
429
- end
276
+ # If we have an authenticator object, we call its authenticator
277
+ # method with ourself.
278
+ #
279
+ rfid = @auth.authenticate(self)
430
280
  end
431
281
 
432
- begin
433
- # Create, open, and return a StyxIO object
434
- ropen = send_message(open_msg)
435
- fp = iclass.new(self, path, fid, ropen.iounit, mode)
436
-
437
- # if we're supposed to append, seek to the end of the file
438
- if append
439
- rstat = send_message(Message::Tstat.new(fid))
440
- fp.seek(rstat.stat.length, :seek_set)
441
- end
282
+ # If we get here, we're connected, and rfid represents the root
283
+ # fid of the connection
284
+ @rootfid = rfid
442
285
 
443
- if block_given?
444
- begin
445
- yield fp
446
- ensure
447
- fp.close
448
- end
449
- else
450
- return(fp)
286
+ if block_given?
287
+ begin
288
+ yield self
289
+ ensure
290
+ self.disconnect
451
291
  end
452
- rescue StyxException => se
453
- return_fid(fid)
454
- raise se # reraise the exception
292
+ else
293
+ return(self)
455
294
  end
456
295
  end
457
296
 
458
297
  ##
459
- # Delete the files specified. Unlike File.delete, this method
460
- # should also work properly on directories
298
+ # Disconnect from the remote server.
461
299
  #
462
- def delete(*files)
463
- files.each do |file|
464
- fid = get_free_fid
465
- # walk to the file to be deleted
300
+ def disconnect
301
+ # Clunk all outstanding fids in reverse order so the root fid
302
+ # gets clunked last.
303
+ while (@usedfids.length > 0)
466
304
  begin
467
- rwalk = send_message(Message::Twalk.new(@rootfid, fid, file))
468
- rescue StyxException => se
469
- return_fid(fid)
470
- raise se
305
+ rclunk = tclunk(@usedfids[-1], true)
306
+ rescue
307
+ # An error is most likely a no such fid error. Return the fid
308
+ # manually in this case.
309
+ return_fid(@usedfids[-1])
471
310
  end
311
+ end
472
312
 
473
- begin
474
- rremove = send_message(Message::Tremove.new(fid))
475
- return_fid(fid)
476
- rescue StyxException => se
477
- # because the Twalk succeeded, we need to clunk the fid
478
- begin
479
- rclunk = send_message(Message::Tclunk.new(fid))
480
- rescue StyxException => se
481
- return_fid(fid)
313
+ @conn.disconnect
314
+ @eventthread.kill
315
+ end
316
+
317
+ ##
318
+ # Send a message, and return the response. Delegates to
319
+ # @conn#send_message. Do not use this method to send Tclunk
320
+ # messages!
321
+ #
322
+ def send_message(msg, timeout=0)
323
+ @conn.send_message(msg, timeout)
324
+ end
325
+
326
+ ##
327
+ # Fire and forget a Tclunk for some fid. When the Rclunk is
328
+ # received, return the fid. USE THIS METHOD, AND THIS METHOD
329
+ # ONLY, to send Tclunk messages.
330
+ #
331
+ def tclunk(fid, sync=false)
332
+ if @rpendingclunks.has_key?(fid)
333
+ return
334
+ end
335
+ q = nil
336
+ if sync
337
+ q = Queue.new
338
+ end
339
+ tag = @conn.send_message_async(Message::Tclunk.new(:fid => fid)) do |tx,rx|
340
+ # Test whether the response is an Rclunk.
341
+ if rx.class != Message::Rclunk
342
+ # this is an error condition, but it will only get reported
343
+ # if Thread.abort_on_exception is set to true, or if
344
+ # the tclunk is synchronous
345
+ exc = StyxException.new("#{tx.to_s} received #{rx.to_s}")
346
+ if sync
347
+ q << exc
348
+ else
349
+ raise exc
482
350
  end
483
- raise se
351
+ end
352
+ # return the FID
353
+ fid = @pendingclunks.delete(tag)
354
+ @rpendingclunks.delete(fid)
355
+ return_fid(fid)
356
+ if sync
357
+ q << fid
358
+ end
359
+ end
360
+ @pendingclunks[tag] = fid
361
+ @rpendingclunks[fid] = tag
362
+ if sync
363
+ res = q.shift
364
+ if res.class == StyxException
365
+ raise res
484
366
  end
485
367
  end
486
368
  end
487
369
 
488
- alias rmdir delete
489
- alias unlink delete
490
-
491
370
  ##
492
- # Create a new directory named by +dirname+, with permissions
493
- # specified by an optional parameter +perm+.
371
+ # Open a file on the remote server, throwing a StyxException if the
372
+ # file can't be found or opened in a given mode.
494
373
  #
495
- def mkdir(dirname, perm=0755)
496
- begin
497
- fid = get_free_fid
498
- perm |= Message::Tcreate::DMDIR
499
- rwalk = send_message(Message::Twalk.new(@rootfid, fid,
500
- File.dirname(dirname)))
501
- rescue StyxException => se
502
- return_fid(fid)
503
- raise se
374
+ # +path+:: The path of the file relative to the server root.
375
+ # +mode+:: Integer representing the mode, or one of "r", "r+",
376
+ # "w", "w+", "a", "a+", "e" as aliases
377
+ # +return+:: A File object representing the opened file, or
378
+ # possibly a Directory object if the file was a directory.
379
+ #
380
+ # If a block is passed, it will yield the file object to the block
381
+ # and close the file when the block finishes (actually it will pass
382
+ # the block on to the StyxFile#open method, which does just that).
383
+ #
384
+ def open(path, mode="r", perm=0666, &block)
385
+ file = File.new(self, path)
386
+
387
+ append = false
388
+ create = false
389
+ numeric_mode = nil
390
+ if mode.is_a?(Integer)
391
+ numeric_mode = mode
392
+ else
393
+ case mode.to_s
394
+ when "r"
395
+ numeric_mode = OREAD
396
+ when "r+"
397
+ numeric_mode = ORDWR
398
+ when "w"
399
+ numeric_mode = OTRUNC | OWRITE
400
+ create = true
401
+ when "w+"
402
+ numeric_mode = OTRUNC | ORDWR
403
+ create = true
404
+ when "a"
405
+ numeric_mode = OWRITE
406
+ append = true
407
+ create = true
408
+ when "a+"
409
+ numeric_mode = ORDWR
410
+ append = true
411
+ create = true
412
+ when "e"
413
+ numeric_mode = OEXEC
414
+ end
504
415
  end
505
416
 
506
- begin
507
- rcreate = send_message(Message::Tcreate.new(fid,
508
- File.basename(dirname),
509
- Message::Topen::OREAD,
510
- perm))
511
- rescue StyxException => se
512
- return_fid(fid)
513
- raise se
514
- ensure
515
- rclunk = send_message(Message::Tclunk.new(fid))
417
+ fp = file.open(numeric_mode, perm, create, &block)
418
+ if append
419
+ fp.seek(0, 2)
516
420
  end
421
+ return(fp)
517
422
  end
518
423
 
519
- end
424
+ end # class Connection
520
425
 
521
426
  ##
522
- # TCP connection instance of a Styx connection
427
+ # TCP connection subclass.
523
428
  #
524
429
  class TCPConnection < Connection
525
- attr_reader :host, :port
526
-
527
- ##
528
- # Create a Styx connection over TCP
529
- #
530
- # +host+:: [String] the host to connect to
531
- # +port+:: [Fixnum] the port on which to connect
532
- # +user+:: [String] the user to connect as
533
- #
534
- def initialize(host, port, user="")
430
+ def initialize(host, port, auth=nil)
535
431
  @host = host
536
432
  @port = port
537
- super(user)
433
+ super(auth)
538
434
  end
539
435
 
436
+ protected
437
+
540
438
  ##
541
- # TCP connection version of startconn
439
+ # Prepare a TCP connection to the Styx server
542
440
  #
543
- def startconn
544
- sock = TCPSocket.new(@host, @port)
545
- return(sock)
441
+ def prepare_connection
442
+ queue = Queue.new
443
+ @eventthread = Thread.new do
444
+ EventMachine::run do
445
+ queue << EventMachine::connect(@host, @port, StyxClient)
446
+ end
447
+ end
448
+
449
+ @conn = queue.shift
546
450
  end
547
451
 
548
- end
452
+ public
453
+
454
+ end # class TCPConnection
549
455
 
550
456
  ##
551
- # Internally used class for buffers
552
- class Buffer
553
- attr_reader :bufsize
457
+ # A Styx client's view of a file. This class should probably
458
+ # never be directly instantiated, but only via Connection#open.
459
+ # The buffering algorithm in use here is somewhat based on the
460
+ # Buffering mix-in module in the Ruby OpenSSL module written by
461
+ # Goto Yuuzou, but modified a bit to provide for offset
462
+ # handling. Note that this class isn't thread-safe.
463
+ #
464
+ class File
465
+ include Enumerable
554
466
 
555
- def initialize(bufsize)
556
- @bufsize = bufsize # maximum size of the buffer
557
- @data = "" # data in the buffer
467
+ attr_reader :conn, :path
468
+ attr_accessor :mode, :fid, :qid, :iounit, :sync
469
+
470
+ def initialize(conn, path)
471
+ @conn = conn # the connection on which the file sits
472
+ @path = path # pathname of the file
473
+ @fid = -1 # the client's file identifier
474
+ @qid = nil # the server's unique identifier for this file
475
+ # maximum number of bytes that can be read or written to the file at a time
476
+ @iounit = 0
477
+ @mode = -1 # mode under which the file is opened, -1 == not open
478
+ @offset = 0 # File offset. This is the same as @boffset only after a seek
479
+ @rboffset = 0 # Read buffer offset
480
+ @eof = false
481
+ @rbuffer = ""
482
+ @sync = false # whether or not to buffer writes
558
483
  end
559
484
 
560
485
  ##
561
- # Returns true if the buffer is empty
486
+ # Open the file on the server. If a block is passed to this method
487
+ # it will pass the file to the block and close the file automatically
488
+ # when the block terminates.
562
489
  #
563
- def empty?
564
- return(@data.length == 0)
565
- end
490
+ # This follows more or less the same semantics as sys-open(2)
491
+ # on Inferno, performing an open with truncate, when a file
492
+ # is opened that doesn't exist.
493
+ #
494
+ # XXX - twalk should handle the case of MAXWELEM, as well as for
495
+ # when the twalk message is too large to fit in the server's
496
+ # designated msize.
497
+ #
498
+ # +mode+:: Integer representing the mode - see the constants in common.rb
499
+ # +perm+:: Permissions of the file (only used on open/create)
500
+ # +create+:: should we create the file if it doesn't exist?
501
+ #
502
+ def open(mode, perm, create, &block)
503
+ dfid = @conn.get_free_fid
504
+ basename = ::File.basename(@path)
505
+ dirname = ::File.dirname(@path)
506
+ # Walk to the dirname first
507
+ twalk = Message::Twalk.new(:fid => @conn.rootfid, :newfid => dfid)
508
+ twalk.path = dirname
509
+ rwalk = @conn.send_message(twalk)
510
+ # if the rwalk has some other length than the number of path
511
+ # elements in the original twalk, we have failed.
512
+ if rwalk.qids.length != twalk.wnames.length
513
+ raise StyxException.new(("#{path} no such file or directory"))
514
+ end
515
+ ropen = fid = nil
516
+ begin
517
+ # Next, walk to the basename from there, using a new fid
518
+ fid = @conn.get_free_fid
519
+ twalk = Message::Twalk.new(:fid => dfid, :newfid => fid)
520
+ twalk.path = basename
521
+ rwalk = @conn.send_message(twalk)
522
+ # Do a Topen if this succeeds
523
+ open_msg = Message::Topen.new(:fid => fid, :mode => mode)
524
+ ropen = @conn.send_message(open_msg)
525
+ @conn.tclunk(dfid)
526
+ rescue Exception => e
527
+ # If we are being directed to create the file if it doesn't
528
+ # already exist, send a Tcreate message, and use the response
529
+ # for it as the ropen (the two classes respond to exactly the
530
+ # same messages--ah the wonders of duck typing!). If not,
531
+ # or if that fails, this should propagate an exception upwards.
532
+ if create
533
+ # Alter the submitted permissions mask according to
534
+ # the connection's umask
535
+ perm &= ~(@conn.umask)
536
+ create_msg = Message::Tcreate.new(:fid => dfid, :name => basename,
537
+ :perm => perm, :mode => mode)
538
+ ropen = @conn.send_message(create_msg)
539
+ fid = dfid
540
+ else
541
+ raise e
542
+ end
543
+ end
566
544
 
567
- ##
568
- # Returns true if the buffer is full
569
- def full?
570
- return(@data.length == @bufsize)
545
+ # If we get here, we were able to successfully open or create
546
+ # the file.
547
+ @mode = mode
548
+ @fid = fid
549
+ @qid = ropen.qid
550
+ @iounit = ropen.iounit
551
+ # Determine if the file is actually a directory. Such a file
552
+ # would have its Qid.qtype high bit set to 1. In this case, instead
553
+ # of returning self, we return self wrapped in a Directory object.
554
+ retval = self
555
+ if twalk.wnames.length == 0 || (rwalk.qids[-1].qtype & 0x80 != 0)
556
+ retval = Directory.new(self)
557
+ end
558
+ if block_given?
559
+ begin
560
+ yield retval
561
+ ensure
562
+ retval.close
563
+ end
564
+ else
565
+ return(retval)
566
+ end
571
567
  end
572
568
 
573
569
  ##
574
- # How much data is needed to fill the buffer?
570
+ # Read the Stat information for the file.
575
571
  #
576
- def slack
577
- return(@bufsize - @data.length)
578
- end
579
-
580
- ##
581
- # Empty the buffer. Return all the data in the buffer
572
+ # returns: the RStyx::Message::Stat instance corresponding to
573
+ # the file
582
574
  #
583
- def empty
584
- retval = @data
585
- @data = ""
586
- return(retval)
575
+ def stat
576
+ rstat = @conn.send_message(Message::Tstat.new(:fid => @fid))
577
+ return(rstat.stat)
587
578
  end
588
579
 
589
580
  ##
590
- # Consume data from the buffer. Returns a string of at most +size+
591
- # length from the buffer.
581
+ # Close the file. This will flush all unwritten buffered data
582
+ # if any.
592
583
  #
593
- def consume(size)
594
- retval = @data[0..(size-1)]
595
- @data = @data[size..@data.length]
596
- if @data == nil # if the buffer goes empty
597
- @data = ""
584
+ def close
585
+ flush rescue nil
586
+ if @fid >= 0
587
+ # Clunk the fid
588
+ @conn.tclunk(@fid)
598
589
  end
599
- return(retval)
590
+ @fid = -1
591
+ @iounit = 0
592
+ @mode = -1
600
593
  end
601
594
 
602
- ##
603
- # Write data to the buffer. Returns the data that was not actually
604
- # written to the buffer (the excess) if the buffer is full.
605
- def fill(data)
606
- if data.length > slack
607
- excess = data[slack..(data.length)]
608
- data = data[0..(slack-1)]
609
- end
610
- @data << data
611
- return(excess)
595
+ def closed?
596
+ return(@mode < 0)
612
597
  end
613
598
 
614
- end
615
-
616
- ##
617
- # A file on a Styx server. This should probably not be instantiated
618
- # directly, but only by the use of Connection#open.
619
- #
620
- # TODO: This should later become a true subclass of IO.
621
- #
622
- class StyxIO
623
- attr_reader :name, :mode, :closed, :offset, :iounit, :fid
624
- attr_accessor :sync
625
-
626
- # Whether or not to buffer writes. Unlike regular IO objects,
627
- # StyxIO has this true by default.
628
- attr_accessor :sync
629
- include Enumerable
630
-
631
- def initialize(conn, name, fid, iounit, mode="r")
632
- @conn = conn
633
- @name = name
634
- @mode = mode
635
- @closed = false # closed flag
636
- @fid = fid
637
- @iounit = iounit # maximum payload of a Twrite or Rread message
638
- @offset = 0 # file offset
639
- @sync = true
640
-
641
- @buffer = Buffer.new(iounit) # the read buffer
642
- @boffset = 0 # the offset of the last buffered read or write
643
- @lastop = :none # the last operation performed
644
- @sync = false # Buffer or not buffer writes
645
- end
599
+ private
646
600
 
647
601
  ##
648
602
  # Read at most +size+ bytes from +offset+
649
603
  # If the size argument is negative or omitted, read until EOF.
650
- # This is an unbuffered read! This function should probably
651
- # not be used directly.
604
+ # This should probably not be used directly.
652
605
  #
653
606
  # +size+:: number of bytes to read from the file
654
607
  # +offset+:: the offset to read from.
655
608
  # return:: the data followed by the new offset
656
609
  #
657
- def sysread_int(size=-1, offset=0)
610
+ def _sysread(size=-1, offset=0)
658
611
  contents = ""
659
612
  bytes_to_read = size
660
613
  loop do
@@ -665,7 +618,10 @@ module RStyx
665
618
  else
666
619
  n = bytes_to_read
667
620
  end
668
- rread = @conn.send_message(Message::Tread.new(@fid, offset, n))
621
+ rread =
622
+ @conn.send_message(Message::Tread.new(:fid => @fid,
623
+ :offset => offset,
624
+ :count => n))
669
625
  if rread.data.length == 0
670
626
  break # EOF
671
627
  end
@@ -681,16 +637,17 @@ module RStyx
681
637
  ##
682
638
  #
683
639
  # Write data to the file at +offset+. No buffering is
684
- # performed. This function should probably not be used
685
- # directly.
640
+ # performed. This should probably not be used directly.
686
641
  #
687
- # +str+:: data to be written
642
+ # +d+:: data to be written
688
643
  # +offset+:: the offset to write at
689
644
  #
690
- # returns the new offset
645
+ # returns the new offset and the number of bytes written
691
646
  #
692
- def syswrite_int(str, offset)
647
+ def _syswrite(d, offset)
648
+ str = d.to_s
693
649
  pos = 0
650
+ count = 0
694
651
  loop do
695
652
  bytes_left = str.length - pos
696
653
  if bytes_left <= 0
@@ -700,256 +657,323 @@ module RStyx
700
657
  else
701
658
  n = bytes_left
702
659
  end
703
- rwrite = @conn.send_message(Message::Twrite.new(@fid, offset,
704
- str[pos..(pos+n)]))
705
- if rwrite.count != n
706
- raise StyxException.new("error writing data")
707
- end
660
+ rwrite = @conn.send_message(Message::Twrite.new(:fid => @fid,
661
+ :offset => offset,
662
+ :data => str[pos..(pos+n)]))
708
663
  pos += n
709
664
  offset += n
665
+ count += rwrite.count
710
666
  end
711
- return(offset)
667
+ return([offset, count])
712
668
  end
713
669
 
714
- public
715
-
716
670
  ##
717
- # closes the file. Once this has been done, no more read or write
718
- # operations should be possible.
671
+ # Add up to @iounit bytes to the read buffer.
719
672
  #
720
- def close
721
- flush # flush any bytes buffered
722
- rclunk = @conn.send_message(Message::Tclunk.new(@fid))
723
- # FIXME: abort all outstanding messages with flushes
673
+ def fill_rbuff
674
+ d, @rboffset = _sysread(@iounit, @rboffset)
675
+ if d.empty?
676
+ @eof = true
677
+ end
678
+ @rbuffer << d
724
679
  end
725
680
 
726
681
  ##
727
- # Read at most +size+ bytes from the current file position.
728
- # If the size argument is negative or omitted, read until EOF.
729
- # Returns nil if we read from the end of stream.
730
- #
731
- # This is a buffered read.
732
- #
733
- # +size+:: number of bytes to read from the file
682
+ # Consume +size+ bytes from the read buffer.
734
683
  #
735
- def read(size=-1)
736
- # flush any buffered data first, so the buffer becomes empty
737
- if @lastop == :write
738
- flush
684
+ def consume_rbuff(size=nil)
685
+ if @rbuffer.empty?
686
+ nil
687
+ else
688
+ size ||= @rbuffer.size
689
+ ret = @rbuffer[0, size]
690
+ @rbuffer[0, size] = ""
691
+ return(ret)
739
692
  end
740
- @lastop = :read
741
-
742
- bytes_to_read = size
743
- contents = ""
744
- loop do
745
- if @buffer.empty?
746
- # fill the buffer if the buffer goes empty
747
- rdata, @boffset = sysread_int(@buffer.slack, @boffset)
748
- if rdata.length == 0
749
- break
750
- end
751
- @buffer.fill(rdata)
752
- end
753
-
754
- d = ""
755
-
756
- if size < 0 || bytes_to_read > @buffer.bufsize
757
- d = @buffer.empty
758
- else
759
- d = @buffer.consume(bytes_to_read)
760
- end
693
+ end
761
694
 
762
- @offset += d.length
763
- contents << d
764
- if size >= 0
765
- bytes_to_read -= d.length
766
- end
695
+ public
767
696
 
768
- if bytes_to_read == 0
697
+ ##
698
+ # Read at most +size+ bytes from the Styx file or to the end of
699
+ # file if omitted. Returns nil if called at end of file.
700
+ #
701
+ def read(size=-1)
702
+ until @eof
703
+ # Fill up the buffer until we have at least the requested
704
+ # size, or until end of file if size is negative.
705
+ if size > 0 && size <= @rbuffer.size
769
706
  break
770
707
  end
708
+ fill_rbuff
771
709
  end
772
- if (contents.length == 0)
773
- return(nil)
710
+
711
+ # We managed to slurp the entire file!
712
+ if size < 0
713
+ size = @rbuffer.size
774
714
  end
775
- return(contents)
715
+
716
+ @offset += size
717
+ retval = consume_rbuff(size) || ""
718
+ (size && retval.empty?) ? nil : retval
776
719
  end
777
720
 
778
721
  ##
779
- # Do an unbuffered read.
722
+ # Reads the next "line" from the Styx file; lines are separated by
723
+ # +eol+. An +eol+ of nil reads the entire contents. Returns nil
724
+ # if called at end of file.
780
725
  #
781
- def sysread(size=-1)
782
- unless @buffer.empty?
783
- raise IOError.new("sysread for buffered IO")
726
+ def gets(eol=$/)
727
+ idx = @rbuffer.index(eol)
728
+ until @eof
729
+ if idx
730
+ break
731
+ end
732
+ fill_rbuff
733
+ idx = @rbuffer.index(eol)
784
734
  end
785
- data, @offset = sysread_int(size, @offset)
786
- if (data.length == 0)
787
- return(nil)
735
+ if eol.is_a?(Regexp)
736
+ size = idx ? idx+$&.size : nil
737
+ else
738
+ size = idx ? idx+eol.size : nil
788
739
  end
789
- return(data)
740
+ @offset += size
741
+ return(consume_rbuff(size))
790
742
  end
791
743
 
792
744
  ##
793
- # Calls the given block once for each byte (0..255) in the StyxIO,
794
- # passing the byte as an argument. The Styx file must be opened
795
- # for reading.
745
+ # Executes the block for evely line in the Styx file, where lines
746
+ # are separated by +eol+.
796
747
  #
797
- def each_byte
798
- if !block_given?
799
- raise LocalJumpError("no block given")
800
- end
801
-
802
- while (!(c=read(1)).nil?)
803
- yield c[0]
748
+ def each(eol=$/)
749
+ while line = self.gets(eol)
750
+ yield line
804
751
  end
805
752
  end
806
753
 
754
+ alias each_line each
755
+
807
756
  ##
808
- # Reads the next "line" from the Styx file; lines are separated by
809
- # sep_str. A separator of nil reads the entire contents, [and a
810
- # zero length separator reads the input a paragraph at a time (two
811
- # successive newlines in the input separate paragraphs) TODO: this
812
- # needs to be implemented]. The Styxfile
813
- # must be opened for reading. The line read in will be returned and
814
- # also assigned to $LAST_READ_LINE. Returns nil if called at end
815
- # of file.
816
- #
817
- def gets(sep_str=$RS)
818
- line = ""
819
- while (!(c=read(1)).nil?)
820
- line << c
821
- if c == $RS
822
- break
823
- end
824
- end
825
- if line.length == 0
826
- return(nil)
757
+ # Reads all of the lines in the Styx file, and returns them in an
758
+ # array. Lines are separated by an optional separator +eol+.
759
+ #
760
+ def readlines(eol=$/)
761
+ ary = []
762
+ while line = self.gets(eol)
763
+ ary << line
827
764
  end
828
- return(line)
765
+ ary
829
766
  end
830
767
 
831
768
  ##
832
- # Executes the passed block for every line in styxio, where lines
833
- # are separated by sep_str. The Styx file must be opened for
834
- # reading.
835
- #
836
- def each
837
- if !block_given?
838
- raise LocalJumpError("no block given")
839
- end
769
+ # Reads a line as with gets, but
770
+ def readline(eol=$/)
771
+ raise EOFError if eof?
772
+ return(gets(eol))
773
+ end
840
774
 
841
- while (!(line = gets).nil?)
842
- yield line
775
+ def getc
776
+ c = read(1)
777
+ return(c ? c[0] : nil)
778
+ end
779
+
780
+ def readchar
781
+ raise EOFError if eof?
782
+ getc
783
+ end
784
+
785
+ def ungetc(c)
786
+ @rbuffer[0,0] = c.chr
787
+ @offset -= 1
788
+ end
789
+
790
+ def eof?
791
+ if !@eof && @rbuffer.empty?
792
+ fill_rbuff
843
793
  end
794
+ return(@eof && @rbuffer.empty?)
844
795
  end
845
796
 
797
+ alias eof eof?
798
+
799
+ private
800
+
846
801
  ##
847
- # A buffered write of data to the file.
802
+ # Write data to the buffer if the buffer is not yet full, or
803
+ # if it has been determined (e.g. by an end of line marker) that
804
+ # we should flush the buffer, and actually write.
848
805
  #
849
- def write(data)
850
- if @lastop == :read
851
- @buffer.empty # empty the buffer
806
+ def do_write(s)
807
+ unless defined?(@wbuffer)
808
+ @wbuffer = ""
809
+ # we obviously start writing at the current offset
810
+ @wboffset = @offset
852
811
  end
853
- @lastop = :write
854
-
855
- # If the sync flag is set, simply write the data straight
856
- # to the file with no buffering at all.
857
- if @sync
858
- @offset = syswrite_int(data, @offset)
859
- return(data.length)
812
+ @wbuffer << s
813
+ @offset += s.length
814
+ #
815
+ # We flush the buffer if at least one of the following conditions
816
+ # has been met:
817
+ #
818
+ # 1. The sync flag is set to true.
819
+ # 2. The write buffer size has equalled or exceeded the connection's
820
+ # iounit.
821
+ # 3. The write buffer now contains an end of line marker, in which
822
+ # cas we flush only until the end of line marker.
823
+ #
824
+ if @sync || @wbuffer.size >= @iounit || (idx = @wbuffer.rindex($/))
825
+ remain = idx ? idx + $/.size : @wbuffer.length
826
+ nwritten = 0
827
+ ofs = @wboffset
828
+ while remain > 0
829
+ str = @wbuffer[nwritten, remain]
830
+ ofs, nwrote = _syswrite(str, ofs)
831
+ remain -= nwrote
832
+ nwritten += nwrote
833
+ end
834
+ @wbuffer[0, nwritten] = ""
835
+ @wboffset += nwritten
860
836
  end
837
+ end
838
+
839
+ public
840
+
841
+ def write(s)
842
+ do_write(s)
843
+ return(s.length)
844
+ end
845
+
846
+ def <<(s)
847
+ do_write(s)
848
+ return(self)
849
+ end
861
850
 
862
- # If the sync flag is not set, try to write the data to the buffer
863
- # until the buffer becomes full.
864
- bytes_written = data.length
865
- while !data.nil?
866
- if @buffer.full?
867
- # flush the buffer if the buffer fills up
868
- flush
851
+ def puts(*args)
852
+ s = ""
853
+ if args.empty?
854
+ s << "\n"
855
+ end
856
+ args.each do |arg|
857
+ s << arg.to_s
858
+ if $/ && /\n\z/ !~ s
859
+ s << "\n"
869
860
  end
870
- data = @buffer.fill(data)
871
861
  end
872
- @offset += bytes_written
873
- return(bytes_written)
862
+ do_write(s)
863
+ return(nil)
864
+ end
865
+
866
+ def print(*args)
867
+ s = ""
868
+ args.each{ |arg| s << arg.to_s }
869
+ do_write(s)
870
+ return(nil)
871
+ end
872
+
873
+ def printf(s, *args)
874
+ do_write(s % args)
875
+ return(nil)
874
876
  end
875
877
 
876
- ##
877
- # Flushes any buffered data
878
- #
879
878
  def flush
880
- if @lastop == :write
881
- @boffset = syswrite_int(@buffer.empty, @boffset)
882
- return(self)
883
- end
879
+ osync = @sync
880
+ @sync = true
881
+ do_write ""
882
+ @sync = osync
884
883
  end
885
884
 
886
885
  ##
887
- # Move to a new seek position
886
+ # Seek in the file. The whence values may be one of SEEK_SET
887
+ # SEEK_CUR, or SEEK_END, as defined in rstyx/common, and they
888
+ # result in the offset being taken from the beginning of the
889
+ # file, relative to the current offset, or from the end of the
890
+ # file respectively.
888
891
  #
889
- # TODO: implement :seek_end
892
+ # XXX: A seek, no matter where it goes, will always invalidate
893
+ # any buffering. This is fine for write buffers, but is
894
+ # somewhat wasteful for the read buffers.
890
895
  #
891
- def seek(offset, whence=:seek_set)
892
- # flush the buffers before doing the seek
896
+ def seek(offset, whence)
897
+ # Before seeking, flush the write buffers
893
898
  flush
899
+ s = self.stat
894
900
  case whence
895
- when :seek_set: @offset = offset
896
- when :seek_cur: @offset += offset
897
- # FIXME: do :seek_end
898
- when :seek_end: raise StyxException.new("Can't seek from end of file")
899
- else raise StyxException.new("whence must be :seek_set, :seek_cur, or :seek_end")
901
+ when 0
902
+ @offset = offset
903
+ when 1
904
+ @offset += offset
905
+ when 2
906
+ # We have to obtain the stat of the file to do this kind of seek.
907
+ @offset = s.length + offset
908
+ else
909
+ raise StyxException.new("Invalid seek")
900
910
  end
901
- @boffset = @offset
902
- @buffer.empty
911
+ # After seeking, discard the read buffers
912
+ @rbuffer = ""
913
+ @rboffset = @offset
914
+ @eof = (@offset >= s.length)
915
+ return(@offset)
916
+ end
917
+
918
+ def tell
919
+ return(@offset)
903
920
  end
904
921
 
905
- ##
906
- # Seek to the start of the file
907
- #
908
922
  def rewind
909
- seek(0, :seek_set)
923
+ seek(0, 0)
910
924
  end
911
925
 
912
926
  ##
913
- # Return the current file position
927
+ # Reads +size+ bytes from the Styx file and returns them as a string.
928
+ # Do not mix with other methods that read from the Styx file or you
929
+ # may get unpredictable results.
914
930
  #
915
- def tell
916
- return @offset
931
+ def sysread(size=-1)
932
+ data, @offset = _sysread(size, @offset)
933
+ if data.length == 0
934
+ return(nil)
935
+ end
936
+ return(data)
917
937
  end
918
938
 
919
939
  ##
920
- # Give the DirEntry corresponding to this file
921
- #
922
- def stat
923
- rstat = @conn.send_message(Message::Tstat.new(@fid))
924
- return(rstat.stat)
940
+ # Writes +data+ to the Styx file. Returns the number of bytes written.
941
+ # do not mix with other methods that write to the Styx file or you
942
+ # may get unpredictable results.
943
+ def syswrite(data)
944
+ @offset, count = _syswrite(data, @offset)
945
+ return(count)
925
946
  end
926
947
 
927
- end
948
+ end # class File
928
949
 
929
950
  ##
930
- # A directory on a Styx server. This obtains the entries inside
931
- # a directory. This actually delegates to StyxIO.
951
+ # Styx directory. This obtains the entries inside a directory, and
952
+ # works by delegating to File.
932
953
  #
933
- class StyxDir
954
+ class Directory
934
955
  include Enumerable
935
956
 
936
- def initialize(conn, name, fid, iounit, mode="r")
937
- @io = StyxIO.new(conn, name, fid, iounit, mode)
957
+ def initialize(fp)
958
+ @io = fp
938
959
  # directory entry buffer
939
960
  @read_direntries = []
961
+ # byte buffer
962
+ @data = ""
940
963
  end
941
964
 
942
965
  def close
943
966
  @io.close
944
967
  end
945
968
 
946
- ##
947
- # get the fid corresponding to this directory
948
- #
949
969
  def fid
950
970
  @io.fid
951
971
  end
952
972
 
973
+ def qid
974
+ @io.qid
975
+ end
976
+
953
977
  ##
954
978
  # Read the next directory entry from the dir and return the file
955
979
  # name as a string. Returns nil at the end of stream.
@@ -961,18 +985,25 @@ module RStyx
961
985
  return(@read_direntries.shift.name)
962
986
  end
963
987
  # read iounit bytes from the directory--this must be unbuffered
964
- data = @io.read(@io.iounit)
988
+ d = @io.sysread
989
+ if d.nil?
990
+ return(nil)
991
+ end
992
+ @data << d
965
993
 
966
- if (data.nil?)
994
+ if (@data.empty?)
967
995
  return(nil)
968
996
  end
969
997
 
970
998
  # decode the directory entries in the iounit
971
- while data.length != 0
972
- delen = data[0..1].unpack("v")[0]
973
- edirent = data[0..(delen + 1)]
974
- data = data[(delen + 2)..(data.length)]
975
- @read_direntries << Message::DirEntry.decode(edirent)
999
+ loop do
1000
+ delen = @data.unpack("v")[0]
1001
+ if delen.nil? || delen + 1 > @data.length
1002
+ break
1003
+ end
1004
+ edirent = @data[0..(delen + 1)]
1005
+ @data = @data[(delen + 2)..-1]
1006
+ @read_direntries << Message::Stat.from_bytes(edirent)
976
1007
  end
977
1008
  de = @read_direntries.shift
978
1009
  if (de.nil?)
@@ -982,36 +1013,21 @@ module RStyx
982
1013
  end
983
1014
 
984
1015
  ##
985
- # Seek to the beginning of the directory. This is the only type of
986
- # seek allowed.
987
- #
988
- def rewind
989
- @io.rewind
990
- end
991
-
992
- ##
993
- # same as StyxIO#stat
994
- #
995
- def stat
996
- return(@io.stat)
997
- end
998
-
999
- ##
1000
- # Call the block once for each entry in the directory, passing the
1001
- # filename of each entry as a parameter to the block.
1016
+ # Call the block once for each entry in the directory, passing
1017
+ # the filename of each entry as a parameter to the block.
1002
1018
  #
1003
1019
  def each
1004
1020
  if !block_given?
1005
- raise LocalJumpError("no block given")
1021
+ raise LocalJumpError.new("no block given")
1006
1022
  end
1007
1023
 
1008
1024
  self.rewind
1009
- while (!(de = self.read).nil?)
1025
+ until (de = read).nil?
1010
1026
  yield de
1011
1027
  end
1012
1028
  end
1013
1029
 
1014
- end
1030
+ end # class Directory
1015
1031
 
1016
1032
  end # module Client
1017
1033