Add PVE::PTY helper class
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Fri, 24 Nov 2017 09:54:38 +0000 (10:54 +0100)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Mon, 27 Nov 2017 13:21:50 +0000 (14:21 +0100)
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
src/PVE/PTY.pm [new file with mode: 0644]

diff --git a/src/PVE/PTY.pm b/src/PVE/PTY.pm
new file mode 100644 (file)
index 0000000..51c7630
--- /dev/null
@@ -0,0 +1,276 @@
+package PVE::PTY;
+
+use strict;
+use warnings;
+
+use Fcntl;
+use POSIX qw(O_RDWR O_NOCTTY);
+
+# Constants
+
+use constant {
+    TCGETS     => 0x5401,   # fixed, from asm-generic/ioctls.h
+    TCSETS     => 0x5402,   # fixed, from asm-generic/ioctls.h
+    TIOCGWINSZ => 0x5413,   # fixed, from asm-generic/ioctls.h
+    TIOCSWINSZ => 0x5414,   # fixed, from asm-generic/ioctls.h
+    TIOCSCTTY  => 0x540E,   # fixed, from asm-generic/ioctls.h
+    TIOCNOTTY  => 0x5422,   # fixed, from asm-generic/ioctls.h
+    TIOCGPGRP  => 0x540F,   # fixed, from asm-generic/ioctls.h
+    TIOCSPGRP  => 0x5410,   # fixed, from asm-generic/ioctls.h
+
+    # IOC: dir:2 size:14 type:8 nr:8
+    # Get pty number: dir=2 size=4 type='T' nr=0x30
+    TIOCGPTN => 0x80045430,
+
+    # Set pty lock: dir=1 size=4 type='T' nr=0x31
+    TIOCSPTLCK => 0x40045431,
+
+    # Send signal: dir=1 size=4 type='T' nr=0x36
+    TIOCSIG => 0x40045436,
+
+    # c_cc indices:
+    VINTR => 0,
+    VQUIT => 1,
+    VERASE => 2,
+    VKILL => 3,
+    VEOF => 4,
+    VTIME => 5,
+    VMIN => 6,
+    VSWTC => 7,
+    VSTART => 8,
+    VSTOP => 9,
+    VSUSP => 10,
+    VEOL => 11,
+    VREPRINT => 12,
+    VDISCARD => 13,
+    VWERASE => 14,
+    VLNEXT => 15,
+    VEOL2 => 16,
+};
+
+# Utility functions
+
+sub createpty() {
+    # Open the master file descriptor:
+    sysopen(my $master, '/dev/ptmx', O_RDWR | O_NOCTTY)
+       or die "failed to create pty: $!\n";
+
+    # Find the tty number
+    my $ttynum = pack('L', 0);
+    ioctl($master, TIOCGPTN, $ttynum)
+       or die "failed to query pty number: $!\n";
+    $ttynum = unpack('L', $ttynum);
+
+    # Get the slave name/path
+    my $ttyname = "/dev/pts/$ttynum";
+
+    # Unlock
+    my $false = pack('L', 0);
+    ioctl($master, TIOCSPTLCK, $false)
+       or die "failed to unlock pty: $!\n";
+
+    return ($master, $ttyname);
+}
+
+my $openslave = sub {
+    my ($ttyname) = @_;
+
+    # Create a slave file descriptor:
+    sysopen(my $slave, $ttyname, O_RDWR | O_NOCTTY)
+       or die "failed to open slave pty handle: $!\n";
+    return $slave;
+};
+
+sub lose_controlling_terminal() {
+    # Can we open our current terminal?
+    if (sysopen(my $ttyfd, '/dev/tty', O_RDWR)) {
+       # Disconnect:
+       ioctl($ttyfd, TIOCNOTTY, 0)
+           or die "failed to disconnect controlling tty: $!\n";
+       close($ttyfd);
+    }
+}
+
+sub termios(%) {
+    my (%termios) = @_;
+    my $cc = $termios{cc} // [];
+    if (@$cc < 19) {
+       push @$cc, (0) x (19-@$cc);
+    } elsif (@$cc > 19) {
+       @$cc = $$cc[0..18];
+    }
+
+    return pack('LLLLCC[19]',
+       $termios{iflag} || 0,
+       $termios{oflag} || 0,
+       $termios{cflag} || 0,
+       $termios{lflag} || 0,
+       $termios{line} || 0,
+       @$cc);
+}
+
+my $parse_termios = sub {
+    my ($blob) = @_;
+    my ($iflag, $oflag, $cflag, $lflag, $line, @cc) =
+    unpack('LLLLCC[19]', $blob);
+    return {
+       iflag => $iflag,
+       oflag => $oflag,
+       cflag => $cflag,
+       lflag => $lflag,
+       line => $line,
+       cc => \@cc
+    };
+};
+
+sub cfmakeraw($) {
+    my ($termios) = @_;
+    $termios->{iflag} &=
+       ~(POSIX::IGNBRK | POSIX::BRKINT | POSIX::PARMRK | POSIX::ISTRIP |
+         POSIX::INLCR | POSIX::IGNCR | POSIX::ICRNL | POSIX::IXON);
+    $termios->{oflag} &= ~POSIX::OPOST;
+    $termios->{lflag} &=
+       ~(POSIX::ECHO | POSIX::ECHONL | POSIX::ICANON | POSIX::ISIG |
+         POSIX::IEXTEN);
+    $termios->{cflag} &= ~(POSIX::CSIZE | POSIX::PARENB);
+    $termios->{cflag} |= POSIX::CS8;
+}
+
+sub tcgetattr($) {
+    my ($fd) = @_;
+    my $blob = termios();
+    ioctl($fd, TCGETS, $blob) or die "failed to get terminal attributes\n";
+    return $parse_termios->($blob);
+}
+
+sub tcsetattr($$) {
+    my ($fd, $termios) = @_;
+    my $blob = termios(%$termios);
+    ioctl($fd, TCSETS, $blob) or die "failed to set terminal attributes\n";
+}
+
+# tcgetsize -> (columns, rows)
+sub tcgetsize($) {
+       my ($fd) = @_;
+       my $struct_winsz = pack('SSSS', 0, 0, 0, 0);
+       ioctl($fd, TIOCGWINSZ, $struct_winsz)
+               or die "failed to get window size: $!\n";
+       return reverse unpack('SS', $struct_winsz);
+}
+
+sub tcsetsize($$$) {
+    my ($fd, $columns, $rows) = @_;
+    my $struct_winsz = pack('SSSS', $rows, $columns, 0, 0);
+    ioctl($fd, TIOCSWINSZ, $struct_winsz)
+       or die "failed to set window size: $!\n";
+}
+
+# Class functions
+
+sub new {
+    my ($class) = @_;
+
+    my ($master, $ttyname) = createpty();
+
+    my $self = {
+       master => $master,
+       ttyname => $ttyname,
+    };
+
+    return bless $self, $class;
+}
+
+# Properties
+
+sub master  { return $_[0]->{master}  }
+sub ttyname { return $_[0]->{ttyname} }
+
+# Methods
+
+sub close {
+    my ($self) = @_;
+    close($self->{master});
+}
+
+sub open_slave {
+    my ($self) = @_;
+    return $openslave->($self->{ttyname});
+}
+
+sub set_size {
+    my ($self, $columns, $rows) = @_;
+    tcsetsize($self->{master}, $columns, $rows);
+}
+
+# get_size -> (columns, rows)
+sub get_size {
+    my ($self) = @_;
+    return tcgetsize($self->{master});
+}
+
+sub kill {
+    my ($self, $signal) = @_;
+    if (!ioctl($self->{master}, TIOCSIG, $signal)) {
+       # kill fallback if the ioctl does not work
+       kill $signal, $self->get_foreground_pid()
+           or die "failed to send signal: $!\n";
+    }
+}
+
+sub get_foreground_pid {
+    my ($self) = @_;
+    my $pid = pack('L', 0);
+    ioctl($self->{master}, TIOCGPGRP, $pid)
+       or die "failed to get foreground pid: $!\n";
+    return unpack('L', $pid);
+}
+
+sub has_process {
+    my ($self) = @_;
+    return 0 != $self->get_foreground_pid();
+}
+
+sub make_controlling_terminal {
+    my ($self) = @_;
+
+    #lose_controlling_terminal();
+    POSIX::setsid();
+    my $slave = $self->open_slave();
+    ioctl($slave, TIOCSCTTY, 0)
+       or die "failed to change controlling tty: $!\n";
+    POSIX::dup2(fileno($slave), 0) or die "failed to dup stdin\n";
+    POSIX::dup2(fileno($slave), 1) or die "failed to dup stdout\n";
+    POSIX::dup2(fileno($slave), 2) or die "failed to dup stderr\n";
+    CORE::close($slave) if fileno($slave) > 2;
+    CORE::close($self->{master});
+}
+
+sub getattr {
+    my ($self) = @_;
+    return tcgetattr($self->{master});
+}
+
+sub setattr {
+    my ($self, $termios) = @_;
+    return tcsetattr($self->{master}, $termios);
+}
+
+sub send_cc {
+    my ($self, $ccidx) = @_;
+    my $attrs = $self->getattr();
+    my $data = pack('C', $attrs->{cc}->[$ccidx]);
+    syswrite($self->{master}, $data)
+    == 1 || die "write failed: $!\n";
+}
+
+sub send_eof {
+    my ($self) = @_;
+    $self->send_cc(VEOF);
+}
+
+sub send_interrupt {
+    my ($self) = @_;
+    $self->send_cc(VINTR);
+}
+
+1;