OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python |
| 2 # |
| 3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. |
| 4 # Use of this source code is governed by a BSD-style license that can be |
| 5 # found in the LICENSE file. |
| 6 |
| 7 """Create and copy update image to target host. |
| 8 |
| 9 auto-update and devserver change out from beneath us often enough |
| 10 that despite having to duplicate a litte code, it seems that the |
| 11 right thing to do here is to start over and do something that is |
| 12 simple enough and easy enough to understand so that when more |
| 13 stuff breaks, at least we can solve them faster. |
| 14 """ |
| 15 |
| 16 import BaseHTTPServer |
| 17 import cgi |
| 18 import errno |
| 19 import optparse |
| 20 import os |
| 21 import signal |
| 22 import subprocess |
| 23 import sys |
| 24 import tempfile |
| 25 import time |
| 26 import traceback |
| 27 |
| 28 from xml.dom import minidom |
| 29 |
| 30 |
| 31 # This is the default filename within the image directory to load updates from |
| 32 DEFAULT_IMAGE_NAME = 'chromiumos_image.bin' |
| 33 |
| 34 # The filenames we provide to clients to pull updates |
| 35 UPDATE_FILENAME = 'update.gz' |
| 36 STATEFUL_FILENAME = 'stateful.image.gz' |
| 37 |
| 38 # How long do we wait for the server to start before launching client |
| 39 SERVER_STARTUP_WAIT = 1 |
| 40 |
| 41 |
| 42 class Command(object): |
| 43 """Shell command ease-ups for Python.""" |
| 44 |
| 45 def __init__(self, env): |
| 46 self.env = env |
| 47 |
| 48 def RunPipe(self, pipeline, infile=None, outfile=None, |
| 49 capture=False, oneline=False): |
| 50 """Perform a command pipeline, with optional input/output filenames.""" |
| 51 |
| 52 last_pipe = None |
| 53 while pipeline: |
| 54 cmd = pipeline.pop(0) |
| 55 kwargs = {} |
| 56 if last_pipe is not None: |
| 57 kwargs['stdin'] = last_pipe.stdout |
| 58 elif infile: |
| 59 kwargs['stdin'] = open(infile, 'rb') |
| 60 if pipeline or capture: |
| 61 kwargs['stdout'] = subprocess.PIPE |
| 62 elif outfile: |
| 63 kwargs['stdout'] = open(outfile, 'wb') |
| 64 |
| 65 self.env.Info('Running: %s' % ' '.join(cmd)) |
| 66 last_pipe = subprocess.Popen(cmd, **kwargs) |
| 67 |
| 68 if capture: |
| 69 ret = last_pipe.communicate()[0] |
| 70 if not ret: |
| 71 return None |
| 72 elif oneline: |
| 73 return ret.rstrip('\r\n') |
| 74 else: |
| 75 return ret |
| 76 else: |
| 77 return os.waitpid(last_pipe.pid, 0)[1] == 0 |
| 78 |
| 79 def Output(self, *cmd): |
| 80 return self.RunPipe([cmd], capture=True) |
| 81 |
| 82 def OutputOneLine(self, *cmd): |
| 83 return self.RunPipe([cmd], capture=True, oneline=True) |
| 84 |
| 85 def Run(self, *cmd, **kwargs): |
| 86 return self.RunPipe([cmd], **kwargs) |
| 87 |
| 88 |
| 89 class SSHCommand(Command): |
| 90 """Remote shell commands.""" |
| 91 |
| 92 CONNECT_TIMEOUT = 5 |
| 93 |
| 94 def __init__(self, env, remote): |
| 95 Command.__init__(self, env) |
| 96 self.remote = remote |
| 97 self.ssh_dir = None |
| 98 self.identity = env.CrosUtilsPath('mod_for_test_scripts/ssh_keys/' |
| 99 'testing_rsa') |
| 100 |
| 101 def Setup(self): |
| 102 self.ssh_dir = tempfile.mkdtemp(prefix='ssh-tmp-') |
| 103 self.known_hosts = os.path.join(self.ssh_dir, 'known-hosts') |
| 104 |
| 105 def Cleanup(self): |
| 106 Command.RunPipe(self, [['rm', '-rf', self.ssh_dir]]) |
| 107 self.ssh_dir = None |
| 108 |
| 109 def GetArgs(self): |
| 110 if not self.ssh_dir: |
| 111 self.Setup() |
| 112 |
| 113 return ['-o', 'Compression=no', |
| 114 '-o', 'ConnectTimeout=%d' % self.CONNECT_TIMEOUT, |
| 115 '-o', 'StrictHostKeyChecking=no', |
| 116 '-o', 'UserKnownHostsFile=%s' % self.known_hosts, |
| 117 '-i', self.identity] |
| 118 |
| 119 def RunPipe(self, pipeline, **kwargs): |
| 120 args = ['ssh'] + self.GetArgs() |
| 121 if 'remote_tunnel' in kwargs: |
| 122 ports = kwargs.pop('remote_tunnel') |
| 123 args += ['-R %d:localhost:%d' % ports] |
| 124 pipeline[0] = args + ['root@%s' % self.remote] + list(pipeline[0]) |
| 125 return Command.RunPipe(self, pipeline, **kwargs) |
| 126 |
| 127 def Reset(self): |
| 128 os.unlink(self.known_hosts) |
| 129 |
| 130 def Copy(self, src, dest): |
| 131 return Command.RunPipe(self, [['scp'] + self.GetArgs() + |
| 132 [src, 'root@%s:%s' % |
| 133 (self.remote, dest)]]) |
| 134 |
| 135 |
| 136 class CrosEnv(object): |
| 137 """Encapsulates the ChromeOS build system environment functionality.""" |
| 138 |
| 139 REBOOT_START_WAIT = 5 |
| 140 REBOOT_WAIT_TIME = 60 |
| 141 |
| 142 def __init__(self, verbose=False): |
| 143 self.cros_root = os.path.dirname(os.path.abspath(sys.argv[0])) |
| 144 parent = os.path.dirname(self.cros_root) |
| 145 if os.path.exists(os.path.join(parent, 'chromeos-common.sh')): |
| 146 self.cros_root = parent |
| 147 self.cmd = Command(self) |
| 148 self.verbose = verbose |
| 149 |
| 150 def Error(self, msg): |
| 151 print >> sys.stderr, 'ERROR: %s' % msg |
| 152 |
| 153 def Fatal(self, msg=None): |
| 154 if msg: |
| 155 self.Error(msg) |
| 156 sys.exit(1) |
| 157 |
| 158 def Info(self, msg): |
| 159 if self.verbose: |
| 160 print 'INFO: %s' % msg |
| 161 |
| 162 def CrosUtilsPath(self, filename): |
| 163 return os.path.join(self.cros_root, filename) |
| 164 |
| 165 def ChrootPath(self, filename): |
| 166 return self.CrosUtilsPath(os.path.join('..', '..', 'chroot', |
| 167 filename.strip(os.path.sep))) |
| 168 |
| 169 def FileOneLine(self, filename): |
| 170 return file(filename).read().rstrip('\r\n') |
| 171 |
| 172 def GetLatestImage(self, board): |
| 173 return self.cmd.OutputOneLine(self.CrosUtilsPath('get_latest_image.sh'), |
| 174 '--board=%s' % board) |
| 175 |
| 176 def GetCached(self, src, dst): |
| 177 return (os.path.exists(dst) and |
| 178 os.path.getmtime(dst) >= os.path.getmtime(src)) |
| 179 |
| 180 def GenerateUpdatePayload(self, src, dst): |
| 181 """Generate an update image from a build-image output file.""" |
| 182 |
| 183 if self.GetCached(src, dst): |
| 184 self.Info('Using cached update image %s' % dst) |
| 185 return True |
| 186 |
| 187 if not self.cmd.Run(self.CrosUtilsPath('cros_generate_update_payload'), |
| 188 '--image=%s' % src, '--output=%s' % dst, |
| 189 '--patch_kernel'): |
| 190 self.Error('generate_payload failed') |
| 191 return False |
| 192 |
| 193 return True |
| 194 |
| 195 def BuildStateful(self, src, dst): |
| 196 """Create a stateful partition update image.""" |
| 197 |
| 198 if self.GetCached(src, dst): |
| 199 self.Info('Using cached stateful %s' % dst) |
| 200 return True |
| 201 |
| 202 cgpt = self.ChrootPath('/usr/bin/cgpt') |
| 203 offset = self.cmd.OutputOneLine(cgpt, 'show', '-b', '-i', '1', src) |
| 204 size = self.cmd.OutputOneLine(cgpt, 'show', '-s', '-i', '1', src) |
| 205 if None in (size, offset): |
| 206 self.Error('Unable to use cgpt to get image geometry') |
| 207 return False |
| 208 |
| 209 return self.cmd.RunPipe([['dd', 'if=%s' % src, 'bs=512', |
| 210 'skip=%s' % offset, 'count=%s' % size], |
| 211 ['gzip', '-c']], outfile=dst) |
| 212 |
| 213 def GetSize(self, filename): |
| 214 return os.path.getsize(filename) |
| 215 |
| 216 def GetHash(self, filename): |
| 217 return self.cmd.RunPipe([['openssl', 'sha1', '-binary'], |
| 218 ['openssl', 'base64']], |
| 219 infile=filename, |
| 220 capture=True, oneline=True) |
| 221 |
| 222 def GetDefaultBoard(self): |
| 223 def_board_file = self.CrosUtilsPath('.default_board') |
| 224 if not os.path.exists(def_board_file): |
| 225 return None |
| 226 return self.FileOneLine(def_board_file) |
| 227 |
| 228 def SetRemote(self, remote): |
| 229 self.ssh_cmd = SSHCommand(self, remote) |
| 230 |
| 231 def ParseShVars(self, string): |
| 232 """Parse an input file into a dict containing all variable assignments.""" |
| 233 |
| 234 ret = {} |
| 235 for line in string.splitlines(): |
| 236 if '=' in line: |
| 237 var, sep, val = line.partition('=') |
| 238 var = var.strip('\t ').rstrip('\t ') |
| 239 if var: |
| 240 ret[var] = val.strip('\t ').rstrip('\t ') |
| 241 return ret |
| 242 |
| 243 def GetRemoteRelease(self): |
| 244 lsb_release = self.ssh_cmd.Output('cat', '/etc/lsb-release') |
| 245 if not lsb_release: |
| 246 return None |
| 247 return self.ParseShVars(lsb_release) |
| 248 |
| 249 def CreateServer(self, port, update_file, stateful_file): |
| 250 """Start the devserver clone.""" |
| 251 |
| 252 PingUpdateResponse.Setup(self.GetHash(update_file), |
| 253 self.GetSize(update_file)) |
| 254 |
| 255 UpdateHandler.SetupUrl('/update', PingUpdateResponse()) |
| 256 UpdateHandler.SetupUrl('/%s' % UPDATE_FILENAME, |
| 257 FileUpdateResponse(update_file, |
| 258 verbose=self.verbose)) |
| 259 UpdateHandler.SetupUrl('/%s' % STATEFUL_FILENAME, |
| 260 FileUpdateResponse(stateful_file, |
| 261 verbose=self.verbose)) |
| 262 |
| 263 self.http_server = BaseHTTPServer.HTTPServer(('', port), UpdateHandler) |
| 264 |
| 265 def StartServer(self): |
| 266 self.Info('Starting http server') |
| 267 self.http_server.serve_forever() |
| 268 |
| 269 def GetUpdateStatus(self): |
| 270 status = self.ssh_cmd.Output('/usr/bin/update_engine_client', '--status') |
| 271 if not status: |
| 272 self.Error('Cannot get update status') |
| 273 return None |
| 274 |
| 275 return self.ParseShVars(status).get('CURRENT_OP', None) |
| 276 |
| 277 def ClientReboot(self): |
| 278 """Send "reboot" command to the client, and wait for it to return.""" |
| 279 |
| 280 self.ssh_cmd.Reset() |
| 281 self.ssh_cmd.Run('reboot') |
| 282 self.Info('Waiting for client to reboot') |
| 283 time.sleep(self.REBOOT_START_WAIT) |
| 284 for attempt in range(self.REBOOT_WAIT_TIME/SSHCommand.CONNECT_TIMEOUT): |
| 285 start = time.time() |
| 286 if self.ssh_cmd.Run('/bin/true'): |
| 287 return True |
| 288 # Make sure we wait at least as long as the connect timeout would have, |
| 289 # since we calculated our number of attempts based on that |
| 290 self.Info('Client has not yet restarted (try %d). Waiting...' % attempt) |
| 291 wait_time = SSHCommand.CONNECT_TIMEOUT - (time.time() - start) |
| 292 if wait_time > 0: |
| 293 time.sleep(wait_time) |
| 294 |
| 295 return False |
| 296 |
| 297 def StartClient(self, port): |
| 298 """Ask the client machine to update from our server.""" |
| 299 |
| 300 status = self.GetUpdateStatus() |
| 301 if status != 'UPDATE_STATUS_IDLE': |
| 302 self.Error('Client update status is not IDLE: %s' % status) |
| 303 return False |
| 304 |
| 305 url_base = 'http://localhost:%d' % port |
| 306 update_url = '%s/update' % url_base |
| 307 fd, update_log = tempfile.mkstemp(prefix='image-to-target-') |
| 308 self.Info('Starting update on client. Client output stored to %s' % |
| 309 update_log) |
| 310 self.ssh_cmd.Run('/usr/bin/update_engine_client', '--update', |
| 311 '--omaha_url', update_url, remote_tunnel=(port, port), |
| 312 outfile=update_log) |
| 313 |
| 314 if self.GetUpdateStatus() != 'UPDATE_STATUS_UPDATED_NEED_REBOOT': |
| 315 self.Error('Client update failed') |
| 316 return False |
| 317 |
| 318 self.ssh_cmd.Copy(self.CrosUtilsPath('../platform/dev/stateful_update'), |
| 319 '/tmp') |
| 320 if not self.ssh_cmd.Run('/tmp/stateful_update', url_base, |
| 321 remote_tunnel=(port, port)): |
| 322 self.Error('Client stateful update failed') |
| 323 return False |
| 324 |
| 325 self.Info('Rebooting client') |
| 326 if not self.ClientReboot(): |
| 327 self.Error('Client may not have successfully rebooted...') |
| 328 return False |
| 329 |
| 330 print 'Client update completed successfully!' |
| 331 return True |
| 332 |
| 333 |
| 334 class UpdateResponse(object): |
| 335 """Default response is the 404 error response.""" |
| 336 |
| 337 def Reply(self, handler, send_content=True, post_data=None): |
| 338 handler.send_Error(404, 'File not found') |
| 339 return None |
| 340 |
| 341 |
| 342 class FileUpdateResponse(UpdateResponse): |
| 343 """Respond by sending the contents of a file.""" |
| 344 |
| 345 def __init__(self, filename, content_type='application/octet-stream', |
| 346 verbose=False, blocksize=16*1024): |
| 347 self.filename = filename |
| 348 self.content_type = content_type |
| 349 self.verbose = verbose |
| 350 self.blocksize = blocksize |
| 351 |
| 352 def Reply(self, handler, send_content=True, post_data=None): |
| 353 """Return file contents to the client. Optionally display progress.""" |
| 354 |
| 355 try: |
| 356 f = open(self.filename, 'rb') |
| 357 except IOError: |
| 358 return UpdateResponse.Reply(self, handler) |
| 359 |
| 360 handler.send_response(200) |
| 361 handler.send_header('Content-type', self.content_type) |
| 362 filestat = os.fstat(f.fileno()) |
| 363 filesize = filestat[6] |
| 364 handler.send_header('Content-Length', str(filesize)) |
| 365 handler.send_header('Last-Modified', |
| 366 handler.date_time_string(filestat.st_mtime)) |
| 367 handler.end_headers() |
| 368 |
| 369 if not send_content: |
| 370 return |
| 371 |
| 372 if filesize <= self.blocksize: |
| 373 handler.wfile.write(f.read()) |
| 374 else: |
| 375 sent_size = 0 |
| 376 sent_percentage = None |
| 377 while True: |
| 378 buf = f.read(self.blocksize) |
| 379 if not buf: |
| 380 break |
| 381 handler.wfile.write(buf) |
| 382 if self.verbose: |
| 383 sent_size += len(buf) |
| 384 percentage = int(100 * sent_size / filesize) |
| 385 if sent_percentage != percentage: |
| 386 sent_percentage = percentage |
| 387 print '\rSent %d%%' % sent_percentage, |
| 388 sys.stdout.flush() |
| 389 if self.verbose: |
| 390 print '\n' |
| 391 f.close() |
| 392 |
| 393 |
| 394 class StringUpdateResponse(UpdateResponse): |
| 395 """Respond by sending the contents of a string.""" |
| 396 |
| 397 def __init__(self, string, content_type='text/plain'): |
| 398 self.string = string |
| 399 self.content_type = content_type |
| 400 |
| 401 def Reply(self, handler, send_content=True, post_data=None): |
| 402 handler.send_response(200) |
| 403 handler.send_header('Content-type', self.content_type) |
| 404 handler.send_header('Content-Length', len(self.string)) |
| 405 handler.end_headers() |
| 406 |
| 407 if not send_content: |
| 408 return |
| 409 |
| 410 handler.wfile.write(self.string) |
| 411 |
| 412 |
| 413 class PingUpdateResponse(StringUpdateResponse): |
| 414 """Respond to a client ping with pre-fab XML response.""" |
| 415 |
| 416 app_id = '87efface-864d-49a5-9bb3-4b050a7c227a' |
| 417 xmlns = 'http://www.google.com/update2/response' |
| 418 payload_success_template = """<?xml version="1.0" encoding="UTF-8"?> |
| 419 <gupdate xmlns="%s" protocol="2.0"> |
| 420 <daystart elapsed_seconds="%s"/> |
| 421 <app appid="{%s}" status="ok"> |
| 422 <ping status="ok"/> |
| 423 <updatecheck |
| 424 codebase="%s" |
| 425 hash="%s" |
| 426 needsadmin="false" |
| 427 size="%s" |
| 428 status="ok"/> |
| 429 </app> |
| 430 </gupdate> |
| 431 """ |
| 432 payload_failure_template = """<?xml version="1.0" encoding="UTF-8"?> |
| 433 <gupdate xmlns="%s" protocol="2.0"> |
| 434 <daystart elapsed_seconds="%s"/> |
| 435 <app appid="{%s}" status="ok"> |
| 436 <ping status="ok"/> |
| 437 <updatecheck status="noupdate"/> |
| 438 </app> |
| 439 </gupdate> |
| 440 """ |
| 441 |
| 442 def __init__(self): |
| 443 self.content_type = 'text/xml' |
| 444 |
| 445 @staticmethod |
| 446 def Setup(filehash, filesize): |
| 447 PingUpdateResponse.file_hash = filehash |
| 448 PingUpdateResponse.file_size = filesize |
| 449 |
| 450 def Reply(self, handler, send_content=True, post_data=None): |
| 451 """Return (using StringResponse) an XML reply to ForcedUpdate clients.""" |
| 452 |
| 453 if not post_data: |
| 454 return UpdateResponse.Reply(self, handler) |
| 455 |
| 456 request_version = (minidom.parseString(post_data).firstChild. |
| 457 getElementsByTagName('o:app')[0]. |
| 458 getAttribute('version')) |
| 459 |
| 460 if request_version == 'ForcedUpdate': |
| 461 host, pdict = cgi.parse_header(handler.headers.getheader('Host')) |
| 462 self.string = (self.payload_success_template % |
| 463 (self.xmlns, self.SecondsSinceMidnight(), |
| 464 self.app_id, 'http://%s/%s' % (host, UPDATE_FILENAME), |
| 465 self.file_hash, self.file_size)) |
| 466 else: |
| 467 self.string = (self.payload_failure_template % |
| 468 (self.xmlns, self.SecondsSinceMidnight(), self.app_id)) |
| 469 |
| 470 StringUpdateResponse.Reply(self, handler, send_content) |
| 471 |
| 472 def SecondsSinceMidnight(self): |
| 473 now = time.localtime() |
| 474 return now[3] * 3600 + now[4] * 60 + now[5] |
| 475 |
| 476 |
| 477 class UpdateHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
| 478 """Handler for HTTP requests to devserver clone.""" |
| 479 |
| 480 server_version = 'ImageToTargetUpdater/0.0' |
| 481 url_mapping = {} |
| 482 |
| 483 @staticmethod |
| 484 def SetupUrl(url, response): |
| 485 UpdateHandler.url_mapping[url] = response |
| 486 |
| 487 def do_GET(self): |
| 488 """Serve a GET request.""" |
| 489 response = UpdateHandler.url_mapping.get(self.path, UpdateResponse()) |
| 490 response.Reply(self, True) |
| 491 |
| 492 def do_HEAD(self): |
| 493 """Serve a HEAD request.""" |
| 494 response = UpdateHandler.url_mapping.get(self.path, UpdateResponse()) |
| 495 response.Reply(self, False) |
| 496 |
| 497 def do_POST(self): |
| 498 content_length = int(self.headers.getheader('Content-Length')) |
| 499 request = self.rfile.read(content_length) |
| 500 response = UpdateHandler.url_mapping.get(self.path, UpdateResponse()) |
| 501 response.Reply(self, True, request) |
| 502 |
| 503 |
| 504 class ChildFinished(Exception): |
| 505 """Child exit exception.""" |
| 506 |
| 507 def __init__(self, pid): |
| 508 Exception.__init__(self) |
| 509 self.pid = pid |
| 510 self.status = None |
| 511 |
| 512 def __str__(self): |
| 513 return 'Process %d exited status %d' % (self.pid, self.status) |
| 514 |
| 515 def __nonzero__(self): |
| 516 return self.status is not None |
| 517 |
| 518 def SigHandler(self, signum, frame): |
| 519 """Handle SIGCHLD signal, and retreive client exit code.""" |
| 520 |
| 521 while True: |
| 522 try: |
| 523 (pid, status) = os.waitpid(-1, os.WNOHANG) |
| 524 except OSError, e: |
| 525 if e.args[0] != errno.ECHILD: |
| 526 raise e |
| 527 |
| 528 # TODO(pstew): returning here won't help -- SocketServer gets EINTR |
| 529 return |
| 530 |
| 531 if pid == self.pid: |
| 532 if os.WIFEXITED(status): |
| 533 self.status = os.WEXITSTATUS(status) |
| 534 else: |
| 535 self.status = 255 |
| 536 raise self |
| 537 |
| 538 |
| 539 def main(argv): |
| 540 usage = 'usage: %prog' |
| 541 parser = optparse.OptionParser(usage=usage) |
| 542 parser.add_option('--board', dest='board', default=None, |
| 543 help='Board platform type') |
| 544 parser.add_option('--force-mismatch', dest='force_mismatch', default=False, |
| 545 action='store_true', |
| 546 help='Upgrade even if client arch does not match') |
| 547 parser.add_option('--from', dest='src', default=None, |
| 548 help='Source image to install') |
| 549 parser.add_option('--image-name', dest='image_name', |
| 550 default=DEFAULT_IMAGE_NAME, |
| 551 help='Filename within image directory to load') |
| 552 parser.add_option('--port', dest='port', default=8081, type='int', |
| 553 help='TCP port to serve from and tunnel through') |
| 554 parser.add_option('--remote', dest='remote', default=None, |
| 555 help='Remote device-under-test IP address') |
| 556 parser.add_option('--server-only', dest='server_only', default=False, |
| 557 action='store_true', help='Do not start client') |
| 558 parser.add_option('--verbose', dest='verbose', default=False, |
| 559 action='store_true', help='Display running commands') |
| 560 |
| 561 (options, args) = parser.parse_args(argv) |
| 562 |
| 563 cros_env = CrosEnv(verbose=options.verbose) |
| 564 |
| 565 if not options.board: |
| 566 options.board = cros_env.GetDefaultBoard() |
| 567 |
| 568 if not options.src: |
| 569 options.src = cros_env.GetLatestImage(options.board) |
| 570 if options.src is None: |
| 571 parser.error('No --from argument given and no default image found') |
| 572 |
| 573 cros_env.Info('Performing update from %s' % options.src) |
| 574 |
| 575 if not os.path.exists(options.src): |
| 576 parser.error('Path %s does not exist' % options.src) |
| 577 |
| 578 if os.path.isdir(options.src): |
| 579 image_directory = options.src |
| 580 image_file = os.path.join(options.src, options.image_name) |
| 581 |
| 582 if not os.path.exists(image_file): |
| 583 parser.error('Image file %s does not exist' % image_file) |
| 584 else: |
| 585 image_file = options.src |
| 586 image_directory = os.path.dirname(options.src) |
| 587 |
| 588 if options.remote: |
| 589 cros_env.SetRemote(options.remote) |
| 590 rel = cros_env.GetRemoteRelease() |
| 591 if not rel: |
| 592 cros_env.Fatal('Could not retrieve remote lsb-release') |
| 593 board = rel.get('CHROMEOS_RELEASE_BOARD', '(None)') |
| 594 if board != options.board and not options.force_mismatch: |
| 595 cros_env.Error('Board %s does not match expected %s' % |
| 596 (board, options.board)) |
| 597 cros_env.Error('(Use --force-mismatch option to override this)') |
| 598 cros_env.Fatal() |
| 599 |
| 600 elif not options.server_only: |
| 601 parser.error('Either --server-only must be specified or ' |
| 602 '--remote=<client> needs to be given') |
| 603 |
| 604 update_file = os.path.join(image_directory, UPDATE_FILENAME) |
| 605 stateful_file = os.path.join(image_directory, STATEFUL_FILENAME) |
| 606 |
| 607 if (not cros_env.GenerateUpdatePayload(image_file, update_file) or |
| 608 not cros_env.BuildStateful(image_file, stateful_file)): |
| 609 cros_env.Fatal() |
| 610 |
| 611 cros_env.CreateServer(options.port, update_file, stateful_file) |
| 612 |
| 613 exit_status = 1 |
| 614 if options.server_only: |
| 615 child = None |
| 616 else: |
| 617 # Start an "image-to-live" instance that will pull bits from the server |
| 618 child = os.fork() |
| 619 if child: |
| 620 signal.signal(signal.SIGCHLD, ChildFinished(child).SigHandler) |
| 621 else: |
| 622 try: |
| 623 time.sleep(SERVER_STARTUP_WAIT) |
| 624 if cros_env.StartClient(options.port): |
| 625 exit_status = 0 |
| 626 except KeyboardInterrupt: |
| 627 cros_env.Error('Client Exiting on Control-C') |
| 628 except: |
| 629 cros_env.Error('Exception in client code:') |
| 630 traceback.print_exc(file=sys.stdout) |
| 631 |
| 632 cros_env.ssh_cmd.Cleanup() |
| 633 cros_env.Info('Client exiting with status %d' % exit_status) |
| 634 sys.exit(exit_status) |
| 635 |
| 636 try: |
| 637 cros_env.StartServer() |
| 638 except KeyboardInterrupt: |
| 639 cros_env.Info('Server Exiting on Control-C') |
| 640 exit_status = 0 |
| 641 except ChildFinished, e: |
| 642 cros_env.Info('Server Exiting on Client Exit (%d)' % e.status) |
| 643 exit_status = e.status |
| 644 child = None |
| 645 except: |
| 646 cros_env.Error('Exception in server code:') |
| 647 traceback.print_exc(file=sys.stdout) |
| 648 |
| 649 if child: |
| 650 os.kill(child, 15) |
| 651 |
| 652 cros_env.Info('Server exiting with status %d' % exit_status) |
| 653 sys.exit(exit_status) |
| 654 |
| 655 |
| 656 if __name__ == '__main__': |
| 657 main(sys.argv) |
OLD | NEW |