update files from pve-common
[pve-client.git] / PVE / APIClient / PTY.pm
1 package PVE::APIClient::PTY;
2
3 use strict;
4 use warnings;
5
6 use Fcntl;
7 use POSIX qw(O_RDWR O_NOCTTY);
8
9 # Constants
10
11 use constant {
12     TCGETS     => 0x5401,   # fixed, from asm-generic/ioctls.h
13     TCSETS     => 0x5402,   # fixed, from asm-generic/ioctls.h
14     TIOCGWINSZ => 0x5413,   # fixed, from asm-generic/ioctls.h
15     TIOCSWINSZ => 0x5414,   # fixed, from asm-generic/ioctls.h
16     TIOCSCTTY  => 0x540E,   # fixed, from asm-generic/ioctls.h
17     TIOCNOTTY  => 0x5422,   # fixed, from asm-generic/ioctls.h
18     TIOCGPGRP  => 0x540F,   # fixed, from asm-generic/ioctls.h
19     TIOCSPGRP  => 0x5410,   # fixed, from asm-generic/ioctls.h
20
21     # IOC: dir:2 size:14 type:8 nr:8
22     # Get pty number: dir=2 size=4 type='T' nr=0x30
23     TIOCGPTN => 0x80045430,
24
25     # Set pty lock: dir=1 size=4 type='T' nr=0x31
26     TIOCSPTLCK => 0x40045431,
27
28     # Send signal: dir=1 size=4 type='T' nr=0x36
29     TIOCSIG => 0x40045436,
30
31     # c_cc indices:
32     VINTR => 0,
33     VQUIT => 1,
34     VERASE => 2,
35     VKILL => 3,
36     VEOF => 4,
37     VTIME => 5,
38     VMIN => 6,
39     VSWTC => 7,
40     VSTART => 8,
41     VSTOP => 9,
42     VSUSP => 10,
43     VEOL => 11,
44     VREPRINT => 12,
45     VDISCARD => 13,
46     VWERASE => 14,
47     VLNEXT => 15,
48     VEOL2 => 16,
49 };
50
51 # Utility functions
52
53 sub createpty() {
54     # Open the master file descriptor:
55     sysopen(my $master, '/dev/ptmx', O_RDWR | O_NOCTTY)
56         or die "failed to create pty: $!\n";
57
58     # Find the tty number
59     my $ttynum = pack('L', 0);
60     ioctl($master, TIOCGPTN, $ttynum)
61         or die "failed to query pty number: $!\n";
62     $ttynum = unpack('L', $ttynum);
63
64     # Get the slave name/path
65     my $ttyname = "/dev/pts/$ttynum";
66
67     # Unlock
68     my $false = pack('L', 0);
69     ioctl($master, TIOCSPTLCK, $false)
70         or die "failed to unlock pty: $!\n";
71
72     return ($master, $ttyname);
73 }
74
75 my $openslave = sub {
76     my ($ttyname) = @_;
77
78     # Create a slave file descriptor:
79     sysopen(my $slave, $ttyname, O_RDWR | O_NOCTTY)
80         or die "failed to open slave pty handle: $!\n";
81     return $slave;
82 };
83
84 sub lose_controlling_terminal() {
85     # Can we open our current terminal?
86     if (sysopen(my $ttyfd, '/dev/tty', O_RDWR)) {
87         # Disconnect:
88         ioctl($ttyfd, TIOCNOTTY, 0)
89             or die "failed to disconnect controlling tty: $!\n";
90         close($ttyfd);
91     }
92 }
93
94 sub termios(%) {
95     my (%termios) = @_;
96     my $cc = $termios{cc} // [];
97     if (@$cc < 19) {
98         push @$cc, (0) x (19-@$cc);
99     } elsif (@$cc > 19) {
100         @$cc = $$cc[0..18];
101     }
102
103     return pack('LLLLCC[19]',
104         $termios{iflag} || 0,
105         $termios{oflag} || 0,
106         $termios{cflag} || 0,
107         $termios{lflag} || 0,
108         $termios{line} || 0,
109         @$cc);
110 }
111
112 my $parse_termios = sub {
113     my ($blob) = @_;
114     my ($iflag, $oflag, $cflag, $lflag, $line, @cc) =
115     unpack('LLLLCC[19]', $blob);
116     return {
117         iflag => $iflag,
118         oflag => $oflag,
119         cflag => $cflag,
120         lflag => $lflag,
121         line => $line,
122         cc => \@cc
123     };
124 };
125
126 sub cfmakeraw($) {
127     my ($termios) = @_;
128     $termios->{iflag} &=
129         ~(POSIX::IGNBRK | POSIX::BRKINT | POSIX::PARMRK | POSIX::ISTRIP |
130           POSIX::INLCR | POSIX::IGNCR | POSIX::ICRNL | POSIX::IXON);
131     $termios->{oflag} &= ~POSIX::OPOST;
132     $termios->{lflag} &=
133         ~(POSIX::ECHO | POSIX::ECHONL | POSIX::ICANON | POSIX::ISIG |
134           POSIX::IEXTEN);
135     $termios->{cflag} &= ~(POSIX::CSIZE | POSIX::PARENB);
136     $termios->{cflag} |= POSIX::CS8;
137 }
138
139 sub tcgetattr($) {
140     my ($fd) = @_;
141     my $blob = termios();
142     ioctl($fd, TCGETS, $blob) or die "failed to get terminal attributes\n";
143     return $parse_termios->($blob);
144 }
145
146 sub tcsetattr($$) {
147     my ($fd, $termios) = @_;
148     my $blob = termios(%$termios);
149     ioctl($fd, TCSETS, $blob) or die "failed to set terminal attributes\n";
150 }
151
152 # tcgetsize -> (columns, rows)
153 sub tcgetsize($) {
154         my ($fd) = @_;
155         my $struct_winsz = pack('SSSS', 0, 0, 0, 0);
156         ioctl($fd, TIOCGWINSZ, $struct_winsz)
157                 or die "failed to get window size: $!\n";
158         return reverse unpack('SS', $struct_winsz);
159 }
160
161 sub tcsetsize($$$) {
162     my ($fd, $columns, $rows) = @_;
163     my $struct_winsz = pack('SSSS', $rows, $columns, 0, 0);
164     ioctl($fd, TIOCSWINSZ, $struct_winsz)
165         or die "failed to set window size: $!\n";
166 }
167
168 sub read_password($;$$) {
169     my ($query, $infd, $outfd) = @_;
170
171     my $password = '';
172
173     $infd //= \*STDIN;
174
175     if (!-t $infd) { # Not a terminal? Then just get a line...
176         local $/ = "\n";
177         $password = <$infd>;
178         die "EOF while reading password\n" if !defined $password;
179         chomp $password; # Chop off the newline
180         return $password;
181     }
182
183     $outfd //= \*STDOUT;
184
185     # Raw read loop:
186     my $old_termios;
187     $old_termios = tcgetattr($infd);
188     my $raw_termios = {%$old_termios};
189     cfmakeraw($raw_termios);
190     tcsetattr($infd, $raw_termios);
191     eval {
192         my $echo = undef;
193         my ($ch, $got);
194         syswrite($outfd, $query, length($query));
195         while (($got = sysread($infd, $ch, 1))) {
196             my ($ord) = unpack('C', $ch);
197             last if $ord == 4; # ^D / EOF
198             if ($ord == 0xA || $ord == 0xD) {
199                 # newline, we're done
200                 syswrite($outfd, "\r\n", 2);
201                 last;
202             } elsif ($ord == 3) { # ^C
203                 die "password input aborted\n";
204             } elsif ($ord == 0x7f) {
205                 # backspace - if it's the first key disable
206                 # asterisks
207                 $echo //= 0;
208                 if (length($password)) {
209                     chop $password;
210                     syswrite($outfd, "\b \b", 3);
211                 }
212             } elsif ($ord == 0x09) {
213                 # TAB disables the asterisk-echo
214                 $echo = 0;
215             } else {
216                 # other character, append to password, if it's
217                 # the first character enable asterisks echo
218                 $echo //= 1;
219                 $password .= $ch;
220                 syswrite($outfd, '*', 1) if $echo;
221             }
222         }
223         die "read error: $!\n" if !defined($got);
224     };
225     my $err = $@;
226     tcsetattr($infd, $old_termios);
227     die $err if $err;
228     return $password;
229 }
230
231 sub get_confirmed_password {
232     my $pw1 = read_password('Enter new password: ');
233     my $pw2 = read_password('Retype new password: ');
234     die "passwords do not match\n" if $pw1 ne $pw2;
235     return $pw1;
236 }
237
238 # Class functions
239
240 sub new {
241     my ($class) = @_;
242
243     my ($master, $ttyname) = createpty();
244
245     my $self = {
246         master => $master,
247         ttyname => $ttyname,
248     };
249
250     return bless $self, $class;
251 }
252
253 # Properties
254
255 sub master  { return $_[0]->{master}  }
256 sub ttyname { return $_[0]->{ttyname} }
257
258 # Methods
259
260 sub close {
261     my ($self) = @_;
262     close($self->{master});
263 }
264
265 sub open_slave {
266     my ($self) = @_;
267     return $openslave->($self->{ttyname});
268 }
269
270 sub set_size {
271     my ($self, $columns, $rows) = @_;
272     tcsetsize($self->{master}, $columns, $rows);
273 }
274
275 # get_size -> (columns, rows)
276 sub get_size {
277     my ($self) = @_;
278     return tcgetsize($self->{master});
279 }
280
281 sub kill {
282     my ($self, $signal) = @_;
283     if (!ioctl($self->{master}, TIOCSIG, $signal)) {
284         # kill fallback if the ioctl does not work
285         kill $signal, $self->get_foreground_pid()
286             or die "failed to send signal: $!\n";
287     }
288 }
289
290 sub get_foreground_pid {
291     my ($self) = @_;
292     my $pid = pack('L', 0);
293     ioctl($self->{master}, TIOCGPGRP, $pid)
294         or die "failed to get foreground pid: $!\n";
295     return unpack('L', $pid);
296 }
297
298 sub has_process {
299     my ($self) = @_;
300     return 0 != $self->get_foreground_pid();
301 }
302
303 sub make_controlling_terminal {
304     my ($self) = @_;
305
306     #lose_controlling_terminal();
307     POSIX::setsid();
308     my $slave = $self->open_slave();
309     ioctl($slave, TIOCSCTTY, 0)
310         or die "failed to change controlling tty: $!\n";
311     POSIX::dup2(fileno($slave), 0) or die "failed to dup stdin\n";
312     POSIX::dup2(fileno($slave), 1) or die "failed to dup stdout\n";
313     POSIX::dup2(fileno($slave), 2) or die "failed to dup stderr\n";
314     CORE::close($slave) if fileno($slave) > 2;
315     CORE::close($self->{master});
316 }
317
318 sub getattr {
319     my ($self) = @_;
320     return tcgetattr($self->{master});
321 }
322
323 sub setattr {
324     my ($self, $termios) = @_;
325     return tcsetattr($self->{master}, $termios);
326 }
327
328 sub send_cc {
329     my ($self, $ccidx) = @_;
330     my $attrs = $self->getattr();
331     my $data = pack('C', $attrs->{cc}->[$ccidx]);
332     syswrite($self->{master}, $data)
333     == 1 || die "write failed: $!\n";
334 }
335
336 sub send_eof {
337     my ($self) = @_;
338     $self->send_cc(VEOF);
339 }
340
341 sub send_interrupt {
342     my ($self) = @_;
343     $self->send_cc(VINTR);
344 }
345
346 1;