From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id F1F221FF0B2 for ; Fri, 25 Sep 2026 13:00:38 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id A2E7B216B1; Fri, 25 Sep 2026 13:00:34 +0200 (CEST) Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=UTF-8 Date: Fri, 25 Sep 2026 13:00:23 +0200 Message-Id: To: "Proxmox VE development discussion" Cc: "pve-devel" Subject: Re: [pve-devel] [PATCH container] add container console scrollback buffer From: "Thomas Ellmenreich" X-Mailer: aerc 0.20.0 References: <20260121112335.84473-1-f.schauer@proxmox.com> In-Reply-To: <20260121112335.84473-1-f.schauer@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1790334023895 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.440 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URI_HEX 0.1 URI hostname has long hexadecimal sequence Message-ID-Hash: U4F5TKUBKXQIXLQ2CQCXRNUU4ZPOA5J6 X-Message-ID-Hash: U4F5TKUBKXQIXLQ2CQCXRNUU4ZPOA5J6 X-MailFrom: t.ellmenreich@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Thanks for this patch! In my opinion, this would be a great addition. I installed the patch on one of my test nodes and it worked wonderfully. However, looking at the approach, I'm not convinced that having a separate process for each container just for the scrollback buffer is ideal, so I wo= uld definitely make it optional. This is especially the case since the new proc= ess is added regardless of whether the terminal is ever used. That said, I'm no= t quite sure how to improve it. I assume that applying the patch [0] to dtach itself would not solve the problem of the extra process, as dtach would just become that extra process= . Overall, I think the approach is reasonable. I considered having one 'maste= r master' process (xD) for all containers instead of a new process for each container. However, that would introduce a lot of programming complexity. When I first started using it, I thought the buffer was a bit small. Implementing it as you have would allow us to easily make the buffer configurable. I can see larger buffer sizes being useful in some cases. As an aside: I found it especially nice that some context is now retained when refreshing the page. Tested-by: Thomas Ellmenreich [0] https://367015.bugs.gentoo.org/attachment.cgi?id=3D272965 On Wed Jan 21, 2026 at 12:23 PM CET, Filip Schauer wrote: > Send a scrollback buffer of the previous 8192 bytes to new clients > connecting to a container console. > > This is achieved with a basic Perl re-implementation of the dtach master > with a scrollback patch applied. [0] This dtach-scrollback-master is > started alongside the container at startup. When dtach attaches to this > master it receives the scrollback buffer. > > This improves the user experience for containers and is especially > useful for many application containers that print important diagnostic > messages at startup, that would otherwise be easily missed by users > opening the container console slightly too late. > > This change is backwards-compatible: upgrading does not break consoles > for already running containers, since we fall back to the previous dtach > behaviour when dtach-scrollback-master is not running. > > As an alternative to implementing our own dtach-scrollback-master, we > could instead apply the scrollback patch [0] directly to dtach. > > [0] https://367015.bugs.gentoo.org/attachment.cgi?id=3D272965 > > Signed-off-by: Filip Schauer > --- > src/Makefile | 2 + > src/PVE/LXC.pm | 5 ++ > src/dtach-scrollback-master | 99 +++++++++++++++++++++++++++++++++++++ > 3 files changed, 106 insertions(+) > create mode 100755 src/dtach-scrollback-master > > diff --git a/src/Makefile b/src/Makefile > index 2baa782..07a6802 100644 > --- a/src/Makefile > +++ b/src/Makefile > @@ -45,6 +45,8 @@ install: pct lxc-pve.conf pct.1 pct.conf.5 pct.bash-com= pletion pct.zsh-completio > pve-userns.seccomp pve-container@.service pve-container-debug@.servi= ce \ > lxc-pve-prestart-hook lxc-pve-autodev-hook lxc-pve-poststop-hook lxc= netaddbr > PVE_GENERATING_DOCS=3D1 perl -I. -T -e "use PVE::CLI::pct; PVE::CLI::pc= t->verify_api();" > + install -d $(BINDIR) > + install -m 0755 dtach-scrollback-master $(BINDIR) > install -d $(SBINDIR) > install -m 0755 pct $(SBINDIR) > install -d $(LXC_SCRIPT_DIR) > diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm > index c19c772..bab27b8 100644 > --- a/src/PVE/LXC.pm > +++ b/src/PVE/LXC.pm > @@ -3100,6 +3100,11 @@ sub vm_start { > eval { > run_command($cmd); > =20 > + my $concmd =3D PVE::LXC::get_console_command($vmid, $conf, -1); > + run_command([ > + 'dtach-scrollback-master', "/var/run/dtach/vzctlconsole$vmid= ", @$concmd, > + ]); > + > monitor_start($monitor_socket, $vmid) if defined($monitor_socket= ); > =20 > # if debug is requested, print the log it also when the start su= cceeded > diff --git a/src/dtach-scrollback-master b/src/dtach-scrollback-master > new file mode 100755 > index 0000000..8d348e2 > --- /dev/null > +++ b/src/dtach-scrollback-master > @@ -0,0 +1,99 @@ > +#!/usr/bin/perl > + > +use strict; > +use warnings; > + > +use IO::Select; > +use IO::Socket::UNIX; > +use POSIX qw(EAGAIN WNOHANG setsid); > + > +use PVE::PTY; > + > +use constant { > + MSG_PUSH =3D> 0, > + MSG_WINCH =3D> 3, > + MSG_REDRAW =3D> 4, > + PACKET_SIZE =3D> 10, > + SCROLLSIZE =3D> 8192, > + BUFSIZE =3D> 4096, > +}; > + > +my $socketpath =3D shift or die "Usage: $0 [args]\n"; > +unlink($socketpath); > + > +# Daemonize > +my $pid =3D fork() // die "fork failed: $!\n"; > +POSIX::_exit(0) if $pid; > +POSIX::setsid(); > +my $pid2 =3D fork() // die "fork failed: $!\n"; > +POSIX::_exit(0) if $pid2; > +open STDIN, '<', '/dev/null'; > +open STDOUT, '>', '/dev/null'; > +open STDERR, '>', '/dev/null'; > + > +$SIG{PIPE} =3D 'IGNORE'; # Prevent crash on client disconnect > +$SIG{INT} =3D $SIG{TERM} =3D sub { unlink $socketpath; exit(0); }; > + > +my $server =3D IO::Socket::UNIX->new(Local =3D> $socketpath, Listen =3D>= 128, Blocking =3D> 0) > + or die "Cannot create socket - $IO::Socket::errstr\n"; > +my $pty =3D PVE::PTY->new(); > +my $pty_fh =3D $pty->master(); > +my $select =3D IO::Select->new($server, $pty_fh); > +my $scrollback =3D ''; > +my %clientbufs; > + > +# Spawn Child > +my $cpid =3D fork() // die "fork failed: $!"; > +if ($cpid =3D=3D 0) { > + close $server; > + $pty->make_controlling_terminal(); > + exec(@ARGV) or die; > +} > + > +while (waitpid($cpid, WNOHANG) <=3D 0) { > + for my $fh ($select->can_read(1)) { > + if ($fh =3D=3D $server) { > + # Accept new client > + my $client =3D $server->accept(); > + $client->blocking(0); > + $select->add($client); > + syswrite($client, $scrollback); > + $clientbufs{$client} =3D ''; > + } elsif ($fh =3D=3D $pty_fh) { > + # PTY Output > + sysread($fh, my $output, BUFSIZE) or last; > + $scrollback =3D substr($scrollback . $output, -SCROLLSIZE); > + > + for my $client (grep { $_ !=3D $server && $_ !=3D $pty_fh } = $select->handles()) { > + disconnect_client($client) if !defined(syswrite($client,= $output)) && $! !=3D EAGAIN; > + } > + } else { > + # Client Input > + sysread($fh, $clientbufs{$fh}, 500, length($clientbufs{$fh})= ) or do { > + disconnect_client($fh); > + next; > + }; > + > + while (length($clientbufs{$fh}) >=3D PACKET_SIZE) { > + my $packet =3D substr($clientbufs{$fh}, 0, PACKET_SIZE, = ''); > + my ($type, $len, $data) =3D unpack('CCa8', $packet); > + if ($type =3D=3D MSG_PUSH) { > + syswrite($pty_fh, substr($data, 0, $len)); > + } elsif ($type =3D=3D MSG_WINCH || $type =3D=3D MSG_REDR= AW) { > + my ($rows, $cols) =3D unpack('S2', $data); > + $pty->set_size($cols, $rows) if $rows > 0 && $cols >= 0; > + kill('WINCH', $pty->get_foreground_pid()); > + } > + } > + } > + } > +} > + > +unlink $socketpath; > + > +sub disconnect_client { > + my $fh =3D shift; > + $select->remove($fh); > + delete $clientbufs{$fh}; > + close $fh; > +}