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   483709 use Mojo::Base 'Mojo::EventEmitter';
  2         15  
  2         13  
4              
5 2     2   3223 use Mojo::Util;
  2         4  
  2         73  
6 2     2   940 use Mojo::Promise;
  2         318162  
  2         13  
7              
8 2     2   77 use Carp ();
  2         4  
  2         31  
9 2     2   9 use Scalar::Util ();
  2         4  
  2         50  
10              
11 2     2   10 use constant DEBUG => $ENV{GUACLITE_GUACD_DEBUG};
  2         5  
  2         4480  
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   9 my $version = shift;
35 4 50       40 return 0 unless
36             $version =~ /^VERSION_(\d+)_(\d+)_(\d+)$/;
37 4 50       34 return 0 unless $1 >= $v[0];
38 4 100       30 return 0 unless $2 >= $v[1];
39 3 50       14 return 0 unless $3 >= $v[2];
40 3         8 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 29830 my $self = shift;
51 7   50     73 my $connect = shift || {};
52 7         33 Scalar::Util::weaken($self);
53             return Mojo::Promise->new(sub {
54 7     7   309 my ($res, $rej) = @_;
55 7   33     56 $connect->{address} ||= $self->host;
56 7   33     90 $connect->{port} ||= $self->port;
57             Mojo::IOLoop->client($connect, sub {
58 7         17189 my (undef, $err, $stream) = @_;
59 7 50       27 return $rej->("Connect error: $err") if $err;
60              
61             #TODO configurable timeout
62 7         30 $stream->timeout(0);
63 7         184 $self->{stream} = $stream;
64              
65             $stream->on(read => sub {
66 10         9865 my (undef, $bytes) = @_;
67 10         20 print STDERR '<- ' . Mojo::Util::term_escape($bytes) . "\n" if DEBUG;
68 10         35 $self->{buffer} .= $bytes;
69 10         136 while($self->{buffer} =~ s/^([^;]+;)//) {
70 10         25 eval { $self->emit(instruction => $1) };
  10         41  
71             }
72 7         72 });
73              
74             $stream->on(error => sub {
75 0         0 $self->emit(error => $_[1]);
76 7         74 });
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         81 });
84              
85 7         43 $res->();
86 7         97 });
87 7         78 });
88             }
89              
90             sub handshake_p {
91 7     7 0 2482 Scalar::Util::weaken(my $self = shift);
92              
93             return Mojo::Promise->reject('Not connected')
94 7 50       36 unless my $stream = $self->{stream};
95              
96 7         15 my $args;
97             return $self->_expect(args => [select => $self->protocol])
98             ->then(sub {
99 4     4   839 my $got = shift;
100 4         11 my $version = shift @$got;
101 4 100       22 return Mojo::Promise->reject("Version $version less than supported ($v)")
102             unless _check_version($version);
103 3         7 $args = $got;
104 3         18 $self->write_p(encode([size => $self->width, $self->height, $self->dpi]));
105             })
106 3     3   1045 ->then(sub{ $self->write_p(encode([audio => @{ $self->audio_mimetypes } ])) })
  3         13  
107 3     3   1251 ->then(sub{ $self->write_p(encode([image => @{ $self->image_mimetypes } ])) })
  3         45  
108 3     3   928 ->then(sub{ $self->write_p(encode([video => @{ $self->video_mimetypes } ])) })
  3         13  
109             ->then(sub{
110 3     3   1307 my @connect = (connect => $v);
111 3         13 my $proto = $self->connection_args;
112 3   100     16 push @connect, map { $proto->{$_} // '' } @$args;
  6         30  
113 3         13 $self->_expect(ready => \@connect);
114             })
115             ->then(sub {
116 2     2   717 my $id = shift;
117 2         4 print STDERR "Session $id->[0] is ready" if DEBUG;
118 2         10 $self->{id} = $id->[0];
119 2         5 return $id->[0];
120 7     5   45 })->catch(sub { Mojo::Promise->reject("Handshake error: $_[0]") });
  5         3084  
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 239 my ($self, $bytes) = @_;
135             return Mojo::Promise->reject('Not connected')
136 22 50       67 unless my $s = $self->{stream};
137              
138 22         68 my $p = Mojo::Promise->new;
139 22         522 print STDERR '-> ' . Mojo::Util::term_escape($bytes) . "\n" if DEBUG;
140 22     22   120 $self->{stream}->write($bytes, sub { $p->resolve });
  22         8968  
141 22         942 return $p;
142             }
143              
144             sub _expect {
145 10     10   78 my ($self, $command, $send) = @_;
146 10         51 my $p = Mojo::Promise->new;
147              
148             $self->once(instruction => sub {
149 10     10   310 my (undef, $raw) = @_;
150 10         17 my $instruction;
151 10 100       20 eval {
152 10         36 $instruction = decode($raw); 1;
  8         25  
153             } or return $p->reject($@);
154 8         19 my $got = shift @$instruction;
155 8 100       26 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         391 });
161              
162             $self->write_p(encode($send))
163 10     0   210 ->catch(sub { $p->reject("Send failed: $_[0]") });
  0         0  
164              
165 10         670 return $p;
166             }
167              
168             ## FUNCTIONS!
169              
170             sub encode {
171 23     23 0 83 my $words = shift;
172 23   50     53 return join(',', map { $_ //= ''; length . '.' . Mojo::Util::encode('UTF-8', $_) } @$words) . ";";
  49         309  
  49         138  
173             }
174              
175             sub decode {
176 10     10 0 40 my $line = Mojo::Util::decode('UTF-8', shift);
177 10 50       372 Carp::croak 'Instruction does not end with ;'
178             unless $line =~ s/;$//;
179              
180             my @words =
181             map {
182 10         50 my ($l, $s) = split /\./, $_, 2;
  22         76  
183 22 100 66     488 Carp::croak 'Invalid instruction encoding'
      66        
184             unless defined $l && defined $s && Scalar::Util::looks_like_number($l);
185 21 100       328 Carp::croak 'Word length mismatch'
186             unless length($s) == $l;
187 20         56 $s;
188             }
189             split ',', $line;
190              
191 8         22 return \@words;
192             }
193              
194              
195             1;
196