rstyx 0.3.1 → 0.3.2

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/server.rb CHANGED
@@ -36,7 +36,7 @@
36
36
  # Copyright:: Copyright (c) 2005-2007 Rafael R. Sevilla
37
37
  # License:: GNU Lesser General Public License
38
38
  #
39
- # $Id: server.rb 234 2007-08-14 11:17:13Z dido $
39
+ # $Id: server.rb 247 2007-09-13 09:16:31Z dido $
40
40
  #
41
41
  require 'thread'
42
42
  require 'monitor'
@@ -45,6 +45,7 @@ require 'rubygems'
45
45
  require 'eventmachine'
46
46
  require 'logger'
47
47
  require 'socket'
48
+ require 'etc'
48
49
  require 'rstyx/common'
49
50
  require 'rstyx/messages'
50
51
  require 'rstyx/errors'
@@ -89,8 +90,8 @@ module RStyx
89
90
  ##
90
91
  # Handle version messages. This handles the version negotiation.
91
92
  # At this point, the only version of the protocol supported is
92
- # 9P2000: all other version strings result in an error being
93
- # returned to the client. A successful Tversion/Rversion
93
+ # 9P2000: all other version strings result in the server returning
94
+ # 'unknown' in its Rversion. A successful Tversion/Rversion
94
95
  # negotiation results in the protocol_negotiated flag in the
95
96
  # current session becoming true, and all other outstanding I/O
96
97
  # on the session (e.g. opened fids and the like) all removed.
@@ -103,8 +104,13 @@ module RStyx
103
104
  @cversion = msg.version
104
105
  @cmsize = msg.msize
105
106
  if @cversion != "9P2000"
106
- # Unsupported protocol version
107
- return(Message::Rerror.new(:ename => "Unsupported protocol version #{@cversion} (must be 9P2000)"))
107
+ # Unsupported protocol version. As per Inferno's version(5):
108
+ #
109
+ # If the server does not understand the client's version
110
+ # string, it should respond with an Rversion message (not
111
+ # Rerror) with the _version_ string the 7 characters 'unknown'.
112
+ #
113
+ return(Message::Rversion.new(:version => "unknown", :msize => 0))
108
114
  end
109
115
  # Reset the session, which also causes the protocol negotiated
110
116
  # flag in the session to be set to true.
@@ -310,7 +316,7 @@ module RStyx
310
316
  # Create the file in the directory. Note that SDirectory#newfile
311
317
  # has to do all of the permission checking and all that.
312
318
  new_file = dir.newfile(msg.name, msg.perm)
313
- dir << new_file
319
+ o dir << new_file
314
320
  @session[msg.fid] = new_file
315
321
  new_file.add_client(SFileClient.new(@session, msg.fid, msg.mode))
