summaryrefslogtreecommitdiffstats
path: root/contrib/vm/serial-console
blob: 5d09876e31345ae4bf81558fbfe878de2bf753eb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#!/usr/bin/perl -w

=head1 NAME

serial-console

=head1 SYNOPSIS

serial-console [options]

Options:

    -h,--help         Display brief help message
    -v,--verbose      Increase verbosity
    -q,--quiet        Decrease verbosity
    -l,--log FILE     Log output to file
    -r,--rcfile	FILE  Modify specified bochsrc file

=head1 DESCRIPTION

C<serial-console> provides a virtual serial console for use with
Bochs.  Running C<serial-console> creates a pseudo-tty.  The master
side of this pty is made available to the user for interaction; the
slave device is written to the Bochs configuration file
(C<bochsrc.txt>) for use by a subsequent Bochs session.

=head1 EXAMPLES

=over 4

=item C<serial-console>

Create a virtual serial console for Bochs, modify C<bochsrc.txt>
appropriately.

=item C<serial-console -r ../.bochsrc -l serial.log>

Create a virtual serial console for Bochs, modify C<../.bochsrc>
appropriately, log output to C<serial.log>.

=back

=head1 INVOCATION

Before starting Bochs, run C<serial-console> in a different session
(e.g. a different xterm window).  When you subsequently start Bochs,
anything that the emulated machine writes to its serial port will
appear in the window running C<serial-console>, and anything typed in
the C<serial-console> window will arrive on the emulated machine's
serial port.

You do B<not> need to rerun C<serial-console> afresh for each Bochs
session.

=head1 OPTIONS

=over 4

=item B<-l,--log FILE>

Log all output (i.e. everything that is printed in the
C<serial-console> window) to the specified file.

=item B<-r,--rcfile FILE>

Modify the specified bochsrc file.  The file will be updated to
contain the path to the slave side of the psuedo tty that we create.
The original file will be restored when C<serial-console> exits.  The
default is to modify the file C<bochsrc.txt> in the current directory.

To avoid modifying any bochsrc file, use C<--norcfile>.

=back

=cut

use IO::Pty;
use IO::Select;
use File::Spec::Functions qw ( :ALL );
use Getopt::Long;
use Pod::Usage;
use POSIX qw ( :termios_h );
use strict;
use warnings;

my $o;
my $restore_file = {};
my $restore_termios;
use constant BLOCKSIZE => 8192;

##############################################################################
#
# Parse command line options into options hash ($o)
#
# $o = parse_opts();

sub parse_opts {
  # $o is the hash that will hold the options
  my $o = {
    verbosity => 1,
    rcfile => 'bochsrc.txt',
  };
  # Special handlers for some options
  my $opt_handlers = {
    verbose => sub { $o->{verbosity}++; },
    quiet => sub { $o->{verbosity}--; },
    help => sub { pod2usage(1); },
    norcfile => sub { delete $o->{rcfile}; },
  };
  # Merge handlers into main options hash (so that Getopt::Long can find them)
  $o->{$_} = $opt_handlers->{$_} foreach keys %$opt_handlers;
  # Option specifiers for Getopt::Long
  my @optspec = ( 'help|h|?',
                  'quiet|q+',
                  'verbose|v+',
		  'log|l=s',
		  'rcfile|r=s',
		  'norcfile',
                  );
  # Do option parsing
  Getopt::Long::Configure ( 'bundling' );
  pod2usage("Error parsing command-line options") unless GetOptions (
  $o, @optspec );
  # Clean up $o by removing the handlers
  delete $o->{$_} foreach keys %$opt_handlers;
  return $o;
}

##############################################################################
#
# Modify bochsrc file

