]> rtime.felk.cvut.cz Git - novaboot.git/blobdiff - novaboot
nix: Update flake inputs
[novaboot.git] / novaboot
index 2b38717b4ae5f5ae1d39313025538df75b49cce2..cd59748d35e6cb9633f345f6ce2d6d504ec4c149 100755 (executable)
--- a/novaboot
+++ b/novaboot
@@ -1,4 +1,4 @@
-#!/usr/bin/perl -w
+#!/usr/bin/env perl
 
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 
 use strict;
 use warnings;
-use warnings (exists $ENV{NOVABOOT_TEST} ? (FATAL => 'all') : ());
+use warnings (exists $ENV{NOVABOOT_TEST} ?
+              (FATAL => 'all') :
+              (FATAL => qw(inplace))); # Open warnings in <<>> are fatal
 use Getopt::Long qw(GetOptionsFromString GetOptionsFromArray);
 use Pod::Usage;
 use File::Basename;
 use File::Spec;
+use File::Path qw(make_path);
 use IO::Handle;
 use Time::HiRes("usleep");
 use Socket;
 use FileHandle;
 use IPC::Open2;
-use POSIX qw(:errno_h);
+use POSIX qw(:errno_h sysconf);
 use Cwd qw(getcwd abs_path);
 use Expect;
 
@@ -36,6 +39,14 @@ $| = 1;
 
 my $invocation_dir = $ENV{PWD} || getcwd();
 
+# We prefer using PWD, to use nicer paths names with symbolic links.
+# However, when executed from 'make -C dir', PWD may contain the make
+# invocation path, not the real invocation path with dir at the end.
+# We fix that here.
+if (abs_path($ENV{PWD}) ne abs_path(getcwd())) {
+    $invocation_dir = getcwd();
+}
+
 ## Configuration file handling
 
 # Default configuration
@@ -43,19 +54,26 @@ $CFG::hypervisor = "";
 $CFG::hypervisor_params = "serial";
 $CFG::genisoimage = "genisoimage";
 $CFG::qemu = 'qemu-system-i386 -cpu coreduo -smp 2';
-$CFG::default_target = 'qemu';
+$CFG::default_target = '';
+$CFG::netif = 'eth0';
 %CFG::targets = (
     'qemu' => '--qemu',
-    "tud" => '--server=erwin.inf.tu-dresden.de:~sojka/boot/novaboot --rsync-flags="--chmod=Dg+s,ug+w,o-w,+rX --rsync-path=\"umask 002 && rsync\"" --grub --grub-prefix=(nd)/tftpboot/sojka/novaboot --grub-preamble="timeout 0" --concat --iprelay=141.76.48.80:2324 --scriptmod=s/\\\\bhostserial\\\\b/hostserialpci/g',
-    "novabox" => '--server=rtime.felk.cvut.cz:/srv/tftp/novaboot --rsync-flags="--chmod=Dg+s,ug+w,o-w,+rX --rsync-path=\"umask 002 && rsync\"" --pulsar --iprelay=147.32.86.92:2324',
-    "localhost" => '--scriptmod=s/console=tty[A-Z0-9,]+// --server=/boot/novaboot/$NAME --grub2 --grub-prefix=/boot/novaboot/$NAME --grub2-prolog="  set root=\'(hd0,msdos1)\'"',
+    "tud" => '--copy=erwin.inf.tu-dresden.de:~sojka/boot/novaboot --rsync-flags="--chmod=Dg+s,ug+w,o-w,+rX --rsync-path=\"umask 002 && rsync\"" --grub --grub-prefix=(nd)/tftpboot/sojka/novaboot --grub-preamble="timeout 0" --concat --iprelay=141.76.48.80:2324 --scriptmod=s/\\\\bhostserial\\\\b/hostserialpci/g',
+    "novabox" => '--ssh=novabox@rtime.felk.cvut.cz',
+    "localhost" => '--scriptmod=s/console=tty[A-Z0-9,]+// --copy=/boot/novaboot/$NAME --grub2 --grub-prefix=/boot/novaboot/$NAME --grub2-prolog="  set root=\'(hd0,msdos1)\'"',
     "ryu" =>  '--uboot --uboot-init="mw f0000b00 \${psc_cfg}; sleep 1" --uboot-addr kernel=800000 --uboot-addr ramdisk=b00000 --uboot-addr fdt=7f0000',
-    "ryuglab" => '--target ryu --server=pc-sojkam.felk.cvut.cz:/srv/tftp --remote-cmd="ssh -tt pc-sojkam.felk.cvut.cz \"sterm -d -s 115200 /dev/ttyUSB0\""',
+    "ryuglab" => '--target ryu --ssh=ryu@pc-sojkam.felk.cvut.cz',
     "ryulocal" => '--target ryu --dhcp-tftp --serial --reset-cmd="if which dtrrts; then dtrrts $NB_SERIAL 0 1; sleep 0.1; dtrrts $NB_SERIAL 1 1; fi"',
     );
-chomp(my $nproc = `nproc`);
-$CFG::scons = "scons -j$nproc";
-$CFG::make = "make -j$nproc";
+
+{
+    my %const;
+    $const{linux}->{_SC_NPROCESSORS_CONF} = 83;
+    my $nproc = sysconf($const{$^O}->{_SC_NPROCESSORS_CONF});
+
+    $CFG::scons = "scons -j$nproc";
+    $CFG::make = "make -j$nproc";
+}
 
 my $builddir;
 
@@ -111,22 +129,28 @@ read_config($_) foreach $cfg or @cfgs;
 
 ## Command line handling
 
-my $explicit_target;
+my $explicit_target = $ENV{'NOVABOOT_TARGET'};
 GetOptions ("target|t=s" => \$explicit_target);
 
-my ($amt, @append, $bender, @chainloaders, $concat, $config_name_opt, $dhcp_tftp, $dump_opt, $dump_config, @exiton, $exiton_timeout, @expect_raw, $gen_only, $grub_config, $grub_prefix, $grub_preamble, $grub2_prolog, $grub2_config, $help, $ider, $interaction, $iprelay, $iso_image, $interactive, $kernel_opt, $make, $man, $no_file_gen, $off_opt, $on_opt, $pulsar, $pulsar_root, $qemu, $qemu_append, $qemu_flags_cmd, $remote_cmd, $remote_expect, $remote_expect_silent, $reset, $reset_cmd, $rom_prefix, $rsync_flags, @scriptmod, $scons, $serial, $server, $stty, $tftp, $tftp_port, $uboot, %uboot_addr, $uboot_cmd, @uboot_init);
+# Variables for command line options
+my ($amt, @append, $bender, @chainloaders, $concat, $config_name_opt, $dhcp_tftp, $dump_opt, $dump_config, @exiton, $exiton_timeout, @expect_raw, $final_eol, $gen_only, $grub_config, $grub_prefix, $grub_preamble, $grub2_prolog, $grub2_config, $help, $ider, $interaction, $iprelay, $iprelay_cmd, $iso_image, $interactive, $kernel_opt, $make, $man, $netif, $no_file_gen, $off_opt, $on_opt, $pulsar, $pulsar_root, $qemu, $qemu_append, $qemu_flags_cmd, @remote_cmd, $remote_expect, $remote_expect_silent, $remote_expect_timeout, $reset, @reset_cmd, $reset_send, $rom_prefix, $rsync_flags, @scriptmod, $scons, $serial, $server, $stty, $tftp, $tftp_port, $uboot, %uboot_addr, $uboot_cmd, @uboot_init, $uboot_stop_key);
 
+my ($target_reset, $target_power_on, $target_power_off);
+
+# Default values of certain command line options
 %uboot_addr = (
     'kernel'  => '${kernel_addr_r}',
     'ramdisk' => '${ramdisk_addr_r}',
     'fdt'     => '${fdt_addr_r}',
     );
-
 $rsync_flags = '';
 $rom_prefix = 'rom://';
-$stty = 'raw -crtscts -onlcr 115200';
+$stty = 'raw -crtscts -onlcr -echo 115200';
 $reset = 1;                    # Reset target by default
 $interaction = 1;              # Perform target interaction by default
+$final_eol = 1;
+$netif = $CFG::netif;
+$remote_expect_timeout = -1;
 
 my @expect_seen = ();
 sub handle_expect
@@ -146,8 +170,28 @@ sub handle_send
     @expect_seen = ();
 }
 
