at 16.09-beta 624 lines 16 kB view raw
1package Machine; 2 3use strict; 4use threads; 5use Socket; 6use IO::Handle; 7use POSIX qw(dup2); 8use FileHandle; 9use Cwd; 10use File::Basename; 11use File::Path qw(make_path); 12use File::Slurp; 13 14 15my $showGraphics = defined $ENV{'DISPLAY'}; 16 17my $sharedDir; 18 19 20sub new { 21 my ($class, $args) = @_; 22 23 my $startCommand = $args->{startCommand}; 24 25 my $name = $args->{name}; 26 if (!$name) { 27 $startCommand =~ /run-(.*)-vm$/ if defined $startCommand; 28 $name = $1 || "machine"; 29 } 30 31 if (!$startCommand) { 32 # !!! merge with qemu-vm.nix. 33 $startCommand = 34 "qemu-kvm -m 384 " . 35 "-net nic,model=virtio \$QEMU_OPTS "; 36 my $iface = $args->{hdaInterface} || "virtio"; 37 $startCommand .= "-drive file=" . Cwd::abs_path($args->{hda}) . ",if=$iface,werror=report " 38 if defined $args->{hda}; 39 $startCommand .= "-cdrom $args->{cdrom} " 40 if defined $args->{cdrom}; 41 $startCommand .= "-device piix3-usb-uhci -drive id=usbdisk,file=$args->{usb},if=none,readonly -device usb-storage,drive=usbdisk " 42 if defined $args->{usb}; 43 $startCommand .= "-bios $args->{bios} " 44 if defined $args->{bios}; 45 $startCommand .= $args->{qemuFlags} || ""; 46 } 47 48 my $tmpDir = $ENV{'TMPDIR'} || "/tmp"; 49 unless (defined $sharedDir) { 50 $sharedDir = $tmpDir . "/xchg-shared"; 51 make_path($sharedDir, { mode => 0700, owner => $< }); 52 } 53 54 my $allowReboot = 0; 55 $allowReboot = $args->{allowReboot} if defined $args->{allowReboot}; 56 57 my $self = { 58 startCommand => $startCommand, 59 name => $name, 60 allowReboot => $allowReboot, 61 booted => 0, 62 pid => 0, 63 connected => 0, 64 socket => undef, 65 stateDir => "$tmpDir/vm-state-$name", 66 monitor => undef, 67 log => $args->{log}, 68 redirectSerial => $args->{redirectSerial} // 1, 69 }; 70 71 mkdir $self->{stateDir}, 0700; 72 73 bless $self, $class; 74 return $self; 75} 76 77 78sub log { 79 my ($self, $msg) = @_; 80 $self->{log}->log($msg, { machine => $self->{name} }); 81} 82 83 84sub nest { 85 my ($self, $msg, $coderef, $attrs) = @_; 86 $self->{log}->nest($msg, $coderef, { %{$attrs || {}}, machine => $self->{name} }); 87} 88 89 90sub name { 91 my ($self) = @_; 92 return $self->{name}; 93} 94 95 96sub stateDir { 97 my ($self) = @_; 98 return $self->{stateDir}; 99} 100 101 102sub start { 103 my ($self) = @_; 104 return if $self->{booted}; 105 106 $self->log("starting vm"); 107 108 # Create a socket pair for the serial line input/output of the VM. 109 my ($serialP, $serialC); 110 socketpair($serialP, $serialC, PF_UNIX, SOCK_STREAM, 0) or die; 111 112 # Create a Unix domain socket to which QEMU's monitor will connect. 113 my $monitorPath = $self->{stateDir} . "/monitor"; 114 unlink $monitorPath; 115 my $monitorS; 116 socket($monitorS, PF_UNIX, SOCK_STREAM, 0) or die; 117 bind($monitorS, sockaddr_un($monitorPath)) or die "cannot bind monitor socket: $!"; 118 listen($monitorS, 1) or die; 119 120 # Create a Unix domain socket to which the root shell in the guest will connect. 121 my $shellPath = $self->{stateDir} . "/shell"; 122 unlink $shellPath; 123 my $shellS; 124 socket($shellS, PF_UNIX, SOCK_STREAM, 0) or die; 125 bind($shellS, sockaddr_un($shellPath)) or die "cannot bind shell socket: $!"; 126 listen($shellS, 1) or die; 127 128 # Start the VM. 129 my $pid = fork(); 130 die if $pid == -1; 131 132 if ($pid == 0) { 133 close $serialP; 134 close $monitorS; 135 close $shellS; 136 if ($self->{redirectSerial}) { 137 open NUL, "</dev/null" or die; 138 dup2(fileno(NUL), fileno(STDIN)); 139 dup2(fileno($serialC), fileno(STDOUT)); 140 dup2(fileno($serialC), fileno(STDERR)); 141 } 142 $ENV{TMPDIR} = $self->{stateDir}; 143 $ENV{SHARED_DIR} = $sharedDir; 144 $ENV{USE_TMPDIR} = 1; 145 $ENV{QEMU_OPTS} = 146 ($self->{allowReboot} ? "" : "-no-reboot ") . 147 "-monitor unix:./monitor -chardev socket,id=shell,path=./shell " . 148 "-device virtio-serial -device virtconsole,chardev=shell " . 149 ($showGraphics ? "-serial stdio" : "-nographic") . " " . ($ENV{QEMU_OPTS} || ""); 150 chdir $self->{stateDir} or die; 151 exec $self->{startCommand}; 152 die "running VM script: $!"; 153 } 154 155 # Process serial line output. 156 close $serialC; 157 158 threads->create(\&processSerialOutput, $self, $serialP)->detach; 159 160 sub processSerialOutput { 161 my ($self, $serialP) = @_; 162 while (<$serialP>) { 163 chomp; 164 s/\r$//; 165 print STDERR $self->{name}, "# $_\n"; 166 $self->{log}->{logQueue}->enqueue({msg => $_, machine => $self->{name}}); # !!! 167 } 168 } 169 170 eval { 171 local $SIG{CHLD} = sub { die "QEMU died prematurely\n"; }; 172 173 # Wait until QEMU connects to the monitor. 174 accept($self->{monitor}, $monitorS) or die; 175 176 # Wait until QEMU connects to the root shell socket. QEMU 177 # does so immediately; this doesn't mean that the root shell 178 # has connected yet inside the guest. 179 accept($self->{socket}, $shellS) or die; 180 $self->{socket}->autoflush(1); 181 }; 182 die "$@" if $@; 183 184 $self->waitForMonitorPrompt; 185 186 $self->log("QEMU running (pid $pid)"); 187 188 $self->{pid} = $pid; 189 $self->{booted} = 1; 190} 191 192 193# Send a command to the monitor and wait for it to finish. TODO: QEMU 194# also has a JSON-based monitor interface now, but it doesn't support 195# all commands yet. We should use it once it does. 196sub sendMonitorCommand { 197 my ($self, $command) = @_; 198 $self->log("sending monitor command: $command"); 199 syswrite $self->{monitor}, "$command\n"; 200 return $self->waitForMonitorPrompt; 201} 202 203 204# Wait until the monitor sends "(qemu) ". 205sub waitForMonitorPrompt { 206 my ($self) = @_; 207 my $res = ""; 208 my $s; 209 while (sysread($self->{monitor}, $s, 1024)) { 210 $res .= $s; 211 last if $res =~ s/\(qemu\) $//; 212 } 213 return $res; 214} 215 216 217# Call the given code reference repeatedly, with 1 second intervals, 218# until it returns 1 or a timeout is reached. 219sub retry { 220 my ($coderef) = @_; 221 my $n; 222 for ($n = 0; $n < 900; $n++) { 223 return if &$coderef; 224 sleep 1; 225 } 226 die "action timed out after $n seconds"; 227} 228 229 230sub connect { 231 my ($self) = @_; 232 return if $self->{connected}; 233 234 $self->nest("waiting for the VM to finish booting", sub { 235 236 $self->start; 237 238 local $SIG{ALRM} = sub { die "timed out waiting for the VM to connect\n"; }; 239 alarm 300; 240 readline $self->{socket} or die "the VM quit before connecting\n"; 241 alarm 0; 242 243 $self->log("connected to guest root shell"); 244 $self->{connected} = 1; 245 246 }); 247} 248 249 250sub waitForShutdown { 251 my ($self) = @_; 252 return unless $self->{booted}; 253 254 $self->nest("waiting for the VM to power off", sub { 255 waitpid $self->{pid}, 0; 256 $self->{pid} = 0; 257 $self->{booted} = 0; 258 $self->{connected} = 0; 259 }); 260} 261 262 263sub isUp { 264 my ($self) = @_; 265 return $self->{booted} && $self->{connected}; 266} 267 268 269sub execute_ { 270 my ($self, $command) = @_; 271 272 $self->connect; 273 274 print { $self->{socket} } ("( $command ); echo '|!=EOF' \$?\n"); 275 276 my $out = ""; 277 278 while (1) { 279 my $line = readline($self->{socket}); 280 die "connection to VM lost unexpectedly" unless defined $line; 281 #$self->log("got line: $line"); 282 if ($line =~ /^(.*)\|\!\=EOF\s+(\d+)$/) { 283 $out .= $1; 284 $self->log("exit status $2"); 285 return ($2, $out); 286 } 287 $out .= $line; 288 } 289} 290 291 292sub execute { 293 my ($self, $command) = @_; 294 my @res; 295 $self->nest("running command: $command", sub { 296 @res = $self->execute_($command); 297 }); 298 return @res; 299} 300 301 302sub succeed { 303 my ($self, @commands) = @_; 304 305 my $res; 306 foreach my $command (@commands) { 307 $self->nest("must succeed: $command", sub { 308 my ($status, $out) = $self->execute_($command); 309 if ($status != 0) { 310 $self->log("output: $out"); 311 die "command `$command' did not succeed (exit code $status)\n"; 312 } 313 $res .= $out; 314 }); 315 } 316 317 return $res; 318} 319 320 321sub mustSucceed { 322 succeed @_; 323} 324 325 326sub waitUntilSucceeds { 327 my ($self, $command) = @_; 328 $self->nest("waiting for success: $command", sub { 329 retry sub { 330 my ($status, $out) = $self->execute($command); 331 return 1 if $status == 0; 332 }; 333 }); 334} 335 336 337sub waitUntilFails { 338 my ($self, $command) = @_; 339 $self->nest("waiting for failure: $command", sub { 340 retry sub { 341 my ($status, $out) = $self->execute($command); 342 return 1 if $status != 0; 343 }; 344 }); 345} 346 347 348sub fail { 349 my ($self, $command) = @_; 350 $self->nest("must fail: $command", sub { 351 my ($status, $out) = $self->execute_($command); 352 die "command `$command' unexpectedly succeeded" 353 if $status == 0; 354 }); 355} 356 357 358sub mustFail { 359 fail @_; 360} 361 362 363sub getUnitInfo { 364 my ($self, $unit) = @_; 365 my ($status, $lines) = $self->execute("systemctl --no-pager show '$unit'"); 366 return undef if $status != 0; 367 my $info = {}; 368 foreach my $line (split '\n', $lines) { 369 $line =~ /^([^=]+)=(.*)$/ or next; 370 $info->{$1} = $2; 371 } 372 return $info; 373} 374 375 376# Wait for a systemd unit to reach the "active" state. 377sub waitForUnit { 378 my ($self, $unit) = @_; 379 $self->nest("waiting for unit ‘$unit’", sub { 380 retry sub { 381 my $info = $self->getUnitInfo($unit); 382 my $state = $info->{ActiveState}; 383 die "unit ‘$unit’ reached state ‘$state’\n" if $state eq "failed"; 384 if ($state eq "inactive") { 385 # If there are no pending jobs, then assume this unit 386 # will never reach active state. 387 my ($status, $jobs) = $self->execute("systemctl list-jobs --full 2>&1"); 388 if ($jobs =~ /No jobs/) { # FIXME: fragile 389 # Handle the case where the unit may have started 390 # between the previous getUnitInfo() and 391 # list-jobs. 392 my $info2 = $self->getUnitInfo($unit); 393 die "unit ‘$unit’ is inactive and there are no pending jobs\n" 394 if $info2->{ActiveState} eq $state; 395 } 396 } 397 return 1 if $state eq "active"; 398 }; 399 }); 400} 401 402 403sub waitForJob { 404 my ($self, $jobName) = @_; 405 return $self->waitForUnit($jobName); 406} 407 408 409# Wait until the specified file exists. 410sub waitForFile { 411 my ($self, $fileName) = @_; 412 $self->nest("waiting for file ‘$fileName’", sub { 413 retry sub { 414 my ($status, $out) = $self->execute("test -e $fileName"); 415 return 1 if $status == 0; 416 } 417 }); 418} 419 420sub startJob { 421 my ($self, $jobName) = @_; 422 $self->execute("systemctl start $jobName"); 423 # FIXME: check result 424} 425 426sub stopJob { 427 my ($self, $jobName) = @_; 428 $self->execute("systemctl stop $jobName"); 429} 430 431 432# Wait until the machine is listening on the given TCP port. 433sub waitForOpenPort { 434 my ($self, $port) = @_; 435 $self->nest("waiting for TCP port $port", sub { 436 retry sub { 437 my ($status, $out) = $self->execute("nc -z localhost $port"); 438 return 1 if $status == 0; 439 } 440 }); 441} 442 443 444# Wait until the machine is not listening on the given TCP port. 445sub waitForClosedPort { 446 my ($self, $port) = @_; 447 retry sub { 448 my ($status, $out) = $self->execute("nc -z localhost $port"); 449 return 1 if $status != 0; 450 } 451} 452 453 454sub shutdown { 455 my ($self) = @_; 456 return unless $self->{booted}; 457 458 print { $self->{socket} } ("poweroff\n"); 459 460 $self->waitForShutdown; 461} 462 463 464sub crash { 465 my ($self) = @_; 466 return unless $self->{booted}; 467 468 $self->log("forced crash"); 469 470 $self->sendMonitorCommand("quit"); 471 472 $self->waitForShutdown; 473} 474 475 476# Make the machine unreachable by shutting down eth1 (the multicast 477# interface used to talk to the other VMs). We keep eth0 up so that 478# the test driver can continue to talk to the machine. 479sub block { 480 my ($self) = @_; 481 $self->sendMonitorCommand("set_link virtio-net-pci.1 off"); 482} 483 484 485# Make the machine reachable. 486sub unblock { 487 my ($self) = @_; 488 $self->sendMonitorCommand("set_link virtio-net-pci.1 on"); 489} 490 491 492# Take a screenshot of the X server on :0.0. 493sub screenshot { 494 my ($self, $filename) = @_; 495 my $dir = $ENV{'out'} || Cwd::abs_path("."); 496 $filename = "$dir/${filename}.png" if $filename =~ /^\w+$/; 497 my $tmp = "${filename}.ppm"; 498 my $name = basename($filename); 499 $self->nest("making screenshot ‘$name’", sub { 500 $self->sendMonitorCommand("screendump $tmp"); 501 system("pnmtopng $tmp > ${filename}") == 0 502 or die "cannot convert screenshot"; 503 unlink $tmp; 504 }, { image => $name } ); 505} 506 507 508# Take a screenshot and return the result as text using optical character 509# recognition. 510sub getScreenText { 511 my ($self) = @_; 512 513 system("command -v tesseract &> /dev/null") == 0 514 or die "getScreenText used but enableOCR is false"; 515 516 my $text; 517 $self->nest("performing optical character recognition", sub { 518 my $tmpbase = Cwd::abs_path(".")."/ocr"; 519 my $tmpin = $tmpbase."in.ppm"; 520 my $tmpout = "$tmpbase.ppm"; 521 522 $self->sendMonitorCommand("screendump $tmpin"); 523 system("ppmtopgm $tmpin | pamscale 4 -filter=lanczos > $tmpout") == 0 524 or die "cannot scale screenshot"; 525 unlink $tmpin; 526 system("tesseract $tmpout $tmpbase") == 0 or die "OCR failed"; 527 unlink $tmpout; 528 $text = read_file("$tmpbase.txt"); 529 unlink "$tmpbase.txt"; 530 }); 531 return $text; 532} 533 534 535# Wait until a specific regexp matches the textual contents of the screen. 536sub waitForText { 537 my ($self, $regexp) = @_; 538 $self->nest("waiting for $regexp to appear on the screen", sub { 539 retry sub { 540 return 1 if $self->getScreenText =~ /$regexp/; 541 } 542 }); 543} 544 545 546# Wait until it is possible to connect to the X server. Note that 547# testing the existence of /tmp/.X11-unix/X0 is insufficient. 548sub waitForX { 549 my ($self, $regexp) = @_; 550 $self->nest("waiting for the X11 server", sub { 551 retry sub { 552 my ($status, $out) = $self->execute("journalctl -b SYSLOG_IDENTIFIER=systemd | grep 'session opened'"); 553 return 0 if $status != 0; 554 ($status, $out) = $self->execute("[ -e /tmp/.X11-unix/X0 ]"); 555 return 1 if $status == 0; 556 } 557 }); 558} 559 560 561sub getWindowNames { 562 my ($self) = @_; 563 my $res = $self->mustSucceed( 564 q{xwininfo -root -tree | sed 's/.*0x[0-9a-f]* \"\([^\"]*\)\".*/\1/; t; d'}); 565 return split /\n/, $res; 566} 567 568 569sub waitForWindow { 570 my ($self, $regexp) = @_; 571 $self->nest("waiting for a window to appear", sub { 572 retry sub { 573 my @names = $self->getWindowNames; 574 foreach my $n (@names) { 575 return 1 if $n =~ /$regexp/; 576 } 577 } 578 }); 579} 580 581 582sub copyFileFromHost { 583 my ($self, $from, $to) = @_; 584 my $s = `cat $from` or die; 585 $self->mustSucceed("echo '$s' > $to"); # !!! escaping 586} 587 588 589sub sendKeys { 590 my ($self, @keys) = @_; 591 foreach my $key (@keys) { 592 $key = "spc" if $key eq " "; 593 $key = "ret" if $key eq "\n"; 594 $self->sendMonitorCommand("sendkey $key"); 595 } 596} 597 598 599sub sendChars { 600 my ($self, $chars) = @_; 601 $self->nest("sending keys ‘$chars’", sub { 602 $self->sendKeys(split //, $chars); 603 }); 604} 605 606 607# Sleep N seconds (in virtual guest time, not real time). 608sub sleep { 609 my ($self, $time) = @_; 610 $self->succeed("sleep $time"); 611} 612 613 614# Forward a TCP port on the host to a TCP port on the guest. Useful 615# during interactive testing. 616sub forwardPort { 617 my ($self, $hostPort, $guestPort) = @_; 618 $hostPort = 8080 unless defined $hostPort; 619 $guestPort = 80 unless defined $guestPort; 620 $self->sendMonitorCommand("hostfwd_add tcp::$hostPort-:$guestPort"); 621} 622 623 6241;