sub patch_bochsrc {
  my $active = shift;
  my $pty = shift;

  # Rename active file to backup file
  ( my $vol, my $dir, my $file ) = splitpath ( $active );
  $file = '.'.$file.".serial-console";
  my $backup = catpath ( $vol, $dir, $file );
  rename $active, $backup
      or die "Could not back up $active to $backup: $!\n";

  # Derive line to be inserted
  my $patch = "com1: enabled=1, mode=term, dev=$pty\n";

  # Modify file
  open my $old, "<$backup" or die "Could not open $backup: $!\n";
  open my $new, ">$active" or die "Could not open $active: $!\n";
  print $new <<"EOF";
##################################################
#
# This file has been modified by serial-console.
#
# Do not modify this file; it will be erased when
# serial-console (pid $$) exits and will be
# replaced with the backup copy held in
# $backup.
#
##################################################


EOF
  my $patched;
  while ( my $line = <$old> ) {
    if ( $line =~ /^\s*\#?\s*com1:\s*\S/ ) {
      if ( ! $patched ) {
	$line = $patch;
	$patched = 1;
      } else {
	$line = '# '.$line unless $line =~ /^\s*\#/;
      }
    }
    print $new $line;
  }
  print $new $patch unless $patched;
  close $old;
  close $new;

  return $backup;
}

##############################################################################
#
# Attach/detach message printing and terminal settings

sub bochs_attached {
  print STDERR "Bochs attached.\n\n\n"
      if $o->{verbosity} >= 1;
}

sub bochs_detached {
  print STDERR "\n\nWaiting for bochs to attach...\n"
      if $o->{verbosity} >= 1;
}

##############################################################################
#
# Main program

$o = parse_opts();
pod2usage(1) if @ARGV;

# Catch signals
my $sigdie = sub { die "Exiting via signal\n"; };
$SIG{INT} = $sigdie;

# Create Pty, close slave side
my $pty = IO::Pty->new();
$pty->close_slave();
$pty->set_raw();
print STDERR "Slave pty is ".$pty->ttyname."\n" if $o->{verbosity} >= 1;

# Open logfile
my $log;
if ( $o->{log} ) {
  open $log, ">$o->{log}" or die "Could not open $o->{log}: $!\n";
}

# Set up terminal
my $termios;
if ( -t STDIN ) {
  $termios = POSIX::Termios->new;
  $restore_termios = POSIX::Termios->new;
  $termios->getattr ( fileno(STDIN) );
  $restore_termios->getattr ( fileno(STDIN) );
  $termios->setlflag ( $termios->getlflag & ~(ICANON) & ~(ECHO) );
  $termios->setiflag ( $termios->getiflag & ~(ICRNL) );
  $termios->setattr ( fileno(STDIN), TCSANOW );
}

# Modify bochsrc file
$restore_file = { $o->{rcfile} =>
		  patch_bochsrc ( $o->{rcfile}, $pty->ttyname ) }
    if $o->{rcfile};

# Start character shunt
my $attached = 1;
my $select = IO::Select->new ( \*STDIN, $pty );
while ( 1 ) {
  my %can_read = map { $_ => 1 }
		     $select->can_read ( $attached ? undef : 1 );
  if ( $can_read{\*STDIN} ) {
    sysread ( STDIN, my $data, BLOCKSIZE )
	or die "Cannot read from STDIN: $!\n";
    $pty->syswrite ( $data );
  }
  if ( $can_read{$pty} ) {
    if ( $pty->sysread ( my $data, BLOCKSIZE ) ) {
      # Actual data available
      bochs_attached() if $attached == 0;
      $attached = 1;
      syswrite ( STDOUT, $data );
      $log->syswrite ( $data ) if $log;
    } else {
      # No data available but select() says we can read.  This almost
      # certainly indicates that nothing is attached to the slave.
      bochs_detached() if $attached == 1;
      $attached = 0;
      sleep ( 1 );
    }
  } else {
    bochs_attached() if $attached == 0;
    $attached = 1;
  }
}

END {
  # Restore bochsrc file if applicable
  if ( ( my $orig_file, my $backup_file ) = %$restore_file ) {
    unlink $orig_file;
    rename $backup_file, $orig_file;
  }
  # Restore terminal settings if applicable
  if ( $restore_termios ) {
    $restore_termios->setattr ( fileno(STDIN), TCSANOW );
  }
}