-my %opt_spec;
-%opt_spec = (
+# Options which can be safely specified on the server (via --ssh),
+# i.e. which cannot cause unwanted local code execution etc.
+my %opt_spec_safe = (
+    "grub|g:s"              => \$grub_config,
+    "grub-preamble=s"=> \$grub_preamble,
+    "grub2-prolog=s" => \$grub2_prolog,
+    "grub2:s"       => \$grub2_config,
+    "prefix|grub-prefix=s" => \$grub_prefix,
+    "pulsar-root=s"  => \$pulsar_root,
+    "pulsar|p:s"     => \$pulsar,
+    "remote-expect=s"=> \$remote_expect,
+    "remote-expect-silent=s"=> sub { $remote_expect=$_[1]; $remote_expect_silent=1; },
+    "remote-expect-timeout=i"=> \$remote_expect_timeout,
+    "uboot-addr=s"   => \%uboot_addr,
+    "uboot-cmd=s"    => \$uboot_cmd,
+    "uboot-stop-key=s" => \$uboot_stop_key,
+    "uboot-init=s"   => sub { push @uboot_init, { command => $_[1] }; },
+    "uboot:s"       => \$uboot,
+    );
+
+my %opt_spec = (
+    %opt_spec_safe,
     "amt=s"         => \$amt,
     "append|a=s"     => \@append,
     "bender|b"       => \$bender,
@@ -163,33 +207,28 @@ my %opt_spec;
     "expect=s"      => \&handle_expect,
     "expect-re=s"    => \&handle_expect,
     "expect-raw=s"   => sub { my ($n, $v) = @_; unshift(@expect_raw, eval($v)); },
+    "final-eol!"     => \$final_eol,
     "gen-only"      => \$gen_only,
-    "grub|g:s"              => \$grub_config,
-    "grub-preamble=s"=> \$grub_preamble,
-    "prefix|grub-prefix=s" => \$grub_prefix,
-    "grub2:s"       => \$grub2_config,
-    "grub2-prolog=s" => \$grub2_prolog,
     "ider"           => \$ider,
     "interaction!"   => \$interaction,
     "iprelay=s"             => \$iprelay,
+    "iprelay-cmd=s"  => \$iprelay_cmd,
     "iso:s"         => \$iso_image,
     "kernel|k=s"     => \$kernel_opt,
     "interactive|i"  => \$interactive,
     "name=s"        => \$config_name_opt,
     "make|m:s"      => \$make,
+    "netif=s"       => \$netif,
     "no-file-gen"    => \$no_file_gen,
     "off"           => \$off_opt,
     "on"            => \$on_opt,
-    "pulsar|p:s"     => \$pulsar,
-    "pulsar-root=s"  => \$pulsar_root,
     "qemu|Q:s"              => \$qemu,
     "qemu-append=s"  => \$qemu_append,
     "qemu-flags|q=s" => \$qemu_flags_cmd,
-    "remote-cmd=s"   => \$remote_cmd,
-    "remote-expect=s"=> \$remote_expect,
-    "remote-expect-silent=s"=> sub { $remote_expect=$_[1]; $remote_expect_silent=1; },
+    "remote-cmd=s"   => sub { @remote_cmd = ($_[1]); },
     "reset!"         => \$reset,
-    "reset-cmd=s"    => \$reset_cmd,
+    "reset-cmd=s"    => sub { @reset_cmd = ($_[1]); },
+    "reset-send=s"   => \$reset_send,
     "rsync-flags=s"  => \$rsync_flags,
     "scons:s"       => \$scons,
     "scriptmod=s"    => \@scriptmod,
@@ -197,19 +236,47 @@ my %opt_spec;
     "sendcont=s"     => \&handle_send,
     "serial|s:s"     => \$serial,
     "server:s"              => \$server,
+    "copy:s"        => \$server,
+    "ssh:s"         => \&handle_novaboot_server,
     "strip-rom"             => sub { $rom_prefix = ''; },
     "stty=s"        => \$stty,
     "tftp"          => \$tftp,
     "tftp-port=i"    => \$tftp_port,
-    "uboot:s"       => \$uboot,
     "no-uboot"      => sub { undef $uboot; },
-    "uboot-addr=s"   => \%uboot_addr,
-    "uboot-cmd=s"    => \$uboot_cmd,
-    "uboot-init=s"   => \@uboot_init,
     "h"             => \$help,
     "help"          => \$man,
     );
 
+sub handle_novaboot_server
+{
+    my ($n, $val) = @_;
+    my $xdg_runtime_dir = $ENV{XDG_RUNTIME_DIR} || '/var/run';
+    my $ssh_ctl_path = "${xdg_runtime_dir}/novaboot$$";
+
+    @remote_cmd = ('ssh', '-tt', '-M', '-S', $ssh_ctl_path, $val, 'console');
+    $remote_expect = "novaboot-shell: Connected";
+    $server = "$val:";
+    $rsync_flags = "--rsh='ssh -S \'${ssh_ctl_path}\''";
+    ($grub_prefix = $val) =~ s|(.*)@.*|\/$1\/| if index($val, '@') != -1;
+    @reset_cmd = ('ssh', '-tt', '-S', $ssh_ctl_path, $val, 'reset');
+
+    $target_power_off = sub { system_verbose('ssh', '-tt', '-S', $ssh_ctl_path, $val, 'off'); };
+    $target_power_on  = sub { system_verbose('ssh', '-tt', '-S', $ssh_ctl_path, $val, 'on'); };
+
+    my $cmd = "ssh '${val}' get-config";
+    print STDERR "novaboot: Running: $cmd\n";
+    my @target_config = qx($cmd  < /dev/null);
+    if ($?) { die("Cannot get target configuration from the server"); }
+    printf "novaboot: Received configuration from the server:%s\n", (!@target_config) ? " empty" : "";
+    foreach (@target_config) { chomp; print "  $_\n"; }
+
+    my $p = Getopt::Long::Parser->new;
+    $p->configure(qw/no_ignore_case no_pass_through/);
+    $p->getoptionsfromarray(\@target_config, %opt_spec_safe) or die("Error processing configuration from the server");
+
+    if (scalar @target_config) { die "Unsuported configuration received from the server: ".join(", ", @target_config); }
+}
+
 # First process target options
 {
     my $t = defined($explicit_target) ? $explicit_target : $CFG::default_target;
@@ -224,6 +291,10 @@ my %opt_spec;
        push(@target_expanded, @$remaining_args);
        $t = $explicit_target;
     }
+
+    my @args = (@target_expanded, @ARGV);
+    print STDERR "novaboot: Effective options: @args\n";
+
     Getopt::Long::Configure(qw/no_ignore_case no_pass_through/);
     GetOptionsFromArray(\@target_expanded, %opt_spec) or die ("Error in target definition");
 }
