File Coverage

blib/lib/GuacLite/Client/Guacd.pm
Criterion Covered Total %
statement 105 119 88.2
branch 19 32 59.3
condition 10 18 55.5
subroutine 24 27 88.8
pod 0 8 0.0
total 158 204 77.4


line stmt bran cond sub pod time code
1             package GuacLite::Client::Guacd;
2              
3 2     2   470363 use Mojo::Base 'Mojo::EventEmitter';
  2         18  
  2         14  
4              
5 2     2   3617 use Mojo::Util;
  2         6  
  2         76  
6 2     2   1049 use Mojo::Promise;
  2         342627  
  2         14  
7              
8 2     2   80 use Carp ();
  2         5  
  2         32  
9 2     2   9 use Scalar::Util ();
  2         3  
  2         54  
10              
11 2     2   10 use constant DEBUG => $ENV{GUACLITE_GUACD_DEBUG};
  2         4  
  2         4722  
12              
13             has host => $ENV{GUACLITE_GUACD_HOST} || 'localhost';
14             has port => $ENV{GUACLITE_GUACD_PORT} || '4822';
15              
16             # the following should probably all be required parameters, but for now, do this
17             has protocol => 'vnc';
18             has connection_args => sub { {} };
19              
20             has width => 1024;
21             has height => 768;
22             has dpi => 96;
23              
24             has audio_mimetypes => sub { [] };
25             has image_mimetypes => sub { [] };
26             has video_mimetypes => sub { [] };
27             has timezone => '';
28              
29             # supported version of guacamole protocol
30             my @v = (1,3,0);
31             my $v = do { local $" = '_'; "VERSION_@v" };
32              
33             sub _check_version {
34 4     4   10 my $version = shift;
35 4 50       35 return 0 unless
36             $version =~ /^VERSION_(\d+)_(\d+)_(\d+)$/;
37 4 50       43 return 0 unless $1 >= $v[0];
38 4 100       25 return 0 unless $2 >= $v[1];
39 3 50       12 return 0 unless $3 >= $v[2];
40 3         10 return 1;
41             }
42              
43             sub close {
44 0     0 0 0 my $self = shift;
45 0 0       0 return unless my $s = $self->{stream};
46 0         0 $s->close;
47             }
48              
49             sub connect_p {
50 7     7 0 28108 my $self = shift;
51 7   50     66 my $connect = shift || {};
52 7         31 Scalar::Util::weaken($self);
53             return Mojo::Promise->new(sub {
54 7     7   318 my ($res, $rej) = @_;
55 7   33     45 $connect->{address} ||= $self->host;
56 7   33     75 $connect->{port} ||= $self->port;
57             Mojo::IOLoop->client($connect, sub {
58 7         16096 my (undef, $err, $stream) = @_;
59 7 50       22 return $rej->("Connect error: $err") if $err;
60              
61             #TODO configurable timeout
62 7         25 $stream->timeout(0);
63 7         181 $self->{stream} = $stream;
64              
65             $stream->on(read => sub {
66 10         9726 my (undef, $bytes) = @_;
67 10         18 print STDERR '<- ' . Mojo::Util::term_escape($bytes) . "\n" if DEBUG;
68 10         32 $self->{buffer} .= $bytes;
69 10         94 while($self->{buffer} =~ s/^([^;]+;)//) {
70 10         22 eval { $self->emit(instruction => $1) };
  10         39  
71             }
72 7         52 });
73              
74             $stream->on(error => sub {
75 0         0 $self->emit(error => $_[1]);
76 7         65 });
77              
78             $stream->on(close => sub {
79 0         0 print STDERR "Connection to guacd closed\n" if DEBUG;
80 0 0       0 return unless $self;
81 0         0 delete @{$self}{qw(buffer id stream)};
  0         0  
82 0         0 $self->emit('close');
83 7         72 });
84              
85 7         44 $res->();
86 7         79 });
87 7         60 });
88             }
89              
90             sub handshake_p {
91 7     7 0 1780 Scalar::Util::weaken(my $self = shift);
92              
93             return Mojo::Promise->reject('Not connected')
94 7 50       27 unless my $stream = $self->{stream};
95              
96 7         16 my $args;
97             return $self->_expect(args => [select => $self->protocol])
98             ->then(sub {
99 4     4   787 my $got = shift;
100 4         9 my $version = shift @$got;
101 4 100       24 return Mojo::Promise->reject("Version $version less than supported ($v)")
102             unless _check_version($version);
103 3         6 $args = $got;
104 3         15 $self->write_p(encode([size => $self->width, $self->height, $self->dpi]));
105             })
106 3     3   1103 ->then(sub{ $self->write_p(encode([audio => @{ $self->audio_mimetypes } ])) })
  3         15  
107 3     3   1122 ->then(sub{ $self->write_p(encode([image => @{ $self->image_mimetypes } ])) })
  3         61  
108 3     3   971 ->then(sub{ $self->write_p(encode([video => @{ $self->video_mimetypes } ])) })
  3         12  
109             ->then(sub{
110 3     3   1160 my @connect = (connect => $v);
111 3         16 my $proto = $self->connection_args;
112 3   100     14 push @connect, map { $proto->{$_} // '' } @$args;
  6         31  
113 3         11 $self->_expect(ready => \@connect);
114             })
115             ->then(sub {
116 2     2   735 my $id = shift;
117 2         5 print STDERR "Session $id->[0] is ready" if DEBUG;
118 2         20 $self->{id} = $id->[0];
119 2         10 return $id->[0];
120 7     5   30 })->catch(sub { Mojo::Promise->reject("Handshake error: $_[0]") });
  5         3168  
121             }
122              
123 1     1 0 3 sub stream { shift->{stream} }
124              
125             sub write {
126 0     0 0 0 my ($self, $bytes) = @_;
127             Carp::croak('Not connected')
128 0 0       0 unless my $s = $self->{stream};
129 0         0 print STDERR '-> ' . Mojo::Util::term_escape($bytes) . "\n" if DEBUG;
130 0         0 $self->{stream}->write($bytes);
131             }
132              
133             sub write_p {
134 22     22 0 232 my ($self, $bytes) = @_;
135             return Mojo::Promise->reject('Not connected')
136 22 50       69 unless my $s = $self->{stream};
137              
138 22         61 my $p = Mojo::Promise->new;
139 22         534 print STDERR '-> ' . Mojo::Util::term_escape($bytes) . "\n" if DEBUG;
140 22     22   120 $self->{stream}->write($bytes, sub { $p->resolve });
  22         11167  
141 22         994 return $p;
142             }
143              
144             sub _expect {
145 10     10   63 my ($self, $command, $send) = @_;
146 10         48 my $p = Mojo::Promise->new;
147              
148             $self->once(instruction => sub {
149 10     10   355 my (undef, $raw) = @_;
150 10         20 my $instruction;
151 10 100       16 eval {
152 10         27 $instruction = decode($raw); 1;
  8         21  
153             } or return $p->reject($@);
154 8         17 my $got = shift @$instruction;
155 8 100       28 if ($got eq $command) {
156 6         23 $p->resolve($instruction);
157             } else {
158 2         14 $p->reject(qq[Unexpected command "$got" received, expected "$command"]);
159             }
160 10         324 });
161              
162             $self->write_p(encode($send))
163 10     0   262 ->catch(sub { $p->reject("Send failed: $_[0]") });
  0         0  
164              
165 10         637 return $p;
166             }
167              
168             ## FUNCTIONS!
169              
170             sub encode {
171 23     23 0 100 my $words = shift;
172 23   50     48 return join(',', map { $_ //= ''; length . '.' . Mojo::Util::encode('UTF-8', $_) } @$words) . ";";
  49         291  
  49         133  
173             }
174              
175             sub decode {
176 10     10 0 46 my $line = Mojo::Util::decode('UTF-8', shift);
177 10 50       370 Carp::croak 'Instruction does not end with ;'
178             unless $line =~ s/;$//;
179              
180             my @words =
181             map {
182 10         90 my ($l, $s) = split /\./, $_, 2;
  22         75  
183 22 100 66     479 Carp::croak 'Invalid instruction encoding'
      66        
184             unless defined $l && defined $s && Scalar::Util::looks_like_number($l);
185 21 100       252 Carp::croak 'Word length mismatch'
186             unless length($s) == $l;
187 20         55 $s;
188             }
189             split ',', $line;
190              
191 8         23 return \@words;
192             }
193              
194              
195             1;
196