File Coverage

blib/lib/Net/SynchroEdit/Service.pm
Criterion Covered Total %
statement 74 183 40.4
branch 7 52 13.4
condition 0 3 0.0
subroutine 21 32 65.6
pod 10 11 90.9
total 112 281 39.8


line stmt bran cond sub pod time code
1             package Net::SynchroEdit::Service;
2              
3 1     1   22663 use 5.008004;
  1         3  
  1         42  
4 1     1   7 use strict;
  1         2  
  1         32  
5 1     1   4 use warnings;
  1         5  
  1         35  
6 1     1   2786 use IO::Select;
  1         1814  
  1         55  
7 1     1   1093 use IO::Socket;
  1         55126  
  1         7  
8              
9             require Exporter;
10 1     1   1773 use AutoLoader qw(AUTOLOAD);
  1         2026  
  1         7  
11              
12             our @ISA = qw(Exporter);
13              
14             # Items to export into callers namespace by default. Note: do not export
15             # names by default without a very good reason. Use EXPORT_OK instead.
16             # Do not simply export all your public functions/methods/constants.
17              
18             # This allows declaration use Net::SynchroEdit ':all';
19             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
20             # will save memory.
21             our %EXPORT_TAGS = ( 'all' => [ qw(
22            
23             SE_ERR_CONNECTION_FAILED SE_ERR_INVALID_WELCOME SE_ERR_STREAM_CLOSED SE_ERR_DOCUMENT_NOT_FOUND SE_ERR_DOCUMENT_UNINITIALIZED SE_ERR_UNRECOGNIZED_REPLY SE_ERR_DOCUMENT_INITIALIZED SE_ERR_DOCUMENT_IN_SESSION SE_ERR_DOCUMENT_OPEN SE_ERR_FAILED_INSTANTIATION SE_ERR_FAILED_MOVING_UPLOAD SE_ERR_FAILED_SOURCE_COPY SE_ERR
24            
25             ) ] );
26              
27             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
28              
29             our @EXPORT = qw(
30            
31             );
32              
33             # Error codes
34 1     1   155 use constant SE_ERR_CONNECTION_FAILED => 1;
  1         3  
  1         99  
35 1     1   6 use constant SE_ERR_INVALID_WELCOME => 2;
  1         3  
  1         50  
36 1     1   6 use constant SE_ERR_STREAM_CLOSED => 3;
  1         2  
  1         69  
37 1     1   6 use constant SE_ERR_DOCUMENT_NOT_FOUND => 4;
  1         1  
  1         55  
38 1     1   7 use constant SE_ERR_DOCUMENT_UNINITIALIZED => 5;
  1         2  
  1         50  
39 1     1   6 use constant SE_ERR_UNRECOGNIZED_REPLY => 6;
  1         2  
  1         97  
40 1     1   5 use constant SE_ERR_DOCUMENT_INITIALIZED => 7;
  1         2  
  1         71  
41 1     1   5 use constant SE_ERR_DOCUMENT_IN_SESSION => 8;
  1         2  
  1         39  
42 1     1   5 use constant SE_ERR_DOCUMENT_OPEN => 9;
  1         1  
  1         53  
43 1     1   5 use constant SE_ERR_FAILED_INSTANTIATION => 10;
  1         2  
  1         417  
44 1     1   7 use constant SE_ERR_FAILED_MOVING_UPLOAD => 11;
  1         124  
  1         61  
45 1     1   6 use constant SE_ERR_FAILED_SOURCE_COPY => 12;
  1         2  
  1         54  
46 1     1   6 use constant SE_ERR => 999;
  1         1  
  1         2759  