@@ -264,8 +335,9 @@ if ($ider) {
 
 {
     my %input_opts = ('--iprelay'    => \$iprelay,
+                     '--iprelay-cmd'=> \$iprelay_cmd,
                      '--serial'     => \$serial,
-                     '--remote-cmd' => \$remote_cmd,
+                     '--remote-cmd' => (@remote_cmd ? \$remote_cmd[0] : undef),
                      '--amt'        => \$amt);
     my @opts = grep(defined(${$input_opts{$_}}) , keys %input_opts);
 
@@ -278,16 +350,16 @@ if (defined $serial) {
     $ENV{NB_SERIAL} = $serial;
 }
 if (defined $grub_config) { $grub_config ||= "menu.lst"; }
-if (defined $grub2_config) { $grub2_config ||= "grub.cfg"; }
+if (defined $grub2_config) { $grub2_config ||= "./boot/grub/grub.cfg"; }
 
 ## Parse the novaboot script(s)
 my @scripts;
 my $file;
 my $EOF;
 my $last_fn = '';
-my ($modules, $variables, $generated, $continuation) = ([], {}, []);
+my ($modules, $variables, $generated, $copy, $chainload, $continuation) = ([], {}, [], []);
 my $skip_reading = defined($on_opt) || defined($off_opt);
-while (!$skip_reading && ($_ = <>)) {
+while (!$skip_reading && ($_ = <<>>)) {
     if ($ARGV ne $last_fn) { # New script
        die "Missing EOF in $last_fn" if $file;
        die "Unfinished line in $last_fn" if $continuation;
@@ -295,7 +367,10 @@ while (!$skip_reading && ($_ = <>)) {
        push @scripts, { 'filename' => $ARGV,
                         'modules' => $modules = [],
                         'variables' => $variables = {},
-                        'generated' => $generated = []};
+                        'generated' => $generated = [],
+                        'copy' => $copy = [],
+                        'chainload' => $chainload = [],
+       };
 
     }
     chomp();
@@ -326,38 +401,53 @@ while (!$skip_reading && ($_ = <>)) {
     if (/^([A-Z_]+)=(.*)$/) {  # Internal variable
        $$variables{$1} = $2;
        push(@exiton, $2) if ($1 eq "EXITON");
+       $interaction = $2 if ($1 eq "INTERACTION");
        next;
     }
-    if (s/^load *//) {         # Load line
+    sub process_load_copy($) {
        die("novaboot: '$last_fn' line $.: Missing file name\n") unless /^[^ <]+/;
        if (/^([^ ]*)(.*?)[[:space:]]*<<([^ ]*)$/) { # Heredoc start
-           push @$modules, "$1$2";
            $file = [];
            push @$generated, {filename => $1, content => $file};
            $EOF = $3;
-           next;
+           return "$1$2";
        }
        if (/^([^ ]*)(.*?)[[:space:]]*< ?(.*)$/) { # Command substitution
-           push @$modules, "$1$2";
            push @$generated, {filename => $1, command => $3};
-           next;
+           return "$1$2";
        }
-       push @$modules, $_;
+       s/\s*$//;               # Strip trailing whitespace
+       return $_;
+    }
+    if (s/^load *//) {         # Load line
+       push @$modules, process_load_copy($_);
+       next;
+    }
+    if (s/^copy *//) {         # Copy line
+       push @$copy, process_load_copy($_);
+       next;
+    }
+    if (s/^chld *//) {         # Chainload line
+       push @$chainload, process_load_copy($_);
        next;
     }
     if (/^run (.*)/) {         # run line
        push @$generated, {command => $1};
        next;
     }
-    if (/^uboot(?::([0-9]+)s)? (.*)/) {        # uboot line
+    if (/^uboot(?::([0-9]+)s)? +(< *)?(.*)/) { # uboot line
        # TODO: If U-Boot supports some interactive menu, it might
        # make sense to store uboot lines per novaboot script.
-       if ($1) {               # Command with explicit timeout
-           push @uboot_init, { command => $2,
-                               timeout => $1 };
-       } else {                # Command without explicit timeout
-           push @uboot_init, $2;
+       my ($timeout, $redir, $string, $dest) = ($1, $2, $3);
+       if ($string =~ /(.*) *> *(.*)/) {
+           $string = $1;
+           $dest = $2;
        }
+       push @uboot_init, { command => $redir ? "" : $string,
+                           system =>  $redir ? $string : "",
+                           timeout => $timeout,
+                           dest => $dest,
+       };
        next;
     }
 
@@ -457,7 +547,7 @@ sub generate_syslinux_config($$$$)
 
     if (system("file $kbin|grep 'Linux kernel'") == 0) {
        my $initrd = @$modules_ref[1];
-       die('To many "load" lines for Linux kernel') if (scalar @$modules_ref > 2);
+       die('Too many "load" lines for Linux kernel') if (scalar @$modules_ref > 2);
        print $fg "LINUX $base$kbin\n";
        print $fg "APPEND $kcmd\n";
        print $fg "INITRD $base$initrd\n";
@@ -482,24 +572,36 @@ sub generate_grub2_config($$$$;$$)
 {
     my ($filename, $title, $base, $modules_ref, $preamble, $prolog) = @_;
     if ($base && substr($base,-1,1) ne '/') { $base = "$base/"; };
+    my $dir = dirname($filename);
+    make_path($dir, {
+                chmod => 0755,
+    });
     open(my $fg, '>', $filename) or die "$filename: $!";
     print $fg "$preamble\n" if $preamble;
     $title ||= 'novaboot';
     print $fg "menuentry $title {\n";
     print $fg "$prolog\n" if $prolog;
     my $first = 1;
+    my $boot_method = $variables->{BOOT_METHOD} // "multiboot";
+    my $module_load_method = "module";
+    if ($boot_method eq "linux") {
+       $module_load_method = "initrd";
+       die('Too many "load" lines for Linux kernel') if (scalar(@$modules_ref) > 2);
+    }
     foreach (@$modules_ref) {
        if ($first) {
            $first = 0;
            my ($kbin, $kcmd) = split(' ', $_, 2);
            $kcmd = '' if !defined $kcmd;
-           print $fg "  multiboot ${base}$kbin $kcmd\n";
+           print $fg "  $boot_method ${base}$kbin $kcmd\n";
        } else {
            my @args = split;
-           # GRUB2 doesn't pass filename in multiboot info so we have to duplicate it here
-           $_ = join(' ', ($args[0], @args));
-           s|\brom://|$rom_prefix|g; # We do not need to translate path for GRUB2
-           print $fg "  module $base$_\n";
+           if ($boot_method eq "multiboot") {
+               # GRUB2 doesn't pass filename in multiboot info so we have to duplicate it here
+               $_ = join(' ', ($args[0], @args));
+               s|\brom://|$rom_prefix|g; # We do not need to translate path for GRUB2
+           }
+           print $fg "  $module_load_method $base$_\n";
        }
     }
     print $fg "}\n";
@@ -508,26 +610,30 @@ sub generate_grub2_config($$$$;$$)
     return $filename;
 }
 
-sub generate_pulsar_config($$)
+sub generate_pulsar_config($$$)
 {
-    my ($filename, $modules_ref) = @_;
+    my ($filename, $modules_ref, $chainload_ref) = @_;
     open(my $fg, '>', $filename) or die "$filename: $!";
     print $fg "root $pulsar_root\n" if defined $pulsar_root;
-    my $first = 1;
-    my ($kbin, $kcmd);
-    foreach (@$modules_ref) {
-       if ($first) {
-           $first = 0;
-           ($kbin, $kcmd) = split(' ', $_, 2);
-           $kcmd = '' if !defined $kcmd;
-       } else {
-           my @args = split;
-           s|\brom://|$rom_prefix|g;
-           print $fg "load $_\n";
+    if (scalar(@$chainload_ref) > 0) {
+       print $fg "chld $$chainload_ref[0]\n";
+    } else {
+       my $first = 1;
+       my ($kbin, $kcmd);
+       foreach (@$modules_ref) {
+           if ($first) {
+               $first = 0;
+               ($kbin, $kcmd) = split(' ', $_, 2);
+               $kcmd = '' if !defined $kcmd;
+           } else {
+               my @args = split;
+               s|\brom://|$rom_prefix|g;
+               print $fg "load $_\n";
+           }
        }
+       # Put kernel as last - this is needed for booting Linux and has no influence on non-Linux OSes
+       print $fg "exec $kbin $kcmd\n";
     }
-    # Put kernel as last - this is needed for booting Linux and has no influence on non-Linux OSes
-    print $fg "exec $kbin $kcmd\n";
     close($fg);
     print("novaboot: Created $builddir/$filename\n");
     return $filename;
@@ -535,7 +641,11 @@ sub generate_pulsar_config($$)
 
 sub shell_cmd_string(@)
 {
-    return join(' ', map((/^[-_=a-zA-Z0-9\/\.\+]+$/ ? "$_" : "'$_'"), @_));
+    if (scalar(@_) == 1) {
+       return $_[0];
+    } else {
+       return join(' ', map((/^[-_=a-zA-Z0-9\/\.\+]+$/ ? "$_" : "'$_'"), @_));
+    }
 }
 
 sub exec_verbose(@)
@@ -545,11 +655,10 @@ sub exec_verbose(@)
     exit(1); # should not be reached
 }
 
-sub system_verbose($)
+sub system_verbose
 {
-    my $cmd = shift;
-    print STDERR "novaboot: Running: $cmd\n";
-    my $ret = system($cmd);
+    print STDERR "novaboot: Running: ".shell_cmd_string(@_)."\n";
+    my $ret = system(@_);
     if ($ret & 0x007f) { die("Command terminated by a signal"); }
     if ($ret & 0xff00) {die("Command exit with non-zero exit code"); }
     if ($ret) { die("Command failure $ret"); }
@@ -579,28 +688,61 @@ if (exists $variables->{WVDESC}) {
 
 my $exp; # Expect object to communicate with the target over serial line
 
-my ($target_reset, $target_power_on, $target_power_off);
+sub kill_exp_on_signal() {
+    # Sometimes, under unclear circumstances (e.g. when running under
+    # both Jenkins and Robotframework), novaboot does not terminate
+    # console command when killed. The console is then blocked by the
+    # stale process forever. Theoretically, this should not happen,
+    # because when novaboot is killed, console command's controlling
+    # terminal sends SIGHUP to the console command and the command
+    # should terminate. It seems that at least SSH sometimes ignores
+    # HUP and does not terminate. The code below seems to work around
+    # that problem by killing the process immediately with SIGTERM,
+    # which is not ignored.
+
+    sub kill_console { kill TERM => $exp->pid if $exp->pid; die "Terminated by SIG$_[0]"; };
+
+    # For our Jenkins/Robotframework use case, it was sufficient to
+    # handle the TERM signal, but to be on the safe side, we also
+    # catch other signals.
+    $SIG{TERM} = \&kill_console;
+    $SIG{HUP} = \&kill_console;
+    $SIG{INT} = \&kill_console;
+    $SIG{QUIT} = \&kill_console;
+}
+
+
 my ($amt_user, $amt_password, $amt_host, $amt_port);
 
-if (defined $iprelay) {
-    my $IPRELAY;
-    $iprelay =~ /([.0-9]+)(:([0-9]+))?/;
-    my $addr = $1;
-    my $port = $3 || 23;
-    my $paddr   = sockaddr_in($port, inet_aton($addr));
-    my $proto   = getprotobyname('tcp');
-    socket($IPRELAY, PF_INET, SOCK_STREAM, $proto)  || die "socket: $!";
-    print STDERR "novaboot: Connecting to IP relay... ";
-    connect($IPRELAY, $paddr)    || die "connect: $!";
-    print STDERR "done\n";
-    $exp = Expect->init(\*$IPRELAY);
-    $exp->log_stdout(1);
+if (defined $iprelay || defined $iprelay_cmd) {
+    if (defined $iprelay) {
+       my $IPRELAY;
+       $iprelay =~ /([.0-9]+)(:([0-9]+))?/;
+       my $addr = $1;
+       my $port = $3 || 23;
+       my $paddr   = sockaddr_in($port, inet_aton($addr));
+       my $proto   = getprotobyname('tcp');
+       socket($IPRELAY, PF_INET, SOCK_STREAM, $proto)  || die "socket: $!";
+       print STDERR "novaboot: Connecting to IP relay... ";
+       connect($IPRELAY, $paddr)    || die "connect: $!";
+       print STDERR "done\n";
+       $exp = Expect->init(\*$IPRELAY);
+       $exp->log_stdout(1);
+    }
+    if (defined $iprelay_cmd) {
+       print STDERR "novaboot: Running: $iprelay_cmd\n";
+       $exp = new Expect;
+       $exp->raw_pty(1);
+       $exp->spawn($iprelay_cmd);
+       kill_exp_on_signal();
+    }
 
     while (1) {
        print $exp "\xFF\xF6";  # AYT
        my $connected = $exp->expect(20, # Timeout in seconds
                                     '<iprelayd: connected>',
-                                    '-re', '<WEB51 HW[^>]*>');
+                                    '-re', '<WEB51 HW[^>]*>')
+           || die "iprelay connection: " . ($! || "timeout");
        last if $connected;
     }
 
@@ -625,7 +767,8 @@ if (defined $iprelay) {
        $exp->log_stdout(0);
        print $exp relaycmd($relay, $onoff);
        my $confirmed = $exp->expect(20, # Timeout in seconds
-                                    relayconf($relay, $onoff));
+                                    relayconf($relay, $onoff))
+           || die "iprelay command: " . ($! || "timeout");
        if (!$confirmed) {
            if ($can_giveup) {
                print("Relay confirmation timeout - ignoring\n");
@@ -660,9 +803,10 @@ elsif ($serial) {
     open($CONN, "+<", $serial) || die "open $serial: $!";
     $exp = Expect->init(\*$CONN);
 }
-elsif ($remote_cmd) {
-    print STDERR "novaboot: Running: $remote_cmd\n";
-    $exp = Expect->spawn($remote_cmd);
+elsif (@remote_cmd) {
+    print STDERR "novaboot: Running: ".shell_cmd_string(@remote_cmd)."\n";
+    $exp = Expect->spawn(@remote_cmd);
+    kill_exp_on_signal();
 }
 elsif (defined $amt) {
     require LWP::UserAgent;
@@ -761,8 +905,7 @@ END
     my $cmd = "amtterm -u $amt_user -p $amt_password $amt_host $amt_port";
     print STDERR "novaboot: Running: $cmd\n" =~ s/\Q$amt_password\E/???/r;
     $exp = Expect->spawn($cmd);
-    $exp->expect(10, "RUN_SOL") || die "Expect for 'RUN_SOL' timed out";
-
+    $exp->expect(10, "RUN_SOL") || die "Expect for 'RUN_SOL': " . ($! || "timeout");
 }
 
 
@@ -772,16 +915,24 @@ if ($remote_expect) {
     if (defined $remote_expect_silent) {
        $exp->log_stdout(0);
     }
-    $exp->expect(180, $remote_expect) || die "Expect for '$remote_expect' timed out";
+    $exp->expect($remote_expect_timeout >= 0 ? $remote_expect_timeout : undef,
+                $remote_expect) || die "Expect for '$remote_expect':" . ($! || "timeout");;
     if (defined $remote_expect_silent) {
        $exp->log_stdout($log);
        print $exp->after() if $log;
     }
 }
 
-if (defined $reset_cmd) {
+if (@reset_cmd) {
     $target_reset = sub {
-       system_verbose($reset_cmd);
+       system_verbose(@reset_cmd);
+    };
+}
+
+if (defined $reset_send) {
+    $target_reset = sub {
+       $reset_send =~ s/\\n/\n/g;
+       $exp->send($reset_send);
     };
 }
 
@@ -835,11 +986,16 @@ foreach my $script (@scripts) {
     my @bootloader_configs;
     push @bootloader_configs, generate_grub_config($grub_config, $config_name, $prefix, $modules, $grub_preamble) if (defined $grub_config);
     push @bootloader_configs, generate_grub2_config($grub2_config, $config_name, $prefix, $modules, $grub_preamble, $grub2_prolog) if (defined $grub2_config);
-    push @bootloader_configs, generate_pulsar_config('config-'.($pulsar||'novaboot'), $modules) if (defined $pulsar);
+    push @bootloader_configs, generate_pulsar_config('config-'.($pulsar||'novaboot'), $modules, $chainload) if (defined $pulsar);
 
 ### Run scons or make
     {
-       my @files = map({ ($file) = m/([^ ]*)/; $file; } @$modules);
+       my @all;
+       push @all, @$modules;
+       push @all, @$copy;
+       push @all, @$chainload;
+       my @files = map({ ($file) = m/([^ ]*)/; $file; } @all);
+
        # Filter-out generated files
        my @to_build = grep({ my $file = $_; !scalar(grep($file eq ($$_{filename} || ''), @$generated)) } @files);
 
@@ -856,14 +1012,16 @@ foreach my $script (@scripts) {
            $path = $hostname;
            $hostname = "";
        }
-       my $files = join(" ", map({ ($file) = m/([^ ]*)/; $file; } ( @$modules, @bootloader_configs)));
-       map({ my $file = (split)[0]; die "$file: $!" if ! -f $file; } @$modules);
+       my $files = join(" ", map({ ($file) = m/([^ ]*)/; $file; } ( @$modules, @bootloader_configs, @$copy)));
+       map({ my $file = (split)[0]; die "Not a file: $file: $!" if ! -e $file || -d $file; } @$modules);
        my $istty = -t STDOUT && ($ENV{'TERM'} || 'dumb') ne 'dumb';
        my $progress = $istty ? "--progress" : "";
-       system_verbose("rsync $progress -RLp $rsync_flags $files $real_server");
-       if ($server =~ m|/\$NAME$| && $concat) {
-           my $cmd = join("; ", map { "( cd $path/.. && cat */$_ > $_ )" } @bootloader_configs);
-           system_verbose($hostname ? "ssh $hostname '$cmd'" : $cmd);
+       if ($files) {
+           system_verbose("rsync $progress -RL --chmod=ugo=rwX $rsync_flags $files $real_server");
+           if ($server =~ m|/\$NAME$| && $concat) {
+               my $cmd = join("; ", map { "( cd $path/.. && cat */$_ > $_ )" } @bootloader_configs);
+               system_verbose($hostname ? "ssh $hostname '$cmd'" : $cmd);
+           }
        }
     }
 
@@ -885,11 +1043,17 @@ if (defined $iso_image) {
     if (-f '/usr/lib/ISOLINUX/isolinux.bin') {
        # Newer ISOLINUX version
        @files = qw(/usr/lib/ISOLINUX/isolinux.bin /usr/lib/syslinux/modules/bios/mboot.c32 /usr/lib/syslinux/modules/bios/libcom32.c32 /usr/lib/syslinux/modules/bios/menu.c32 /usr/lib/syslinux/modules/bios/ldlinux.c32);
-    } else {
+    } elsif (-f '/usr/lib/syslinux/isolinux.bin') {
        # Older ISOLINUX version
        @files = qw(/usr/lib/syslinux/isolinux.bin /usr/lib/syslinux/mboot.c32 /usr/lib/syslinux/menu.c32);
+    } else {
+       # NixOS and maybe others
+       my $syslinux = `which syslinux` || die "Cannot find syslinux";
+       chomp $syslinux;
+       $syslinux =~ s,/bin/syslinux$,,;
+       @files = ("$syslinux/share/syslinux/isolinux.bin", "$syslinux/share/syslinux/mboot.c32", "$syslinux/share/syslinux/libcom32.c32", "$syslinux/share/syslinux/menu.c32", "$syslinux/share/syslinux/ldlinux.c32");
     }
-    system_verbose("cp @files isolinux");
+    system_verbose("cp @files isolinux && chmod +w isolinux/*");
     open(my $fh, ">isolinux/isolinux.cfg");
     if ($#scripts) {
        print $fh "TIMEOUT 50\n";
@@ -957,7 +1121,9 @@ if (defined $qemu) {
            push(@qemu_flags, ('-dtb', $dtb)) if $dtb;
        }
     }
-    push(@qemu_flags,  qw(-serial stdio)); # Redirect serial output (for collecting test restuls)
+    if (!grep /^-serial$/, @qemu_flags) {
+       push(@qemu_flags,  qw(-serial stdio)); # Redirect serial output (for collecting test restuls)
+    }
     unshift(@qemu_flags, ('-name', $config_name));
     print STDERR "novaboot: Running: ".shell_cmd_string($qemu, @qemu_flags)."\n";
     $exp = Expect->spawn(($qemu, @qemu_flags)) || die("exec() failed: $!");
@@ -975,20 +1141,29 @@ if (defined $dhcp_tftp)
     system_verbose('mkdir -p tftpboot');
     generate_grub_config("tftpboot/os-menu.lst", $config_name, "(nd)", \@$modules, "timeout 0");
     open(my $fh, '>', 'dhcpd.conf');
-    my $mac = `cat /sys/class/net/eth0/address`;
+    my $mac = `cat /sys/class/net/$netif/address`;
     chomp $mac;
-    print $fh "subnet 10.23.23.0 netmask 255.255.255.0 {
-                     range 10.23.23.10 10.23.23.100;
-                     filename \"bin/boot/grub/pxegrub.pxe\";
-                     next-server 10.23.23.1;
+    print $fh "
+subnet 10.23.23.0 netmask 255.255.255.0 {
+       range 10.23.23.10 10.23.23.100;
+       next-server 10.23.23.1;
+}
+class \"pxe-clients\" {
+     match option vendor-class-identifier;
+}
+subclass \"pxe-clients\"  \"PXEClient:Arch:00000:UNDI:002001\" {
+     option bootfile-name \"boot/grub/i386-pc/core.0\";
+}
+subclass \"pxe-clients\"  \"PXEClient:Arch:00007:UNDI:003016\" {
+     option bootfile-name \"boot/grub/x86_64-efi/core.efi\";
 }
 host server {
        hardware ethernet $mac;
        fixed-address 10.23.23.1;
 }";
     close($fh);
-    system_verbose("sudo ip a add 10.23.23.1/24 dev eth0;
-           sudo ip l set dev eth0 up;
+    system_verbose("sudo ip a add 10.23.23.1/24 dev $netif;
+           sudo ip l set dev $netif up;
            sudo touch dhcpd.leases");
 
     # We run servers by forking ourselves, because the servers end up
@@ -1000,16 +1175,28 @@ host server {
 
 if (defined $dhcp_tftp || defined $tftp) {
     $tftp_port ||= 69;
+    my $tftp_root = "$builddir";
+    $tftp_root = "$server" if(defined $server);
+
+    # Prepare a GRUB netboot directory
+    system_verbose("grub-mknetdir --net-directory=$tftp_root") if (defined $grub2_config);
+
+    # Generate TFTP mapfile
+    open(my $fh, '>', "$tftp_root/mapfile");
+    print $fh "# Some PXE clients (mainly UEFI) have bug. They add zero byte to the end of the
+# path name. This rule removes it
+r     \\.efi.*   \\.efi";
+    close($fh);
     # Unfortunately, tftpd requires root privileges even with
     # non-privileged (>1023) port due to initgroups().
-    system_verbose("sudo in.tftpd --listen --secure -v -v -v --pidfile tftpd.pid  --address :$tftp_port $builddir");
+    system_verbose("sudo in.tftpd --listen --secure -v -v -v --pidfile tftpd.pid -m mapfile --address :$tftp_port $tftp_root");
 
     # Kill server when we die
     $SIG{__DIE__} = sub { system_verbose('sudo pkill --pidfile=dhcpd.pid') if (defined $dhcp_tftp);
-                         system_verbose('sudo pkill --pidfile=tftpd.pid'); };
+                         system_verbose("sudo pkill --pidfile=$tftp_root/tftpd.pid"); };
 
     # We have to kill tftpd explicitely, because it is not in our process group
-    $SIG{INT} = sub { system_verbose('sudo pkill --pidfile=tftpd.pid'); exit(0); };
+    $SIG{INT} = sub { system_verbose("sudo pkill --pidfile=$tftp_root/tftpd.pid"); exit(0); };
 }
 
 ### AMT IDE-R
@@ -1029,7 +1216,7 @@ if (defined $ider) {
 ### Reset target (IP relay, AMT, ...)
 
 if (defined $target_reset && $reset) {
-    print STDERR "novaboot: Reseting the test box... ";
+    print STDERR "novaboot: Resetting the test box... ";
     &$target_reset();
     print STDERR "done\n";
     if (defined $exp) {
@@ -1056,19 +1243,23 @@ if (defined $uboot) {
     $exp->log_stdout(1);
     #$exp->exp_internal(1);
     $exp->expect(20,
-                [qr/Hit any key to stop autoboot:/, sub { $exp->send("\n"); exp_continue; }],
+                [qr/Hit any key to stop autoboot:/, sub {
+                    $exp->send($uboot_stop_key // "\n");
+                    exp_continue; }],
                 $uboot_prompt) || die "No U-Boot prompt deteceted";
     foreach my $cmdspec (@uboot_init) {
        my ($cmd, $timeout);
-       if (ref($cmdspec) eq "HASH") {
-           $cmd = $cmdspec->{command};
-           $timeout = $cmdspec->{timeout};
+       die "Internal error - please report a bug" unless ref($cmdspec) eq "HASH";
+
+       if ($cmdspec->{system}) {
+           $cmd = `$cmdspec->{system}`;
        } else {
-           $cmd = $cmdspec;
-           $timeout = 10;
+           $cmd = $cmdspec->{command};
        }
+       $timeout = $cmdspec->{timeout} // 10;
+
        if ($cmd =~ /\$NB_MYIP/) {
-           my $ip = (grep /inet /, `ip addr show eth0`)[0] || die "Problem determining our IP address";
+           my $ip = (grep /inet /, `ip addr show $netif`)[0] || die "Problem determining IP address of $netif";
            $ip =~ s/\s*inet ([0-9.]*).*/$1/;
            $cmd =~ s/\$NB_MYIP/$ip/g;
        }
@@ -1079,52 +1270,67 @@ if (defined $uboot) {
        }
        chomp($cmd);
        $exp->send("$cmd\n");
-       $exp->expect($timeout, $uboot_prompt) || die "U-Boot prompt timeout";
+
+       my ($matched_pattern_position, $error,
+           $successfully_matching_string,
+           $before_match, $after_match) =
+               $exp->expect($timeout, $uboot_prompt);
+       die "No U-Boot prompt: $error" if $error;
+
+       if ($cmdspec->{dest}) {
+           open(my $fh, ">", $cmdspec->{dest}) or die "Cannot open '$cmdspec->{dest}': $!";
+           print $fh $before_match;
+           close($fh);
+       }
     }
 
-    # Boot the system if there are some load lines in the script
-    if ((scalar(@$modules) > 0 && !$variables->{NO_BOOT}) ||
-       defined $uboot_cmd) {
+    # Load files if there are some load lines in the script
+    if (scalar(@$modules) > 0  && !$variables->{NO_BOOT}) {
        my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
        my $dtb;
        @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
        my $initrd = shift @$modules;
 
-       if (defined $kbin) {
+       if (defined $kbin && $kbin ne '/dev/null') {
            die "No '--uboot-addr kernel' given" unless $uboot_addr{kernel};
            $exp->send("tftpboot $uboot_addr{kernel} $prefix$kbin\n");
-           $exp->expect(10,
-                        [qr/##/, sub { exp_continue; }],
-                        $uboot_prompt) || die "Kernel load timeout";
+           $exp->expect(15,
+                         $uboot_prompt,
+                        [qr/#/, sub { exp_continue; }]
+                ) || die "Kernel load: " . ($! || "timeout");
        }
        if (defined $dtb) {
            die "No '--uboot-addr fdt' given" unless $uboot_addr{fdt};
            $exp->send("tftpboot $uboot_addr{fdt} $prefix$dtb\n");
-           $exp->expect(10,
+           $exp->expect(15,
                         [qr/##/, sub { exp_continue; }],
-                        $uboot_prompt) || die "Device tree load timeout";
+                        $uboot_prompt) || die "Device tree load: " . ($! || "timeout");
+       } else  {
+           $uboot_addr{fdt} = '';
        }
        if (defined $initrd) {
            die "No '--uboot-addr ramdisk' given" unless $uboot_addr{ramdisk};
            $exp->send("tftpboot $uboot_addr{ramdisk} $prefix$initrd\n");
-           $exp->expect(10,
+           $exp->expect(15,
                         [qr/##/, sub { exp_continue; }],
-                        $uboot_prompt) || die "Initrd load timeout";
+                        $uboot_prompt) || die "Initrd load: " . ($! || "timeout");
        } else {
            $uboot_addr{ramdisk} = '-';
        }
 
        $kcmd //= '';
        $exp->send("setenv bootargs $kcmd\n");
-       $exp->expect(5, $uboot_prompt)  || die "U-Boot prompt timeout";
+       $exp->expect(5, $uboot_prompt)  || die "U-Boot prompt: " . ($! || "timeout");
 
-       $uboot_cmd //= $variables->{UBOOT_CMD} // 'bootm $kernel_addr $ramdisk_addr $fdt_addr';
+    }
+    $uboot_cmd //= $variables->{UBOOT_CMD} // 'bootm $kernel_addr $ramdisk_addr $fdt_addr';
+    if (!$variables->{NO_BOOT} && $uboot_cmd ne '') {
        $uboot_cmd =~ s/\$kernel_addr/$uboot_addr{kernel}/g;
        $uboot_cmd =~ s/\$ramdisk_addr/$uboot_addr{ramdisk}/g;
        $uboot_cmd =~ s/\$fdt_addr/$uboot_addr{fdt}/g;
 
        $exp->send($uboot_cmd . "\n");
-       $exp->expect(5, "\n")  || die "U-Boot command timeout";
+       $exp->expect(5, "\n")  || die "U-Boot command: " . ($! || "timeout");
     }
 }
 
@@ -1138,8 +1344,7 @@ if ($interaction && defined $exp) {
     print STDERR "novaboot: Serial line interaction (press $interrupt to interrupt)...\n";
     $exp->log_stdout(1);
     if (@exiton) {
-       $exp->expect($exiton_timeout, @expect_raw, @exiton) || die("exiton timeout");
-       print STDERR "\n";
+       $exp->expect($exiton_timeout, @exiton, @expect_raw) || die("exiton: " . ($! || "timeout"));
     } else {
        my @inputs = ($exp);
        my $infile = new IO::File;
@@ -1157,17 +1362,29 @@ if ($interaction && defined $exp) {
        #use Data::Dumper;
        #print Dumper(\@expect_raw);
        $exp->expect(undef, @expect_raw) if @expect_raw;
+
+       $^W = 0; # Suppress Expect warning: handle id(3) is not a tty. Not changing mode at /usr/share/perl5/Expect.pm line 393, <> line 8.
        Expect::interconnect(@inputs) unless defined($exp->exitstatus);
+       $^W = 1;
     }
 }
 
+# When exp-spawned command ignores SIGHUP, Expect waits 5 seconds
+# before killing it. We kill it by SIGTERM immediately.
+kill TERM => $exp->pid if defined $exp && $exp->pid;
+
 ## Kill dhcpc or tftpd
 if (defined $dhcp_tftp || defined $tftp) {
     die("novaboot: This should kill servers on background\n");
 }
 
+# Always finish novaboot output with newline
+print "\n" if $final_eol;
+
 ## Documentation
 
+=encoding utf8
+
 =head1 NAME
 
 novaboot - Boots a locally compiled operating system on a remote
@@ -1183,84 +1400,173 @@ B<./script> [option]...
 
 =head1 DESCRIPTION
 
-This program makes booting of a locally compiled operating system (OS)
+Novaboot makes booting of a locally compiled operating system (OS)
 (e.g. NOVA or Linux) on remote targets as simple as running a program
 locally. It automates things like copying OS images to a TFTP server,
 generation of bootloader configuration files, resetting of target
 hardware or redirection of target's serial line to stdin/out. Novaboot
-is highly configurable and it makes it easy to boot a single image on
+is highly configurable and makes it easy to boot a single image on
 different targets or different images on a single target.
 
-Novaboot operation is controlled by command line options and by a so
-called novaboot script, which can be thought as a generalization of
-bootloader configuration files (see L</"NOVABOOT SCRIPT SYNTAX">).
-Typical way of using novaboot is to make the novaboot script
-executable and set its first line to I<#!/usr/bin/env novaboot>. Then,
-booting a particular OS configuration becomes the same as executing a
-local program - the novaboot script.
+Novaboot operation is controlled by configuration files, command line
+options and by a so-called novaboot script, which can be thought as a
+generalization of bootloader configuration files (see L</"NOVABOOT
+SCRIPT SYNTAX">). The typical way of using novaboot is to make the
+novaboot script executable and set its first line to I<#!/usr/bin/env
+novaboot>. Then, booting a particular OS configuration becomes the
+same as executing a local program â€“ the novaboot script.
 
 Novaboot uses configuration files to, among other things, define
 command line options needed for different targets. Users typically use
 only the B<-t>/B<--target> command line option to select the target.
 Internally, this option expands to the pre-configured options.
-Configuration files are searched at multiple places, which allows to
-have per-system, per-user or per-project configurations. Configuration
-file syntax is described in section L</"CONFIGURATION FILE">.
+Novaboot searches configuration files at multiple places, which allows
+having per-system, per-user or per-project configurations.
+Configuration file syntax is described in section L</"CONFIGURATION
+FILES">.
+
+Novaboot newcomers may be confused by a large number of configuration
+options. Understanding all these options is not always needed,
+depending on the used setup. The L<figure from the doc directory
+|https://github.com/wentasah/novaboot/blob/master/doc/typical-setups.svg>
+shows different setups that vary in how much effort is needed
+to configure novaboot for them. The setups are:
+
+=over 3
+
+=item A: Laptop and target device only
+
+This requires to configure everything on the laptop side, including a
+serial line connection (L</--serial>, L</--remote-cmd>, ...), power
+on/off/reset commands (L</--reset-cmd>, ...), TFTP server
+(L</--copy>, L</--prefix>...), device IP addresses, etc.
+
+=item B: Laptop, target device and external TFTP server
+
+Like the previous setup, but the TFTP (and maybe DHCP) configuration
+is handled by a server. Novaboot users need to understand where to
+copy their files to the TFTP server (L</--copy>) and which IP
+addresses their target will get, but do not need to configure the
+servers themselves.
+
+=item C: Novaboot server running novaboot-shell
+
+With this setup, the configuration is done on the server. Users only
+need to know the SSH account (L</--ssh>) used to communicate between
+novaboot and novaboot server. The server is implemented as a
+restricted shell (L<novaboot-shell(1)>) on the server. No need to give
+full shell access to novaboot users on the server.
 
-Simple examples of using C<novaboot>:
+=back
+
+=head2 Simple examples of using C<novaboot>:
+
+To boot Linux (files F<bzImage> and F<rootfs.cpio> in current
+directory), create F<mylinux> file with this content:
+
+    #!/usr/bin/env novaboot
+    load bzImage console=ttyS0,115200
+    load rootfs.cpio
 
 =over 3
 
 =item 1.
 
-Run an OS in Qemu. This is the default action when no other action is
-specified by command line switches. Thus running C<novaboot myos> (or
-C<./myos> as described above) will run Qemu and make it boot the
-configuration specified in the F<myos> script.
+Booting an OS in Qemu can be accomplished by giving the B<--qemu> option.
+Thus running
+
+ novaboot --qemu mylinux
+
+(or C<./mylinux --qemu> as described above) will run Qemu and make it
+boot the configuration specified in the F<mylinux> script. How is qemu
+started can be configured in various ways (see below).
 
 =item 2.
 
 Create a bootloader configuration file (currently supported
-bootloaders are GRUB, GRUB2, ISOLINUX, Pulsar and U-Boot) and copy it
-with all other files needed for booting to a remote boot server. Then
+bootloaders are GRUB, GRUB2, ISOLINUX, Pulsar, and U-Boot) and copy it
+with all other files needed for booting to a remote TFTP server. Then
 use a TCP/IP-controlled relay/serial-to-TCP converter to reset the
 target and receive its serial output.
 
- ./myos --grub2 --server=192.168.1.1:/tftp --iprelay=192.168.1.2
+ ./mylinux --grub2 --copy=192.168.1.1:/tftp --iprelay=192.168.1.2
+
+Alternatively, you can put these switches to the configuration file
+and run:
+
+ ./mylinux --target mytarget
 
 =item 3.
 
+Specifying all the options needed by novaboot to successfully control
+the target, either on command line or in configuration files, can be
+difficult for users. Novaboot supports configuring the target
+centrally via L<novaboot-shell(1)> on a server. With such a
+configuration, users only need to use the B<--ssh> option to specify
+where to boot their OS:
+
+ ./mylinux --ssh myboard@example.com
+
+Typically, the server is the computer connected to and controlling the
+target board and running the TFTP server.
+
+=item 4.
+
 Run DHCP and TFTP server on developer's machine to boot the target
 from it.
 
- ./myos --dhcp-tftp
+ ./mylinux --dhcp-tftp
 
-This is useful when no network infrastructure is in place and
+This usage is useful when no network infrastructure is in place, and
 the target is connected directly to developer's box.
 
-=item 4.
+=item 5.
 
 Create bootable ISO image.
 
  novaboot --iso -- script1 script2
 
-The created ISO image will have ISOLINUX bootloader installed on it
+The created ISO image will have ISOLINUX bootloader installed on it,
 and the boot menu will allow selecting between I<script1> and
 I<script2> configurations.
 
 =back
 
-=head1 PHASES AND OPTIONS
+=head1 OPTIONS AND PHASES
+
+Novaboot performs its work in several phases. Command line options
+described bellow influence the execution of each phase or allow their
+skipping. The list of phases (in the execution order) is as follows.
+
+=over
+
+=item 1. L<Configuration reading|/Configuration reading phase>
+
+=item 2. L<Command line processing|/Command line processing phase>
+
+=item 3. L<Script preprocessing|/Script preprocessing phase>
+
+=item 4. L<File generation|/File generation phase>
 
-Novaboot performs its work in several phases. Each phase can be
-influenced by several command line options, certain phases can be
-skipped. The list of phases (in the execution order) and the
-corresponding options follows.
+=item 5. L<Target connection|/Target connection check>
+
+=item 6. L<File deployment|/File deployment phase>
+
+=item 7. L<Target power-on and reset|/Target power-on and reset phase>
+
+=item 8. L<Interaction with the bootloader|/Interaction with the bootloader on the target>
+
+=item 9. L<Target interaction|/Target interaction phase>
+
+=back
+
+Each phase is described in the following sections together with the
+command line options that control it.
 
 =head2 Configuration reading phase
 
-After starting, novaboot reads configuration files. Their content is
-described in section L</"CONFIGURATION FILE">. By default,
+After starting, novaboot reads zero or more configuration files. We
+describe their content in section L</"CONFIGURATION FILES">. By default, the
 configuration is read from multiple locations. First from the system
 configuration directory (F</etc/novaboot.d/>), second from the user
 configuration file (F<~/.config/novaboot>) and third from F<.novaboot>
@@ -1276,24 +1582,24 @@ solely of English letters, numbers, dashes '-' and underscores '_'
 (note that dot '.' is not included) are read in alphabetical order.
 
 Then, the user configuration file is read from
-F<$XDG_CONFIG_HOME/novaboot>. If C<$XDG_CONFIG_HOME> environemnt
+F<$XDG_CONFIG_HOME/novaboot>. If C<$XDG_CONFIG_HOME> environment
 variable is not set F<~/.config/novaboot> is read instead.
 
 Finally, novaboot searches for files named F<.novaboot> starting from the
 directory of the novaboot script (or working directory, see bellow)
 and continuing upwards up to the root directory. The found
 configuration files are then read in the opposite order (i.e. from the
-root directory downwards). This allows to have, for example, a project
+root directory downwards). This ordering allows having, for example, a project
 specific configuration in F<~/project/.novaboot>.
 
 Note the difference between F<~/.config/novaboot> and F<~/.novaboot>.
-The former one is read always, whereas the latter only when novaboot
+The former one is always read, whereas the latter only when novaboot
 script or working directory is under the C<$HOME> directory.
 
 In certain cases, the location of the novaboot script cannot be
-determined in this early phase. This happens either when the script is
+determined in this early phase. This situation happens either when the script is
 read from the standard input or when novaboot is invoked explicitly as
-in the example L</"4."> above. In this case the current working
+in the example L</"4."> above. In this case, the current working
 directory is used as a starting point for configuration file search
 instead of the novaboot script directory.
 
@@ -1321,30 +1627,50 @@ Print short (B<-h>) or long (B<--help>) help.
 =item -t, --target=I<target>
 
 This option serves as a user configurable shortcut for other novaboot
-options. The effect of this option is the same as the options stored
-in the C<%targets> configuration variable under key I<target>. See
-also L</"CONFIGURATION FILE">.
+options. The effect of this option is the same as specifying the
+options stored in the C<%targets> configuration variable under key
+I<target>. See also L</"CONFIGURATION FILES">.
+
+When this option is not given, novaboot tries to determine the target
+to use from either B<NOVABOOT_TARGET> environment variable or
+B<$default_target> configuration file variable.
+
+=item --ssh=I<user@hostname>
+
+Configures novaboot to control the target via C<novaboot-shell>
+running remotely via SSH.
+
+Using this option is the same as specifying B<--remote-cmd>,
+B<--remote-expect>, B<--copy> B<--rsync-flags>, B<--prefix> and
+B<--reset-cmd> manually in a way compatible with C<novaboot-shell>.
+The server can be configured to provide other, safe bootloader-related
+options, to the client. When this happens, novaboot prints them to
+stdout.
+
+Currently, this in an initial experimental implementation. We plan to
+change/extend this feature soon!
 
 =back
 
 =head2 Script preprocessing phase
 
-This phases allows to modify the parsed novaboot script before it is
+This phase allows modifying the parsed novaboot script before it is
 used in the later phases.
 
 =over 8
 
 =item -a, --append=I<parameters>
 
-Append a string to the first C<load> line in the novaboot script. This
+Append a string to the first C<load> line in the novaboot script. This option
 can be used to append parameters to the kernel's or root task's
 command line. This option can appear multiple times.
 
 =item -b, --bender
 
-Use F<bender> chainloader. Bender scans the PCI bus for PCI serial
-ports and stores the information about them in the BIOS data area for
-use by the kernel.
+Use L<Bender|https://github.com/TUD-OS/morbo/blob/master/standalone/bender.c>
+chainloader. Bender scans the PCI bus for PCI serial ports and stores
+the information about them in the BIOS data area for use by the
+kernel.
 
 =item --chainloader=I<chainloader>
 
@@ -1354,7 +1680,7 @@ promisc'.
 
 =item --dump
 
-Print the modules to boot and their parameters after this phase
+Print the modules to boot and their parameters, after this phase
 finishes. Then exit. This is useful for seeing the effect of other
 options in this section.
 
@@ -1363,9 +1689,9 @@ options in this section.
 Replace the first word on the first C<load> line in the novaboot
 script with F<file>.
 
-=item --scriptmod=I<perl expression>
+=item --scriptmod=I<Perl expression>
 
-When novaboot script is read, I<perl expression> is executed for every
+When novaboot reads the script, I<Perl expression> is executed for every
 line (in $_ variable). For example, C<novaboot
 --scriptmod=s/sigma0/omega6/g> replaces every occurrence of I<sigma0>
 in the script with I<omega6>.
@@ -1379,11 +1705,11 @@ line order.
 
 =head2 File generation phase
 
-In this phase, files needed for booting are generated in a so called
+In this phase, files needed for booting are generated in a so-called
 I<build directory> (see L</--build-dir>). In most cases configuration
 for a bootloader is generated automatically by novaboot. It is also
 possible to generate other files using I<heredoc> or I<"<"> syntax in
-novaboot scripts. Finally, binaries can be generated in this phases by
+novaboot scripts. Finally, novaboot can generate binaries in this phases by
 running C<scons> or C<make>.
 
 =over 8
@@ -1407,14 +1733,14 @@ build directory (see B<--build-dir>).
 
 =item --grub-preamble=I<prefix>
 
-Specifies the I<preable> that is at the beginning of the generated
+Specifies the I<preamble> that is at the beginning of the generated
 GRUB or GRUB2 config files. This is useful for specifying GRUB's
 timeout.
 
 =item --prefix=I<prefix>
 
 Specifies I<prefix> (e.g. F</srv/tftp>) that is put in front of every
-file name in generated bootloader configuration files (or in U-Boot
+filename in generated bootloader configuration files (or in U-Boot
 commands).
 
 If the I<prefix> contains string $NAME, it will be replaced with the
@@ -1430,11 +1756,15 @@ Alias for B<--prefix>.
 =item --grub2[=I<filename>]
 
 Generate GRUB2 menu entry in I<filename>. If I<filename> is not
-specified F<grub.cfg> is used. The content of the menu entry can be
+specified F<./boot/grub/grub.cfg> is used. The content of the menu entry can be
 customized with B<--grub-preamble>, B<--grub2-prolog> or
 B<--grub_prefix> options.
 
-In order to use the the generated menu entry on your development
+GRUB2 can boot multiboot-compliant kernels and a few kernels with specific
+support. L</BOOT_METHOD> could be used to specify the command used by GRUB2 to
+load the kernel. See L<GNU GRUB Manual|https://www.gnu.org/software/grub/manual/grub/grub.html#Booting>.
+
+To use the generated menu entry on your development
 machine that uses GRUB2, append the following snippet to
 F</etc/grub.d/40_custom> file and regenerate your grub configuration,
 i.e. run update-grub on Debian/Ubuntu.
@@ -1445,7 +1775,7 @@ i.e. run update-grub on Debian/Ubuntu.
 
 =item --grub2-prolog=I<prolog>
 
-Specifies text that is put at the beginning of the GRUB2 menu entry.
+Specifies the text that novaboot puts at the beginning of the GRUB2 menu entry.
 
 =item -m, --make[=make command]
 
@@ -1460,7 +1790,7 @@ server directory where the boot files are copied to.
 =item --no-file-gen
 
 Do not run external commands to generate files (i.e. "<" syntax and
-C<run> keyword). This switch does not influence generation of files
+C<run> keyword). This switch does not influence the generation of files
 specified with "<<WORD" syntax.
 
 =item -p, --pulsar[=mac]
@@ -1487,9 +1817,9 @@ Exit novaboot after file generation phase.
 
 =head2 Target connection check
 
-If supported by the target, the connection to it is made and it is
-checked whether the target is not occupied by another novaboot
-user/instance.
+In this phase novaboot connects to target's serial port (if it has
+one). If another novaboot user/instance occupies the target, novaboot
+exits here with an error message.
 
 =over 8
 
@@ -1497,19 +1827,24 @@ user/instance.
 
 Use Intel AMT technology to control the target machine. WS management
 is used to powercycle it and Serial-Over-Lan (SOL) for input/output.
-The hostname or (IP address) is given by the I<host> parameter. If
+The hostname or (IP address) is given by the I<host> parameter. If the
 I<password> is not specified, environment variable AMT_PASSWORD is
 used. The I<port> specifies a TCP port for SOL. If not specified, the
-default is 16992. Default I<user> is admin.
+default is 16992. The default I<user> is admin.
 
 =item --iprelay=I<addr[:port]>
 
 Use TCP/IP relay and serial port to access the target's serial port
-and powercycle it. The IP address of the relay is given by I<addr>
-parameter. If I<port> is not specified, it default to 23.
+and powercycle it. The I<addr> parameter specifies the IP address of
+the relay. If I<port> is not specified, it defaults to 23.
 
 Note: This option is supposed to work with HWG-ER02a IP relays.
 
+=item --iprelay-cmd=I<command>
+
+Similar to B<--iprelay> but uses I<command> to talk to the iprelay
+rather than direct network connection.
+
 =item -s, --serial[=device]
 
 Target's serial line is connected to host's serial line (device). The
@@ -1531,8 +1866,23 @@ example C<ssh server 'cu -l /dev/ttyS0'>.
 
 =item --remote-expect=I<string>
 
-Wait for reception of I<string> after establishing the remote
-connection.
+Wait for reception of I<string> after establishing the remote serial
+line connection. Novaboot assumes that after establishing the serial
+line connection, the user running novaboot has exclusive access to the
+target. If establishing of the serial line connection happens
+asynchronously (e.g. running a command remotely via SSH), we need this
+option to wait until the exclusive access is confirmed by the remote
+side.
+
+Depending on target configuration, this option can solve two practical
+problems: 1) Overwriting of files deployed by another user currently
+using the target. 2) Resetting the target board before serial line
+connection is established and thus missing bootloader interaction.
+
+Example of usage with the L<sterm
+tool|https://rtime.felk.cvut.cz/gitweb/sojka/sterm.git>:
+
+  --remote-cmd='ssh -tt example.com sterm -v /dev/ttyUSB0' --remote-expect='sterm: Connected'
 
 =item --remote-expect-silent=I<string>
 
@@ -1541,6 +1891,12 @@ echoed to stdout while waiting for the I<string>. Everything after the
 matched string is printed to stdout, so you may want to include line
 end characters in the I<string> as well.
 
+=item --remote-expect-timeout=I<seconds>
+
+Timeout in seconds for B<--remote-expect> or
+B<--remote-expect-seconds>. When negative, waits forever. The default
+is -1 seconds.
+
 =back
 
 =head2 File deployment phase
@@ -1557,10 +1913,10 @@ Turns your workstation into a DHCP and TFTP server so that the OS can
 be booted via PXE BIOS (or similar mechanism) on the test machine
 directly connected by a plain Ethernet cable to your workstation.
 
-The DHCP and TFTP servers requires root privileges and C<novaboot>
+The DHCP and TFTP servers require root privileges and C<novaboot>
 uses C<sudo> command to obtain those. You can put the following to
 I</etc/sudoers> to allow running the necessary commands without asking
-for password.
+for password.
 
  Cmnd_Alias NOVABOOT = /bin/ip a add 10.23.23.1/24 dev eth0, /bin/ip l set dev eth0 up, /usr/sbin/dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid, /usr/sbin/in.tftpd --listen --secure -v -v -v --pidfile tftpd.pid *, /usr/bin/touch dhcpd.leases, /usr/bin/pkill --pidfile=dhcpd.pid, /usr/bin/pkill --pidfile=tftpd.pid
  your_login ALL=NOPASSWD: NOVABOOT
@@ -1570,9 +1926,9 @@ for password.
 Starts a TFTP server on your workstation. This is similar to
 B<--dhcp-tftp> except that DHCP server is not started.
 
-The TFTP server require root privileges and C<novaboot> uses C<sudo>
+The TFTP server requires root privileges and C<novaboot> uses C<sudo>
 command to obtain those. You can put the following to I</etc/sudoers>
-to allow running the necessary commands without asking for password.
+to allow running the necessary commands without asking for password.
 
  Cmnd_Alias NOVABOOT =  /usr/sbin/in.tftpd --listen --secure -v -v -v --pidfile tftpd.pid *, /usr/bin/pkill --pidfile=tftpd.pid
  your_login ALL=NOPASSWD: NOVABOOT
@@ -1581,6 +1937,14 @@ to allow running the necessary commands without asking for password.
 
 Port to run the TFTP server on. Implies B<--tftp>.
 
+=item --netif=I<network interface>
+
+Network interface used to deploy files to the target. This option
+influences the configuration of the DHCP server started by
+B<--dhcp-tftp> and the value that B<$NB_MYIP> get replaced with during
+U-Boot conversation. The default value is C<$netif> variable from
+configuration files, which defaults to I<eth0>.
+
 =item --iso[=filename]
 
 Generates the ISO image that boots NOVA system via GRUB. If no filename
@@ -1589,6 +1953,10 @@ of the novaboot script (see also B<--name>).
 
 =item --server[=[[user@]server:]path]
 
+Alias of B<--copy> (kept for backward compatibility).
+
+=item --copy[=[[user@]server:]path]
+
 Copy all files needed for booting to another location. The files will
 be copied (by B<rsync> tool) to the directory I<path>. If the I<path>
 contains string $NAME, it will be replaced with the name of the
@@ -1596,14 +1964,14 @@ novaboot script (see also B<--name>).
 
 =item --rsync-flags=I<flags>
 
-Specifies which I<flags> are appended to F<rsync> command line when
-copying files as a result of I<--server> option.
+Specifies I<flags> to append to F<rsync> command line when
+copying files as a result of I<--copy> option.
 
 =item --concat
 
-If B<--server> is used and its value ends with $NAME, then after
+If B<--copy> is used and its value ends with $NAME, then after
 copying the files, a new bootloader configuration file (e.g. menu.lst)
-is created at I<path-wo-name>, i.e. the path specified by B<--server>
+is created at I<path-wo-name>, i.e. the path specified by B<--copy>
 with $NAME part removed. The content of the file is created by
 concatenating all files of the same name from all subdirectories of
 I<path-wo-name> found on the "server".
@@ -1620,18 +1988,18 @@ obtained from https://github.com/wentasah/amtterm.
 
 =head2 Target power-on and reset phase
 
-At this point, the target is reset (or switched on/off). There is
+At this point, the target is reset (or switched on/off). There are
 several ways how this can be accomplished. Resetting a physical target
 can currently be accomplished by the following options: B<--amt>,
-B<--iprelay>, B<--reset-cmd>.
+B<--iprelay>, B<--reset-cmd> and B<--reset-send>.
 
 =over 8
 
 =item --on, --off
 
 Switch on/off the target machine and exit. The script (if any) is
-completely ignored. Currently it works only with B<--iprelay> or
-B<--amt>.
+completely ignored. Currently, it works only with the following
+options: B<--iprelay>, B<--amt>, B<--ssh>.
 
 =item -Q, --qemu[=I<qemu-binary>]
 
@@ -1650,7 +2018,12 @@ Replace the default qemu flags (QEMU_FLAGS variable or C<-cpu coreduo
 
 =item --reset-cmd=I<cmd>
 
-Command that resets the target.
+Runs command I<cmd> to reset the target.
+
+=item --reset-send=I<string>
+
+Reset the target by sending the given I<string> to the remote serial
+line. "\n" sequences are replaced with the newline character.
 
 =item --no-reset, --reset
 
@@ -1667,13 +2040,17 @@ Disable/enable resetting of the target.
 Interact with U-Boot bootloader to boot the thing described in the
 novaboot script. I<prompt> specifies the U-Boot's prompt (default is
 "=> ", other common prompts are "U-Boot> " or "U-Boot# ").
-Implementation of this option is currently tied to a particular board
-that we use. It may be subject to changes in the future!
 
 =item --no-uboot
 
 Disable U-Boot interaction previously enabled with B<--uboot>.
 
+=item --uboot-stop-key=I<key>
+
+Character, which is sent as a response to U-Boot's "Hit any key to
+stop autoboot" message. The default value is newline, but some devices
+(e.g. TP-Link TD-W8970) require a specific key to be pressed.
+
 =item --uboot-init
 
 Command(s) to send the U-Boot bootloader before loading the images and
@@ -1681,8 +2058,8 @@ booting them. This option can be given multiple times. After sending
 commands from each option novaboot waits for U-Boot I<prompt>.
 
 If the command contains string I<$NB_MYIP> then this string is
-replaced by IPv4 address of eth0 interface. Similarly I<$NB_PREFIX> is
-replaced with prefix given by B<--prefix>.
+replaced by IPv4 address of eth0 interface (see also B<--netif>).
+Similarly, I<$NB_PREFIX> is replaced with prefix given by B<--prefix>.
 
 See also C<uboot> keyword in L</"NOVABOOT SCRIPT SYNTAX">).
 
@@ -1719,12 +2096,12 @@ interactive work with the target.
 
 =item --exiton=I<string>
 
-When I<string> is sent by the target, novaboot exits. This option can
+When the I<string> is sent by the target, novaboot exits. This option can
 be specified multiple times, in which case novaboot exits whenever
 either of the specified strings is sent.
 
-If I<string> is C<-re>, then the next B<--exiton>'s I<string> is
-treated as regular expression. For example:
+If the I<string> is C<-re>, then the next B<--exiton>'s I<string> is
+treated as regular expression. For example:
 
     --exiton -re --exiton 'error:.*failed'
 
@@ -1735,26 +2112,26 @@ The same as --exiton -re --exiton I<regex>.
 =item --exiton-timeout=I<seconds>
 
 By default B<--exiton> waits for the string match forever. When this
-option is specified, "exiton" timeouts after the specifies number of
+option is specified, "exiton" timeouts after the specified number of
 seconds and novaboot returns non-zero exit code.
 
 =item -i, --interactive
 
-Setup things for interactive use of target. Your terminal will be
-switched to raw mode. In raw mode, your system does not process input
-in any way (no echoing of entered characters, no interpretation
-special characters). This, among others, means that Ctrl-C is passed
-to the target and does no longer interrupt novaboot. Use "~~."
-sequence to exit novaboot.
+Setup things for the interactive use of the target. Your terminal will
+be switched to raw mode. In raw mode, your local terminal does not
+process input in any way (no echoing of entered characters, no
+interpretation of special characters). This, among others, means that
+Ctrl-C is passed to the target and does not interrupt novaboot. To
+exit from novaboot interactive mode type "~~.".
 
 =item --no-interaction, --interaction
 
 Skip resp. force target interaction phase. When skipped, novaboot exits
-immediately when boot is initiated.
+immediately after the boot is initiated.
 
 =item --expect=I<string>
 
-When I<string> is received from the target, send the string specified
+When the I<string> is received from the target, send the string specified
 with the subsequent B<--send*> option to the target.
 
 =item --expect-re=I<regex>
@@ -1784,12 +2161,20 @@ Similar to B<--send> but continue expecting more input.
 
 Example: C<--expect='Continue?' --sendcont='yes\n'>
 
+=item --final-eol, --no-final-eol
+
+By default, B<novaboot> always prints an end-of-line character at the
+end of its execution in order to ensure that the output of programs
+started after novaboot appears at the beginning of the line. When this
+is not desired B<--no-final-eol> option can be used to override this
+behavior.
+
 =back
 
 =head1 NOVABOOT SCRIPT SYNTAX
 
 The syntax tries to mimic POSIX shell syntax. The syntax is defined
-with the following rules.
+by the following rules.
 
 Lines starting with "#" and empty lines are ignored.
 
@@ -1800,14 +2185,28 @@ Lines of the form I<VARIABLE=...> (i.e. matching '^[A-Z_]+=' regular
 expression) assign values to internal variables. See L</VARIABLES>
 section.
 
-Lines starting with C<load> keyword represent modules to boot. The
+Otherwise, the first word on the line defines the meaning of the line.
+The following keywords are supported:
+
+=over 4
+
+=item C<load>
+
+These lines represent modules to boot. The
 word after C<load> is a file name (relative to the build directory
 (see B<--build-dir>) of the module to load and the remaining words are
 passed to it as the command line parameters.
 
+When booting Linux, the first C<load> line usually refers to the
+kernel image and its command line parameters (unless you use some
+special pre-loader). Other C<load> lines may refer to an initramfs
+image and/or a device tree blob. Their order is not important, as the
+device tree is recognized as the file name ending with C<.dtb>.
+
 When the C<load> line ends with "<<WORD" then the subsequent lines
 until the line containing solely WORD are copied literally to the file
-named on that line. This is similar to shell's heredoc feature.
+named on that line. This is similar to the heredoc feature of UNIX
+shells.
 
 When the C<load> line ends with "< CMD" then command CMD is executed
 with F</bin/sh> and its standard output is stored in the file named on
@@ -1815,20 +2214,59 @@ that line. The SRCDIR variable in CMD's environment is set to the
 absolute path of the directory containing the interpreted novaboot
 script.
 
+=item C<copy>
+
+These lines are similar to C<load> lines. The
+file mentioned there is copied to the same place as in the case of C<load>
+(e.g. tftp server), but the file is not used in the bootloader
+configuration. Such a file can be used by the target for other
+purposes than booting, e.g. at OS runtime or for firmware update.
+
+=item C<chld>
+
+Chainload another bootloader. Instead of loading multiboot modules
+identified with C<load> keyword, run another bootloader. This is
+currently supported only by pulsar and can be used to load e.g. Grub
+as in the example below:
+
+ chld boot/grub/i386-pc/core.0
+
+
+=item C<run>
+
 Lines starting with C<run> keyword contain shell commands that are run
 during file generation phase. This is the same as the "< CMD" syntax
 for C<load> keyboard except that the command's output is not
 redirected to a file. The ordering of commands is the same as they
 appear in the novaboot script.
 
-Lines starting with C<uboot> represent U-Boot commands that are sent
-to the target if B<--uboot> option is given. Having a U-Boot line in
-the novaboot script is the same as passing an equivalent
-B<--uboot-init> option to novaboot. The C<uboot> keyword can be
-suffixed with timeout specification. The syntax is C<uboot:Ns>, where
-C<N> is the whole number of seconds. If the U-Boot command prompt does
-not appear before the timeout, novaboot fails. The default timeout is
-10 seconds.
+=item C<uboot>
+
+These lines represent U-Boot commands that are sent to the target if
+B<--uboot> option is given. Having a U-Boot line in the novaboot
+script is the same as giving B<--uboot-init> option to novaboot. The
+following syntax variants are supported:
+
+
+  uboot[:<timeout>] <string> [> <file>]
+  uboot[:<timeout>] < <shell> [> <file>]
+
+C<string> is the literal U-Boot command.
+
+The C<uboot> keyword can be suffixed with timeout specification. The
+syntax is C<uboot:Ns>, where C<N> is the whole number of seconds. If
+the U-Boot command prompt does not appear before the timeout, novaboot
+fails. The default timeout is 10 seconds.
+
+In the second variant with the C<<> character the shell code is
+executed and its standard output is sent to U-Boot. Example:
+
+  uboot < printf "mmc write \$loadaddr 1 %x" $(($(/usr/bin/stat -c%s rootfs.ext4) / 512))
+
+When C<E<gt> file> part is present, the output of the U-Boot command
+is written into the given file.
+
+=back
 
 Example (Linux):
 
@@ -1854,12 +2292,39 @@ F<hello.nulconfig>. sigma0 receives some command line parameters and
 F<hello.nulconfig> file is generated on the fly from the lines between
 C<<<EOF> and C<EOF>.
 
+Example (Zynq system update via U-Boot):
+
+  #!/usr/bin/env novaboot
+
+  uboot dhcp
+
+  # Write kernel to FAT filesystem on the 1st SD card partition
+  run mkimage -f uboot-image.its image.ub
+  copy image.ub
+  uboot:60s tftpboot ${loadaddr} $NB_PREFIX/image.ub
+  uboot fatwrite mmc 0:1 ${loadaddr} image.ub $filesize
+  uboot set bootargs console=ttyPS0,115200 root=/dev/mmcblk0p2
+
+  # Write root FS image to the 2nd SD card partition
+  copy rootfs/images/rootfs.ext4
+  uboot:60s tftpboot ${loadaddr} $NB_PREFIX/rootfs/images/rootfs.ext4
+  uboot mmc part > mmc-part.txt
+  uboot < printf "mmc write \$loadaddr %x %x" $(awk '{ if ($1 == "2") { print $2 }}' mmc-part.txt) $(($(/usr/bin/stat -L --printf=%s rootfs/images/rootfs.ext4) / 512))
+
+  UBOOT_CMD=boot
+
+
 =head2 VARIABLES
 
 The following variables are interpreted in the novaboot script:
 
 =over 8
 
+=item BOOT_METHOD
+
+Specifies the way GRUB2 boots the kernel. For kernels with multiboot
+support use C<multiboot> method (the default). For Linux kernel use C<linux> method.
+
 =item BUILDDIR
 
 Novaboot chdir()s to this directory before file generation phase. The
@@ -1871,10 +2336,16 @@ specified by other means (see L</--build-dir>).
 Assigning this variable has the same effect as specifying L</--exiton>
 option.
 
+=item INTERACTION
+
+Setting this variable to zero is the same as giving
+L</--no-interaction>, specifying to one corresponds to
+L</--interaction>.
+
 =item HYPERVISOR_PARAMS
 
-Parameters passed to hypervisor. The default value is "serial", unless
-overridden in configuration file.
+Parameters passed to the hypervisor. The default value is "serial", unless
+overridden in the configuration file.
 
 =item KERNEL
 
@@ -1919,7 +2390,7 @@ intermediate output.
 
 =back
 
-=head1 CONFIGURATION FILE
+=head1 CONFIGURATION FILES
 
 Novaboot can read its configuration from one or more files. By
 default, novaboot looks for files in F</etc/novaboot.d>, file
@@ -1945,7 +2416,13 @@ file.
 =item $default_target
 
 Default target (see below) to use when no target is explicitly
-specified on command line with the B<--target> option.
+specified with the B<--target> command line option or
+B<NOVABOOT_TARGET> environment variable.
+
+=item $netif
+
+Default value for the B<--netif> option. If not specified, it defaults
+to I<eth0>.
 
 =item %targets
 
@@ -1953,11 +2430,11 @@ Hash of target definitions to be used with the B<--target> option. The
 key is the identifier of the target, the value is the string with
 command line options. For instance, if the configuration file contains:
 
- $targets{'mybox'} = '--server=boot:/tftproot --serial=/dev/ttyUSB0 --grub',
+ $targets{'mybox'} = '--copy=boot:/tftproot --serial=/dev/ttyUSB0 --grub',
 
 then the following two commands are equivalent:
 
- ./myos --server=boot:/tftproot --serial=/dev/ttyUSB0 --grub
+ ./myos --copy=boot:/tftproot --serial=/dev/ttyUSB0 --grub
  ./myos -t mybox
 
 =back
@@ -1966,7 +2443,7 @@ then the following two commands are equivalent:
 
 Some options can be specified not only via config file or command line
 but also through environment variables. Environment variables override
-the values from configuration file and command line parameters
+the values from the configuration file and command line parameters
 override the environment variables.
 
 =over 8
@@ -1981,9 +2458,16 @@ one(s).
 Name of the novaboot configuration directory. When not specified
 F</etc/novaboot.d> is used.
 
+=item NOVABOOT_TARGET
+
+Name of the novaboot target to use. This overrides the value of
+B<$default_target> from the configuration file and can be overridden
+with the B<--target> command line option.
+
 =item NOVABOOT_BENDER
 
-Defining this variable has the same meaning as B<--bender> option.
+Defining this variable has the same effect as using B<--bender>
+option.
 
 =back
 
@@ -1991,6 +2475,9 @@ Defining this variable has the same meaning as B<--bender> option.
 
 Michal Sojka <sojka@os.inf.tu-dresden.de>
 
+Latest novaboot version can be found at
+L<https://github.com/wentasah/novaboot>.
+
 =cut
 
 # LocalWords:  novaboot Novaboot NOVABOOT TFTP PXE DHCP filename stty