316
322
  return(Message::Rcreate.new(:qid => new_file.qid,
@@ -589,10 +595,38 @@ module RStyx
589
595
  # Session state of a Styx connection.
590
596
  #
591
597
  class Session < Monitor
592
-
593
- attr_accessor :msize, :auth, :fids, :tags, :version_negotiated, :user
598
+ ##
599
+ # Maximum message size to be used by this session, based on
600
+ # the lesser of the server's and the client's msize.
601
+ #
602
+ attr_accessor :msize
603
+ ##
604
+ # Authenticated flag
605
+ attr_accessor :auth
606
+ ##
607
+ # List of active fids on this session
608
+ attr_accessor :fids
609
+ ##
610
+ # List of active tags on this session
611
+ attr_accessor :tags
612
+ ##
613
+ # Flag which is true if version negotiation has been performed
614
+ # on this session
615
+ attr_accessor :version_negotiated
616
+ ##
617
+ # User this connection has authenticated against
618
+ #
619
+ attr_accessor :user
620
+ ##
621
+ # Active iounit for this connection
622
+ #
594
623
  attr_accessor :iounit
595
624
 
625
+ ##
626
+ # Create a new session.
627
+ #
628
+ # _conn_:: The connection object (Server mixin)
629
+ #
596
630
  def initialize(conn)
597
631
  @conn = conn
598
632
  @version_negotiated = false
@@ -603,10 +637,24 @@ module RStyx
603
637
  @tags = []
604
638
  end
605
639
 
640
+ ##
641
+ # Return true if the session peer has completed version negotiation
642
+ #
606
643
  def version_negotiated?
607
644
  return(@version_negotiated)
608
645
  end
609
646
 
647
+ ##
648
+ # Reset the session, setting version negotiation flag and
649
+ # iounit.
650
+ #
651
+ #--
652
+ # FIXME: should clunk all outstanding fids and release all outstanding
653
+ # tags on this connection
654
+ #++
655
+ #
656
+ # _msize_:: the maximum message size for this connection
657
+ #
610
658
  def reset_session(msize)
611
659
  # XXX: clunk all outstanding fids and release all outstanding tags
612
660
  @version_negotiated = true
@@ -617,6 +665,9 @@ module RStyx
617
665
  # Associates a FID with a file. The FID passed must be checked before
618
666
  # using this or the old FID will be forgotten.
619
667
  #
668
+ # _fid_:: the fid to be associated
669
+ # _file_:: the SFile to associate with _fid_
670
+ #
620
671
  def []=(fid, file)
621
672
  @fids.delete(fid)
622
673
  @fids[fid] = file
@@ -626,6 +677,8 @@ module RStyx
626
677
  # Gets the file associated with the indexed FID. Raises a
627
678
  # FidNotFoundException if the fid is not present.
628
679
  #
680
+ # _fid_:: the fid to obtain the associated SFile instance of
681
+ #
629
682
  def [](fid)
630
683
  unless has_fid?(fid)
631
684
  raise FidNotFoundException.new(fid)
@@ -633,10 +686,18 @@ module RStyx
633
686
  return(@fids[fid])
634
687
  end
635
688
 
689
+ ##
690
+ # Returns true if _fid_ is associated with some file on this
691
+ # session.
692
+ #
636
693
  def has_fid?(fid)
637
694
  return(@fids.has_key?(fid))
638
695
  end
639
696
 
697
+ ##
698
+ # Clunk _fid_, i.e. make the server forget about the fid assignment
699
+ # for this connection.
700
+ #
640
701
  def clunk(fid)
641
702
  unless @fids.has_key?(fid)
642
703
  raise FidNotFoundException.new(fid)
@@ -658,6 +719,9 @@ module RStyx
658
719
  end
659
720
  end
660
721
 
722
+ ##
723
+ # Clunk all outstanding fids on this connection.
724
+ #
661
725
  def clunk_all
662
726
  @fids.each_key do |k|
663
727
  begin
@@ -668,14 +732,17 @@ module RStyx
668
732
  end
669
733
  end
670
734
 
735
+ ##
736
+ # Returns true if _tag_ is associated with some active message
737
+ #
671
738
  def has_tag?(tag)
672
739
  return(!@tags.index(tag).nil?)
673
740
  end
674
741
 
675
742
  ##
676
- # Adds the given tag to the list of tags in use, first checking to
677
- # see if it is already in use. Raises a TagInUseException if the
678
- # tag is already in use.
743
+ # Adds the given _tag_ to the list of tags in use, first checking to
744
+ # see if it is already in use. Raises a TagInUseException if _tag_
745
+ # is already in use.
679
746
  #
680
747
  def add_tag(tag)
681
748
  if has_tag?(tag)
@@ -687,13 +754,18 @@ module RStyx
687
754
  alias << add_tag
688
755
 
689
756
  ##
690
- # Called when a message is replied to, releasing the tag.
757
+ # Called when a message is replied to, releasing _tag_ so it can
758
+ # be used again.
759
+ #
691
760
  def release_tag(tag)
692
761
  @tags.delete(tag)
693
762
  end
694
763
 
695
764
  alias flush_tag release_tag
696
765
 
766
+ ##
767
+ # Flush all outstanding tags on this session.
768
+ #
697
769
  def flush_all
698
770
  @tags.each do |f|
699
771
  flush_tag(t)
@@ -702,11 +774,13 @@ module RStyx
702
774
 
703
775
  ##
704
776
  # Check the permissions for a given mode
705
- # +sf+ the file to check against
706
- # +mode+ the mode to check (OEXEC, OWRITE, or OREAD)
707
777
  #
708
- # XXX: the permissions are only for anonymous access at the
778
+ #--
779
+ # FIXME: the permissions are only for anonymous access at the
709
780
  # moment, so only the world permissions are ever checked.
781
+ #++
782
+ # _sf_:: the file to check against
783
+ # _mode_:: the mode to check (OEXEC, OWRITE, or OREAD)
710
784
  #
711
785
  def permission?(sf, mode)
712
786
  if mode < 0 || mode > 2
@@ -726,15 +800,19 @@ module RStyx
726
800
  end
727
801
 
728
802
  ##
729
- # Check for executable permission for the styx file
803
+ # Check for executable permission for the SFile _sf_.
730
804
  #
731
805
  def execute?(sf)
806
+ return(true)
732
807
  end
733
808
 
734
809
  ##
735
810
  # Checks that the given file can be opened with the given mode.
736
811
  # Raises a StyxException if this is not possible.
737
812
  #
813
+ # _sf_:: the SFile to be opened
814
+ # _mode_:: the open mode
815
+ #
738
816
  def confirm_open(sf, mode)
739
817
  if sf.exclusive? && sf.num_clients != 0
740
818
  raise StyxException.new("can't open locked file")
@@ -745,7 +823,24 @@ module RStyx
745
823
 
746
824
  end # class Session
747
825
 
826
+ ##
827
+ # Base server class. This does nothing really useful, instantiate
828
+ # subclasses such as TCPServer instead.
829
+ #
748
830
  class Server
831
+ ##
832
+ # Create a new server. The _config_ hash contains the server
833
+ # configuration. The configuration options recognized by all
834
+ # Styx server subclasses are:
835
+ #
836
+ # _:root_:: The root directory of the filesystem you want to
837
+ # serve (typically an SDirectory instance)
838
+ # _:log_:: A Logger object where server-generated log messages
839
+ # are stored.
840
+ # _:debug_:: Debug level, which is assigned to the logger's level
841
+ # Set this to Logger::DEBUG if you want full debugging
842
+ # messages to appear.
843
+ #
749
844
  def initialize(config)
750
845
  @root = config[:root]
751
846
  @log = config[:log] || Logger.new(STDERR)
@@ -754,6 +849,9 @@ module RStyx
754
849
 
755
850
  protected
756
851
 
852
+ ##
853
+ # Start a new server. This is overriden by subclasses.
854
+ #
757
855
  def start_server
758
856
  end
759
857
 
@@ -762,6 +860,7 @@ module RStyx
762
860
  ##
763
861
  # Start the Styx server, returning the thread of the
764
862
  # running Styx server instance.
863
+ #
765
864
  def run
766
865
  t = Thread.new do
767
866
  @log.info("starting")
@@ -773,6 +872,14 @@ module RStyx
773
872
  end # class Server
774
873
 
775
874
  class TCPServer < Server
875
+ ##
876
+ # Create a new TCP-based server. In addition to the options described
877
+ # in the Server superclass, the following further options are also
878
+ # available:
879
+ #
880
+ # _:bindaddr_:: The address that the Styx server should listen on
881
+ # _:port_:: The port that the Styx server should listen on
882
+ #
776
883
  def initialize(config)
777
884
  @bindaddr = config[:bindaddr]
778
885
  @port = config[:port]
@@ -780,6 +887,10 @@ module RStyx
780
887
  end
781
888
 
782
889
  protected
890
+
891
+ ##
892
+ # Start a TCP-based server using EventMachine.
893
+ #
783
894
  def start_server
784
895
  EventMachine::run do
785
896
  @log.info("TCP server on #{@bindaddr}:#{@port}")
@@ -798,16 +909,31 @@ module RStyx
798
909
  # a client opens a file.
799
910
  #
800
911
  class SFileClient
801
- attr_reader :session, :fid, :mode
802
- attr_accessor :offset, :next_file_to_read
912
+ ##
913
+ # The session for which this SFileClient was created
914
+ attr_reader :session
915
+ ##
916
+ # The fid which the client used to open the file in question
917
+ attr_reader :fid
918
+ ##
919
+ # The mode under which the client opened the file in question
920
+ attr_reader :mode
921
+ ##
922
+ # When a client reads from or writes to file, this records the
923
+ # new offset
924
+ attr_accessor :offset
925
+ ##
926
+ # Used when reading a directory: stores the index of the next
927
+ # child of an SFile to include in an RreadMessage.
928
+ attr_accessor :next_file_to_read
803
929
 
804
930
  ##
805
931
  # Create a new SFileClient.
806
932
  #
807
- # +session+:: The session object associated with the client.
808
- # +fid+:: The client's handle to the file. Note that clients may
933
+ # _session_:: The session object associated with the client.
934
+ # _fid_:: The client's handle to the file. Note that clients may
809
935
  # use many fids opened representing the same file.
810
- # +mode+:: The mode field as received from the client's Topen
936
+ # _mode_:: The mode field as received from the client's Topen
811
937
  # message (including the OTRUNC and ORCLOSE bits)
812
938
  #
813
939
  def initialize(session, fid, mode)
@@ -816,18 +942,22 @@ module RStyx
816
942
  @truncate = ((mode & OTRUNC) == OTRUNC)
817
943
  @orclose = ((mode & ORCLOSE) == ORCLOSE)
818
944
  @mode = mode & 0x03 # mask off all but the last two bits
819
- # When a client reads from or writes to file, this records the
820
- # new offset
821
945
  @offset = 0
822
- # Used when reading a directory: stores the index of the next
823
- # child of an SFile to include in an RreadMessage.
824
946
  @next_file_to_read = 0
825
947
  end
826
948
 
949
+ ##
950
+ # Returns true if the Styx file was opened by the client in the
951
+ # OTRUNC mode.
952
+ #
827
953
  def truncate?
828
954
  return(@truncate)
829
955
  end
830
956
 
957
+ ##
958
+ # Returns true if the Styx file was opened by the client in the
959
+ # ORCLOSE mode (i.e. the client wants the file deleted on clunk).
960
+ #
831
961
  def orclose?
832
962
  return(@orclose)
833
963
  end
@@ -836,7 +966,7 @@ module RStyx
836
966
 
837
967
  ##
838
968
  # Check to see if the client can read the file (i.e. the client
839
- # opened it with read access mode)
969
+ # opened it with read access mode).
840
970
  #
841
971
  def readable?
842
972
  return(mode == OREAD || mode == ORDWR)
@@ -864,23 +994,51 @@ module RStyx
864
994
  # underlying operating system cannot be represented.
865
995
  #
866
996
  class SFile < Monitor
867
-
868
- attr_reader :name, :uid, :gid, :muid, :mtime
869
- attr_accessor :permissions, :atime, :parent, :version
997
+ ##
998
+ # File name; must be / if the file is the root directory of the server
999
+ #
1000
+ attr_reader :name
1001
+ ##
1002
+ # Owner name
1003
+ attr_reader :uid
1004
+ ##
1005
+ # Group name
1006
+ #
1007
+ attr_reader :gid
1008
+ ##
1009
+ # Name of the user who last modified the file
1010
+ #
1011
+ attr_reader :muid
1012
+ ##
1013
+ # Last modification time
1014
+ attr_reader :mtime
1015
+ ##
1016
+ # Permissions and flags
1017
+ attr_accessor :permissions
1018
+ ##
1019
+ # Last access time
1020
+ attr_accessor :atime
1021
+ ##
1022
+ # Parent directory which contains this file
1023
+ #
1024
+ attr_accessor :parent
1025
+ ##
1026
+ # Version number for the given path
1027
+ attr_accessor :version
870
1028
 
871
1029
  ##
872
- # Create a new file object with the given permissions.
873
- # This accepts a hash with the following keys:
1030
+ # Create a new file object with name _name_. This accepts a hash
1031
+ # _argv_ for the file's other parameters with the following keys:
874
1032
  #
875
- # name:: the name of the file
876
- # permissions:: The permissions of the file (e.g. 0755 in octal)
877
- # apponly:: true if the file is append only
878
- # excl:: true if the file is for exclusive use, i.e. only one
879
- # client at a time may open.
880
- # user:: the username of the owner of the file. If not specified
881
- # gets the value from an environment variable.
882
- # group:: the group name of the owner of the file. If not specified
883
- # gets the value from an environment variable.
1033
+ # _:permissions_:: The permissions of the file (e.g. 0755 in octal).
1034
+ # the default is 0666.
1035
+ # _:apponly_:: true if the file is append only. Default is false.
1036
+ # _:excl_:: true if the file is for exclusive use, i.e. only one
1037
+ # client at a time may open. Default is false.
1038
+ # _:user_:: the username of the owner of the file. If not specified
1039
+ # gets the value from the environment variable USER.
1040
+ # _:group_:: the group name of the owner of the file. If not specified
1041
+ # gets the value from the environment variable GROUP.
884
1042
  #
885
1043
  #
886
1044
  def initialize(name, argv={ :permissions => 0666, :apponly => false,
@@ -1017,7 +1175,7 @@ module RStyx
1017
1175
  # of the file to be changed. This is called when the server receives
1018
1176
  # a Twstat message. This default implementation does nothing.
1019
1177
  #
1020
- # +newmode+: the new mode of the file (permissions plus any other flags
1178
+ # _newmode_: the new mode of the file (permissions plus any other flags
1021
1179
  # such as DMDIR, etc.)
1022
1180
  #
1023
1181
  def can_setmode?(newmode)
@@ -1036,12 +1194,18 @@ module RStyx
1036
1194
  return(newmode)
1037
1195
  end
1038
1196
 
1197
+ ##
1198
+ # Returns the Qid of this file.
1199
+ #
1039
1200
  def qid
1040
1201
  t = filetype() >> 24 & 0xff
1041
1202
  q = Message::Qid.new(t, @version, self.uuid)
1042
1203
  return(q)
1043
1204
  end
1044
1205
 
1206
+ ##
1207
+ # Returns a Stat object for this file.
1208
+ #
1045
1209
  def stat
1046
1210
  s = Message::Stat.new
1047
1211
  s.dtype = s.dev = 0
@@ -1076,14 +1240,32 @@ module RStyx
1076
1240
  def length=(newlength)
1077
1241
  end
1078
1242
 
1243
+ ##
1244
+ # Check to see if the modification time of this file can be changed
1245
+ # to the given value. If this does not throw an exception then
1246
+ # set_mtime should always succeed.
1079
1247
  def can_setmtime?(nmtime)
1248
+ return(true)
1080
1249
  end
1081
1250
 
1251
+ ##
1252
+ # Sets the mtime of the file. The usual disclaimers about permissions
1253
+ # and SFile#can_setmtime? apply. Default implementation will simply
1254
+ # set the modification time to _nmtime_ and the muid to _uid_.
1255
+ #
1082
1256
  def set_mtime(nmtime, uid)
1083
1257
  @mtime = nmtime
1084
1258
  @muid = uid
1085
1259
  end
1086
1260
 
1261
+ ##
1262
+ # Rename the file to _newname_. This will raise a StyxException if
1263
+ #
1264
+ # 1. An attempt is made to rename a file representing the root
1265
+ # directory.
1266
+ # 2. An attempt is made to rename a file to a name of some other
1267
+ # file already present in the same directory.
1268
+ #
1087
1269
  def rename(newname)
1088
1270
  if @parent == nil
1089
1271
  raise StyxException.new("Cannot change the name of the root directory")
@@ -1094,6 +1276,7 @@ module RStyx
1094
1276
  @name = newname
1095
1277
  end
1096
1278
 
1279
+ ##
1097
1280
  # Gets the unique numeric ID for the path of this file (generated from
1098
1281
  # the low-order bytes of the creation time and the hashcode of the full
1099
1282
  # path). If the file is deleted and re-created the unique ID will
@@ -1113,6 +1296,11 @@ module RStyx
1113
1296
  # should override this to provide the desired behavior when the
1114
1297
  # file is read.
1115
1298
  #
1299
+ # _client_:: the SFileClient object representing the client reading
1300
+ # from this file.
1301
+ # _offset_:: the offset the client wants to read from
1302
+ # _count_:: the number of bytes that the client wishes to read
1303
+ #
1116
1304
  def read(client, offset, count)
1117
1305
  raise StyxException.new("Cannot read from this file")
1118
1306
  end
@@ -1122,22 +1310,39 @@ module RStyx
1122
1310
  # Writes data to this file. This method should be overriden by
1123
1311
  # subclasses to provide the desired behavior when the file is
1124
1312
  # written to. It should return the number of bytes actually
1125
- # "written".
1313
+ # written.
1314
+ #
1315
+ # _client_:: the SFileClient object representing the client writing
1316
+ # to this file
1317
+ # _offset_:: the offset the client wants to write to
1318
+ # _data_:: the data that the client wishes to write
1319
+ # _truncate_:: true or false depending on whether the file is to be
1320
+ # truncated.
1126
1321
  #
1127
1322
  def write(client, offset, data, truncate)
1128
1323
  raise StyxException.new("Cannot write to this file")
1129
1324
  end
1130
1325
 
1326
+ ##
1327
+ # Remove the file from the Styx server. This will simply remove
1328
+ # the file from the parent directory.
1131
1329
  #
1132
- # Remove the file from the Styx server
1133
1330
  def remove
1134
1331
  self.delete
1135
1332
  self.parent.remove_child(self)
1136
1333
  end
1137
1334
 
1335
+ ##
1336
+ # Any pre-deletion actions must be performed in this method.
1337
+ #
1138
1338
  def delete
1139
1339
  end
1140
1340
 
1341
+ ##
1342
+ # Add a client to the list of clients reading this file.
1343
+ #
1344
+ # _cl_:: an SFileClient instance representing the client reading
1345
+ # the file
1141
1346
  def add_client(cl)
1142
1347
  @clients.synchronize { @clients << cl }
1143
1348
  self.client_connected(cl)
@@ -1149,6 +1354,11 @@ module RStyx
1149
1354
  ##
1150
1355
  # Get the client connection to this file
1151
1356
  #
1357
+ # _sess_:: the client session in question
1358
+ # _fid_:: the fid that the client is using to access the file.
1359
+ # returns:: the SFileClient instance representing that file access,
1360
+ # or nil if there is no such client connection.
1361
+ #
1152
1362
  def client(sess, fid)
1153
1363
  @clients.synchronize do
1154
1364
  @clients.each do |cl|
@@ -1160,13 +1370,20 @@ module RStyx
1160
1370
  return(nil)
1161
1371
  end
1162
1372
 
1373
+ ##
1374
+ # Return the number of clients accessing this file.
1375
+ #
1163
1376
  def num_clients
1164
1377
  @clients.synchronize do
1165
1378
  remove_dead_clients
1166
1379
  return(@clients.length)
1167
1380
  end
1168
1381
  end
1169
-
1382
+
1383
+ ##
1384
+ # Remove clients which are no longer really using the file.
1385
+ # If a client session is either gone or the session is no longer
1386
+ # connected, it removes the client from the list.
1170
1387
  def remove_dead_clients
1171
1388
  @clients.synchronize do
1172
1389
  @clients.each do |clnt|
@@ -1177,6 +1394,9 @@ module RStyx
1177
1394
  end
1178
1395
  end
1179
1396
 
1397
+ ##
1398
+ # Remove the client _cl_ from the list of clients accessing the file.
1399
+ #
1180
1400
  def remove_client(cl)
1181
1401
  unless cl.nil?
1182
1402
  @clients.delete(cl)
@@ -1184,13 +1404,30 @@ module RStyx
1184
1404
  end
1185
1405
  end
1186
1406
 
1407
+ ##
1408
+ # Add any custom behavior for the file that has to happen whenever
1409
+ # a client disconnects from the file here.
1410
+ #
1187
1411
  def client_disconnected(cl)
1188
1412
  end
1189
1413
 
1414
+ ##
1415
+ # Add a change listener to the list of change listeners of this
1416
+ # file. The change listener will execute whenever some modification
1417
+ # is made to the file, and is passed the SFile instance to which it
1418
+ # is attached as a parameter.
1419
+ #
1420
+ # _block_:: the change listener block
1421
+ #
1190
1422
  def add_changelistener(&block)
1191
1423
  @changelisteners << block
1192
1424
  end
1193
1425
 
1426
+ ##
1427
+ # Method executed whenever the contents of the file change. This
1428
+ # increments the file's version on the server, and executes any
1429
+ # change listeners active on the file.
1430
+ #
1194
1431
  def contents_changed
1195
1432
  version_incr
1196
1433
  @changelisteners.each do |listener|
@@ -1198,29 +1435,72 @@ module RStyx
1198
1435
  end
1199
1436
  end
1200
1437
 
1438
+ ##
1439
+ # Increment the file's version. This wraps after the version goes
1440
+ # above 2^64.
1441
+ #
1201
1442
  def version_incr
1202
1443
  @version = ((@version + 1) & 0xffffffffffffffff)
1203
1444
  end
1204
1445
 
1446
+ ##
1447
+ # Return a Message::Rwrite for a successful write of _count_ bytes
1448
+ # from _session_. A write method for a subclass should use this
1449
+ # method (which updates mtime, atime, and calls contents_changed
1450
+ # callbacks) instead of manually returning a Message::Rwrite
1451
+ #
1205
1452
  def reply_write(count, session)
1206
- self.set_mtime(Time.now, session.user)
1453
+ @atime = Time.now
1454
+ self.set_mtime(@atime, session.user)
1207
1455
  self.contents_changed
1208
1456
  return(Message::Rwrite.new(:count => count))
1209
1457
  end
1210
1458
 
1459
+ ##
1460
+ # Return a Message::Rread for a successful read of _data_. This
1461
+ # updates access time and should be used instead of manually
1462
+ # returning a Message::Rread.
1463
+ #
1464
+ def reply_read(data)
1465
+ @atime = Time.now
1466
+ return(Message::Rread.new(:data => data))
1467
+ end
1468
+
1469
+ ##
1470
+ # Refreshes this file (if it represents another entity, such as a
1471
+ # file on disk, this method is used to make sure that the file
1472
+ # metadata (length, access time, etc.) are up to date. This default
1473
+ # implementation does nothing; subclasses must override this to
1474
+ # provide the correct functionality.
1475
+ #
1211
1476
  def refresh
1212
1477
  end
1213
1478
  end # class SFile
1214
1479
 
1480
+ ##
1481
+ # Class representing a directory on the Styx server.
1482
+ #
1215
1483
  class SDirectory < SFile
1216
- def initialize(name, argv={ :permissions => 777, :uid => ENV["USER"],
1217
- :gid => ENV["GROUP"] })
1484
+ ##
1485
+ # Create a new directory with name _name_. Permissions are obtained
1486
+ # from the _argv_ hash as with SFile. The default permissions are
1487
+ # 0777 though. In addition to the usual file arguments in _argv_,
1488
+ # SDirectory instances also recognize a _:filemaker_ key which
1489
+ # specifies a block that may be called whenever a file is created.
1490
+ # The block receives as parameters the SDirectory instance, the
1491
+ # name of the file to create, and the permissions of the file. It
1492
+ # should return an SFile subclass instance, which becomes the new
1493
+ # file on success, or raise a StyxException if there is some problem.
1494
+ #
1495
+ def initialize(name, argv={ :permissions => 0777, :uid => ENV["USER"],
1496
+ :gid => ENV["GROUP"], :filemaker => nil })
1218
1497
  # directories cannot be append-only, exclusive, or auth files
1219
1498
  argv.merge({:apponly => false, :excl => false})
1220
1499
  super(name, argv)
1221
1500
  @directory = true
1222
1501
  @children = []
1223
1502
  @children.extend(MonitorMixin)
1503
+ @filemaker = argv[:filemaker]
1224
1504
  end
1225
1505
 
1226
1506
  def child_exists?(name)
@@ -1235,8 +1515,8 @@ module RStyx
1235
1515
  end
1236
1516
 
1237
1517
  ##
1238
- # Add a child to this directory. If a file with the same name
1239
- # already exists, throws a FileExists exception.
1518
+ # Add an SFile _child_ to this directory. If a file with the
1519
+ # same name already exists, throws a FileExists exception.
1240
1520
  #
1241
1521
  def <<(child)
1242
1522
  @children.synchronize do
@@ -1250,12 +1530,16 @@ module RStyx
1250
1530
  end
1251
1531
 
1252
1532
  ##
1253
- # Get the child with the name +name+, or nil if no such file
1533
+ # Get the child with the name _name_, or nil if no such file is
1534
+ # present in this directory.
1254
1535
  #
1255
1536
  def [](name)
1256
1537
  if name == "."
1257
1538
  return(self)
1258
1539
  end
1540
+ if name == ".."
1541
+ return(self.parent)
1542
+ end
1259
1543
  @children.synchronize do
1260
1544
  @children.each do |c|
1261
1545
  if c.name == name
@@ -1274,7 +1558,21 @@ module RStyx
1274
1558
  end
1275
1559
 
1276
1560
  ##
1277
- # Read the contents of the directory
1561
+ # Read the contents of the directory.
1562
+ #
1563
+ # _client_:: the SFileClient object representing the client reading
1564
+ # from this directory.
1565
+ # _offset_:: the offset the client wants to read from. Nonzero
1566
+ # offsets are only valid if the client has read from
1567
+ # the directory before, and may only be the values
1568
+ # obtained from a previous read.
1569
+ # _count_:: the number of bytes that the client wishes to read.
1570
+ # The read will always return the nearest integral
1571
+ # number of directory entries whose length is less than
1572
+ # the count (i.e. if five entries take 300 bytes and
1573
+ # the sixth entry takes 40 bytes, and _count_ was
1574
+ # set to 320, only five entries and 300 bytes will be
1575
+ # returned).
1278
1576
  #
1279
1577
  def read(client, offset, count)
1280
1578
  # Check that the offset is valid; zero offsets are always valid,
@@ -1300,13 +1598,30 @@ module RStyx
1300
1598
  end
1301
1599
  client.next_file_to_read = nextfile
1302
1600
  client.offset += str.length
1303
- return(Message::Rread.new(:data => str))
1601
+ return(reply_read(str))
1602
+ end
1304
1603
 
1604
+ ##
1605
+ # Create a new file with name _name_ and permissions _perm_ in this
1606
+ # directory.
1607
+ #--
1608
+ # FIXME: make this method actually DO something!
1609
+ #++
1610
+ #
1611
+ def newfile(name, perm)
1612
+ raise StyxException.new("cannot create files in this directory")
1305
1613
  end
1306
1614
 
1307
1615
  end # class SDirectory
1308
1616
 
1617
+ ##
1618
+ # An SFile whose underlying data are stored as a String in memory.
1619
+ # This string may grow to arbitrary size.
1620
+ #
1309
1621
  class InMemoryFile < SFile
1622
+ ##
1623
+ # The contents of the file, as a string.
1624
+ #
1310
1625
  attr_accessor :contents
1311
1626
 
1312
1627
  def initialize(name, argv={ :permissions => 0666, :apponly => false,
@@ -1316,12 +1631,31 @@ module RStyx
1316
1631
  @contentslock = Mutex.new
1317
1632
  end
1318
1633
 
1634
+ ##
1635
+ # Read data from the file.
1636
+ #
1637
+ # _client_:: the SFileClient object representing the client reading
1638
+ # from this file.
1639
+ # _offset_:: the offset the client wants to read from
1640
+ # _count_:: the number of bytes that the client wishes to read
1641
+ #
1319
1642
  def read(client, offset, count)
1320
1643
  data = @contents[offset..(offset+count)]
1321
1644
  data ||= ""
1322
- return(Message::Rread.new(:data => data))
1645
+ return(reply_read(data))
1323
1646
  end
1324
1647
 
1648
+ ##
1649
+ # Writes data to this file. Raises a StyxException if the
1650
+ # offset is past the end of the file.
1651
+ #
1652
+ # _client_:: the SFileClient object representing the client writing
1653
+ # to this file
1654
+ # _offset_:: the offset the client wants to write to
1655
+ # _data_:: the data that the client wishes to write
1656
+ # _truncate_:: true or false depending on whether the file is to be
1657
+ # truncated.
1658
+ #
1325
1659
  def write(client, offset, data, truncate)
1326
1660
  @contentslock.synchronize do
1327
1661
  # First write to the file
@@ -1330,12 +1664,11 @@ module RStyx
1330
1664
  end
1331
1665
 
1332
1666
  if offset > @contents.length
1333
- raise StyxException.new("attempt to write past the end of the file")
1334
- end
1667
+ raise StyxException.new("attempt to write past the end of the file") end
1335
1668
 
1336
1669
  @contents[offset..(offset + data.length)] = data
1337
1670
  if (truncate)
1338
- @contents = @contents[0..(offset+data.length)]
1671
+ @contents = @contents[0..(offset+data.length)] = data
1339
1672
  end
1340
1673
  return(reply_write(data.length, client.session))
1341
1674
  end
@@ -1343,6 +1676,205 @@ module RStyx
1343
1676
 
1344
1677
  end
1345
1678
 
1679
+ class FileOnDisk < SFile
1680
+ ##
1681
+ # Create a new FileOnDisk whose path on the local filesystem is _path_,
1682
+ # whose name as it appears on the server's namespace is _name_, whose
1683
+ # base permissions (which are ANDed with the file's real permissions
1684
+ # mask on the underlying filesystem) are _perm_. The file must
1685
+ # already exist on the local filesystem. If _name_ is not specified,
1686
+ # it defaults to the basename of _path_. If _perm_ is not specified,
1687
+ # it defaults to 0666. If the path specified is actually a directory,
1688
+ # returns a DirectoryOnDisk instance instead.
1689
+ #
1690
+ #--
1691
+ # FIXME: should create a DirectoryOnDisk instance instead if _path_
1692
+ # actually represents a directory.
1693
+ #++
1694
+ #
1695
+ def self.new(path, name=nil, perm=nil)
1696
+ unless File.exists?(path)
1697
+ raise StyxException.new("file #{path} does not exist on local filesystem")
1698
+ end
1699
+
1700
+ if name.nil?
1701
+ name = File.basename(path)
1702
+ end
1703
+
1704
+ if File.directory?(path)
1705
+ perm ||= 0777
1706
+ return(DirectoryOnDisk.new(path, name, perm))
1707
+ end
1708
+
1709
+ perm ||= 0666
1710
+ obj = allocate
1711
+ obj.send(:initialize, path, name, perm)
1712
+ return(obj)
1713
+ end
1714
+
1715
+ ##
1716
+ # Hidden initialize method.
1717
+ # See self.new for the real thing.
1718
+ #
1719
+ def initialize(path, name, perm)
1720
+ @name = name
1721
+ @path = File.expand_path(path)
1722
+
1723
+ s = File.stat(@path)
1724
+ pwent = Etc.getpwuid(s.uid)
1725
+ grent = Etc.getgrgid(s.gid)
1726
+ argv = { :permissions => perm & s.mode, :apponly => false,
1727
+ :excl => false, :uid => pwent.name, :gid => grent.name }
1728
+ super(@name, argv)
1729
+ end
1730
+
1731
+ ##
1732
+ # Read data from the file.
1733
+ #
1734
+ # _client_:: the SFileClient object representing the client reading
1735
+ # from this file.
1736
+ # _offset_:: the offset the client wants to read from
1737
+ # _count_:: the number of bytes that the client wishes to read
1738
+ #
1739
+ def read(client, offset, count)
1740
+ begin
1741
+ File.open(@path, "r") do |fp|
1742
+ fp.seek(offset)
1743
+ data = fp.read(count)
1744
+ if data.nil?
1745
+ data = ""
1746
+ end
1747
+ return(reply_read(data))
1748
+ end
1749
+ rescue Exception => e
1750
+ raise StyxException.new("An error of class #{e.class} occurred when trying to read from #{@path}: #{e.message}")
1751
+ end
1752
+
1753
+ end
1754
+
1755
+ ##
1756
+ # Writes data to the file.
1757
+ #
1758
+ # _client_:: the SFileClient object representing the client writing
1759
+ # to this file
1760
+ # _offset_:: the offset the client wants to write to
1761
+ # _data_:: the data that the client wishes to write
1762
+ # _truncate_:: true or false depending on whether the file is to be
1763
+ # truncated.
1764
+ #
1765
+ def write(client, offset, data, truncate)
1766
+ unless File.exists?(@path)
1767
+ # The underlying file was removed out from under us!
1768
+ self.remove
1769
+ raise StyxException.new("The file #{@path} was removed")
1770
+ end
1771
+
1772
+ begin
1773
+ File.open(@path, "r+") do |fp|
1774
+ fp.seek(offset)
1775
+ count = fp.write(data)
1776
+ if truncate
1777
+ fp.truncate(offset + data.length)
1778
+ end
1779
+ reply_write(count, client.session)
1780
+ end
1781
+ rescue Exception => e
1782
+ raise StyxException.new("An error of class #{e.class} occurred when trying to write to #{@path}: #{e.message}")
1783
+ end
1784
+ end
1785
+
1786
+ ##
1787
+ # Refreshes the file's stat information based on a real stat call
1788
+ def refresh
1789
+ s = File.stat(@path)
1790
+ @mtime = s.mtime
1791
+ @atime = s.atime
1792
+ end
1793
+
1794
+ ##
1795
+ # Deletes the underlying file from the disk.
1796
+ #
1797
+ def delete
1798
+ if File.exists?(@path)
1799
+ File.delete(@path)
1800
+ end
1801
+ end
1802
+
1803
+ end
1804
+
1805
+ ##
1806
+ # Class representing a directory on the host filesystem. While there's
1807
+ # no real problem with using this class directly, it's probably better
1808
+ # to use FileOnDisk instead (it will return a DirectoryOnDisk instance
1809
+ # when passed a directory.
1810
+ #
1811
+ class DirectoryOnDisk < SDirectory
1812
+ ##
1813
+ # Create a new DirectoryOnDisk instance. This
1814
+ def initialize(path, name, perm)
1815
+ @name = name
1816
+ @path = File.expand_path(path)
1817
+
1818
+ s = File.stat(@path)
1819
+ pwent = Etc.getpwuid(s.uid)
1820
+ grent = Etc.getgrgid(s.gid)
1821
+ argv = { :permissions => perm & s.mode, :apponly => false,
1822
+ :excl => false, :uid => pwent.name, :gid => grent.name }
1823
+ super(@name, argv)
1824
+ self.refresh
1825
+ end
1826
+
1827
+ ##
1828
+ # Read all metadata from the underlying directory. If _update_children_
1829
+ # is true, all immediate children of this directory will be refreshed
1830
+ # as well.
1831
+ #--
1832
+ # TODO: check for files deleted in the host filesystem
1833
+ #++
1834
+ #
1835
+ def refresh(update_children=true)
1836
+ s = File.stat(@path)
1837
+ @mtime = s.mtime
1838
+ @atime = s.atime
1839
+ unless update_children
1840
+ return
1841
+ end
1842
+ Dir.foreach(@path) do |file|
1843
+ # do not treat . or .. as valid files
1844
+ if file == '.' || file == '..'
1845
+ next
1846
+ end
1847
+ # Check if a file with this name is already known
1848
+ sf = self[file]
1849
+ if sf.nil?
1850
+ begin
1851
+ filepath = @path + File::SEPARATOR + file
1852
+ sf = FileOnDisk.new(filepath)
1853
+ if sf.is_a?(SDirectory)
1854
+ sf.permissions = @permissions
1855
+ else
1856
+ # This is an SFile (a FileOnDisk). Set to the same permissions
1857
+ # as the host directory without the execute flags.
1858
+ sf.permissions = @permissions & 0666
1859
+ end
1860
+ self << sf
1861
+ rescue Exception => e
1862
+ # This should be impossible
1863
+ end
1864
+ else
1865
+ # The file is already known to us. Refresh the file metadata
1866
+ # but do not descend into subdirectories (could lead to deep
1867
+ # recursion).
1868
+ if (sf.is_a?(SDirectory))
1869
+ sf.refresh(false)
1870
+ else
1871
+ sf.refresh
1872
+ end
1873
+ end
1874
+ end
1875
+ end
1876
+ end
1877
+
1346
1878
  end # module Server
1347
1879
 
1348
1880
  end # module RStyx