47              
48              
49             our $VERSION = '0.039';
50             our @buf = ();
51             our %lastSessions;
52              
53             # Constructor.
54             #
55             sub new {
56 2     2 0 417 my $package = shift;
57 2         9 return bless({}, $package);
58             }
59              
60             # Connect to the service.
61             # Returns true on success and false on failure.
62             #
63             ### connect([host[, port[, user[, pass]]]])
64             sub connect {
65 2     2 1 41 my $self = shift;
66              
67             # Reset internal vars.
68 2         13 $self->{'queries'} = 0;
69 2         3 $self->{'connected'} = 0;
70 2         5 @buf = ();
71 2         5 $self->{'bchop'} = "";
72 2         4 $self->{'host'} = "localhost";
73 2         4 $self->{'port'} = 7962;
74 2         5 $self->{'user'} = "root";
75 2         4 $self->{'pass'} = "secret";
76              
77             # Acquire arguments, if any.
78 2 100       8 $self->{'host'} = shift if @_;
79 2 100       6 $self->{'port'} = shift if @_;
80 2 50       16 $self->{'user'} = shift if @_;
81 2 50       6 $self->{'pass'} = shift if @_;
82              
83             # Create new socket to service.
84 2         21 my $stream = new IO::Socket::INET
85             (PeerAddr => $self->{'host'},
86             PeerPort => $self->{'port'},
87             Proto => "tcp",
88             );
89 2 50       1782 return unless $stream;
90            
91 0           $self->{'stream'} = $stream;
92              
93             # Create select.
94 0           $self->{'select'} = new IO::Select();
95 0           $self->{'select'}->add($self->{'stream'});
96              
97             # Read welcome string.
98 0           ($self->{'reader'}) = $self->{'select'}->can_read(10);
99 0 0         return if not defined $self->{'reader'};
100 0           $self->{'reader'}->sysread($self->{'welcome'}, 1024);
101              
102             # Mark us as connected.
103 0           $self->{'connected'} = 1;
104              
105             # Send login information.
106 0           print $stream "USER $self->{'user'}\nPASS $self->{'pass'}\n";
107              
108             # Fetch information about server.
109 0 0         return unless $self->fetch_info;
110 0           return 1;
111             }
112              
113             # Shutdown a specific session by SID in $timer minutes.
114             #
115             ### shutdown(SID[, timer])
116             sub shutdown {
117 0     0 1   my $self = shift;
118 0           my $sid = shift;
119 0           my $timer = 0;
120 0 0         $timer = shift if @_;
121              
122 0 0         return unless $self->query("SHUTDOWN $sid"); # XXX: $timer is currently ignored.
123             # my @result = ;
124 0 0         return unless $self->fetch_status eq "ACK";
125 0           return 1;
126             }
127              
128             # Disconnect from response service.
129             #
130             ### disconnect()
131             sub disconnect {
132 0     0 1   my $self = shift;
133              
134 0 0         return unless $self->{'connected'};
135 0           close $self->{'stream'};
136 0           undef $self->{'connected'};
137 0           return 1;
138             }
139              
140             # Request a list of existing sessions. If $extended is true, an additional STATUS request is sent
141             # per document.
142             # On success, returns a map with a set of values. The "SIDS" key contains a list of the sessions,
143             # space-separated. The data of a particular session can be retrieved using the get() method.
144             # If $extended, each entry additionally contains AGE, USERS, CONTRIBUTORS, DOCSIZE.
145             #
146             ### sessions([$extended = 0])
147             sub sessions {
148 0     0 1   my $self = shift;
149            
150 0           my $extended = 0;
151 0 0         $extended = shift if @_;
152              
153 0 0         return unless defined $self->query("QUERY");
154 0           my @result = $self->fetch_result;
155 0           my $ix = $#result+1;
156 0 0         return if $ix == 1;
157 0           my %retval;
158             my $i;
159 0           for ($i = 0; $i < $ix; $i++) {
160 0           my @docexpr = split(/ /, $result[$i++]);
161 0           my @isexpr = split(/ /, $result[$i]);
162 0           shift @docexpr;
163 0           my $doc = join(" ", @docexpr);
164 0           shift @isexpr;
165 0           my $sid = shift @isexpr;
166 0           $retval{"$sid-DOCUMENT"} = $doc;
167 0           $retval{"$sid-PORT"} = shift @isexpr;
168 0           $retval{"$sid-FLAGS"} = shift @isexpr;
169 0 0         if ($extended) {
170 0           $self->query("STATUS $sid");
171 0           my %edata = $self->fetch_map;
172 0           $retval{"$sid-AGE"} = $edata{'AGE'};
173 0           $retval{"$sid-USERS"} = $edata{'USERS'};
174 0           $retval{"$sid-CONTRIBUTORS"} = $edata{'CONTRIBUTORS'};
175 0           $retval{"$sid-DOCSIZE"} = $edata{'DOCSIZE'};
176             }
177 0 0         if (defined $retval{'SIDS'}) {
178 0           $retval{'SIDS'} = "$retval{'SIDS'} $sid";
179             } else {
180 0           $retval{'SIDS'} = "$sid";
181             }
182             }
183 0           %lastSessions = %retval;
184 0           return %retval;
185             }
186              
187             # Get the shortened variables for a particular sessions session.
188             #
189             ### get($sid)
190             sub get {
191 0     0 1   my $self = shift;
192 0           my %hashmap = %lastSessions;
193 0           my $sid = shift;
194              
195 0           my %result = ('DOCUMENT', $hashmap{"$sid-DOCUMENT"},
196             'PORT', $hashmap{"$sid-PORT"},
197             'FLAGS', $hashmap{"$sid-FLAGS"},
198             'AGE', $hashmap{"$sid-AGE"},
199             'USERS', $hashmap{"$sid-USERS"},
200             'CONTRIBUTORS', $hashmap{"$sid-CONTRIBUTORS"},
201             'DOCSIZE', $hashmap{"$sid-DOCSIZE"});
202 0           return %result;
203             }
204              
205             # Perform service query.
206             #
207             ### query($cmd)
208             sub query {
209 0     0 1   my $self = shift;
210              
211             # Acquire arguments, if any.
212 0 0         return unless @_;
213 0           my $cmd = shift;
214 0           $self->{'queries'}++;
215 0 0         return unless $self->{'connected'};
216 0           my $stream = $self->{'stream'};
217 0           print $stream "$cmd\n";
218 0           return 1;
219             }
220              
221             # Fill buffer.
222             sub _fillbuf {
223 0     0     my $self = shift;
224 0           my $bchop = $self->{'bchop'};
225 0           my $line = "";
226 0           $self->{'reader'}->sysread($line, 4096);
227 0           $line = "$bchop$line";
228 0           push(@buf, split(/\n/, $line));
229 0 0         if (substr($line, (length $line)-1, 1) ne "\n") {
230 0           $bchop = pop @buf;
231 0           print "substr caught unended line; chopping off '$bchop' from '$line'\n";
232             }
233 0           $self->{'bchop'} = $bchop;
234             }
235              
236             # Get next line.
237             # NOTE: This function requires that either
238             # 1) a new line is buffered, or
239             # 2) there is data from the server waiting.
240             # If neither 1 nor 2, the code will hang for a while,
241             # until new data is available, which probably is never.
242             sub _nextline {
243 0     0     my $self = shift;
244 0 0         if (!@buf) {
245 0           $self->_fillbuf;
246             }
247 0           return shift @buf;
248             }
249              
250             # Read the next pending result, if any.
251             #
252             ### fetch_result()
253             sub fetch_result {
254 0     0 1   my $self = shift;
255              
256 0 0         return unless $self->{'connected'};
257 0 0         return unless $self->{'queries'} > 0;
258              
259 0           $self->{'queries'}--;
260 0           my @result;
261             my $line;
262 0   0       while (($line = $self->_nextline) && $line ne "END") {
263 0           push(@result, $line);
264             }
265 0           return @result;
266             }
267              
268             # Fetch the first line only in a pending result.
269             # The remaining data, if any, is discarded.
270             #
271             ### fetch_status()
272             sub fetch_status {
273 0     0 1   my $self = shift;
274              
275 0           my @result = $self->fetch_result;
276 0 0         return unless @result;
277 0           return shift @result;
278             }
279              
280             # Return the next pending result as a hashmap.
281             # This is only supported when the response is a list of keys
282             # and values in the format "KEY VALUE\nKEY2 VALUE2\n..."
283             #
284             ### fetch_map()
285             sub fetch_map {
286 0     0 1   my $self = shift;
287              
288 0           my @query = $self->fetch_result;
289 0           my %result;
290             my $line;
291 0           my @pair;
292 0           while ($line = shift @query) {
293 0           @pair = split(/ /, $line, 2);
294 0 0         if ($#pair != 1) {
295             # The result most likely failed, so we set the _ key to $line.
296 0           $result{'_'} = $line;
297             } else {
298 0           $result{$pair[0]} = $pair[1];
299             }
300             }
301 0           return %result;
302             }
303              
304             # Re-fetch information from service.
305             # Returns success boolean.
306             #
307             ### fetch_info()
308             sub fetch_info {
309 0     0 1   my $self = shift;
310            
311 0 0         return unless $self->query("INFO");
312              
313 0           my %result = $self->fetch_map;
314 0 0         return if $result{'_'};
315 0           $self->{'localPath'} = $result{'LOCALPATH'};
316 0           $self->{'uptime'} = $result{'UPTIME'};
317 0           $self->{'serverModel'} = $result{'SERVERMODEL'};
318 0           return 1;
319             }
320              
321             # Autoload methods go after =cut, and are processed by the autosplit program.
322              
323             1;
324             __END__