| Index: bin/cros_image_to_target.py
|
| diff --git a/bin/cros_image_to_target.py b/bin/cros_image_to_target.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..2b88cce6cf08a05031b07793fbc2ecf93934916f
|
| --- /dev/null
|
| +++ b/bin/cros_image_to_target.py
|
| @@ -0,0 +1,657 @@
|
| +#!/usr/bin/python
|
| +#
|
| +# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +"""Create and copy update image to target host.
|
| +
|
| +auto-update and devserver change out from beneath us often enough
|
| +that despite having to duplicate a litte code, it seems that the
|
| +right thing to do here is to start over and do something that is
|
| +simple enough and easy enough to understand so that when more
|
| +stuff breaks, at least we can solve them faster.
|
| +"""
|
| +
|
| +import BaseHTTPServer
|
| +import cgi
|
| +import errno
|
| +import optparse
|
| +import os
|
| +import signal
|
| +import subprocess
|
| +import sys
|
| +import tempfile
|
| +import time
|
| +import traceback
|
| +
|
| +from xml.dom import minidom
|
| +
|
| +
|
| +# This is the default filename within the image directory to load updates from
|
| +DEFAULT_IMAGE_NAME = 'chromiumos_image.bin'
|
| +
|
| +# The filenames we provide to clients to pull updates
|
| +UPDATE_FILENAME = 'update.gz'
|
| +STATEFUL_FILENAME = 'stateful.image.gz'
|
| +
|
| +# How long do we wait for the server to start before launching client
|
| +SERVER_STARTUP_WAIT = 1
|
| +
|
| +
|
| +class Command(object):
|
| + """Shell command ease-ups for Python."""
|
| +
|
| + def __init__(self, env):
|
| + self.env = env
|
| +
|
| + def RunPipe(self, pipeline, infile=None, outfile=None,
|
| + capture=False, oneline=False):
|
| + """Perform a command pipeline, with optional input/output filenames."""
|
| +
|
| + last_pipe = None
|
| + while pipeline:
|
| + cmd = pipeline.pop(0)
|
| + kwargs = {}
|
| + if last_pipe is not None:
|
| + kwargs['stdin'] = last_pipe.stdout
|
| + elif infile:
|
| + kwargs['stdin'] = open(infile, 'rb')
|
| + if pipeline or capture:
|
| + kwargs['stdout'] = subprocess.PIPE
|
| + elif outfile:
|
| + kwargs['stdout'] = open(outfile, 'wb')
|
| +
|
| + self.env.Info('Running: %s' % ' '.join(cmd))
|
| + last_pipe = subprocess.Popen(cmd, **kwargs)
|
| +
|
| + if capture:
|
| + ret = last_pipe.communicate()[0]
|
| + if not ret:
|
| + return None
|
| + elif oneline:
|
| + return ret.rstrip('\r\n')
|
| + else:
|
| + return ret
|
| + else:
|
| + return os.waitpid(last_pipe.pid, 0)[1] == 0
|
| +
|
| + def Output(self, *cmd):
|
| + return self.RunPipe([cmd], capture=True)
|
| +
|
| + def OutputOneLine(self, *cmd):
|
| + return self.RunPipe([cmd], capture=True, oneline=True)
|
| +
|
| + def Run(self, *cmd, **kwargs):
|
| + return self.RunPipe([cmd], **kwargs)
|
| +
|
| +
|
| +class SSHCommand(Command):
|
| + """Remote shell commands."""
|
| +
|
| + CONNECT_TIMEOUT = 5
|
| +
|
| + def __init__(self, env, remote):
|
| + Command.__init__(self, env)
|
| + self.remote = remote
|
| + self.ssh_dir = None
|
| + self.identity = env.CrosUtilsPath('mod_for_test_scripts/ssh_keys/'
|
| + 'testing_rsa')
|
| +
|
| + def Setup(self):
|
| + self.ssh_dir = tempfile.mkdtemp(prefix='ssh-tmp-')
|
| + self.known_hosts = os.path.join(self.ssh_dir, 'known-hosts')
|
| +
|
| + def Cleanup(self):
|
| + Command.RunPipe(self, [['rm', '-rf', self.ssh_dir]])
|
| + self.ssh_dir = None
|
| +
|
| + def GetArgs(self):
|
| + if not self.ssh_dir:
|
| + self.Setup()
|
| +
|
| + return ['-o', 'Compression=no',
|
| + '-o', 'ConnectTimeout=%d' % self.CONNECT_TIMEOUT,
|
| + '-o', 'StrictHostKeyChecking=no',
|
| + '-o', 'UserKnownHostsFile=%s' % self.known_hosts,
|
| + '-i', self.identity]
|
| +
|
| + def RunPipe(self, pipeline, **kwargs):
|
| + args = ['ssh'] + self.GetArgs()
|
| + if 'remote_tunnel' in kwargs:
|
| + ports = kwargs.pop('remote_tunnel')
|
| + args += ['-R %d:localhost:%d' % ports]
|
| + pipeline[0] = args + ['root@%s' % self.remote] + list(pipeline[0])
|
| + return Command.RunPipe(self, pipeline, **kwargs)
|
| +
|
| + def Reset(self):
|
| + os.unlink(self.known_hosts)
|
| +
|
| + def Copy(self, src, dest):
|
| + return Command.RunPipe(self, [['scp'] + self.GetArgs() +
|
| + [src, 'root@%s:%s' %
|
| + (self.remote, dest)]])
|
| +
|
| +
|
| +class CrosEnv(object):
|
| + """Encapsulates the ChromeOS build system environment functionality."""
|
| +
|
| + REBOOT_START_WAIT = 5
|
| + REBOOT_WAIT_TIME = 60
|
| +
|
| + def __init__(self, verbose=False):
|
| + self.cros_root = os.path.dirname(os.path.abspath(sys.argv[0]))
|
| + parent = os.path.dirname(self.cros_root)
|
| + if os.path.exists(os.path.join(parent, 'chromeos-common.sh')):
|
| + self.cros_root = parent
|
| + self.cmd = Command(self)
|
| + self.verbose = verbose
|
| +
|
| + def Error(self, msg):
|
| + print >> sys.stderr, 'ERROR: %s' % msg
|
| +
|
| + def Fatal(self, msg=None):
|
| + if msg:
|
| + self.Error(msg)
|
| + sys.exit(1)
|
| +
|
| + def Info(self, msg):
|
| + if self.verbose:
|
| + print 'INFO: %s' % msg
|
| +
|
| + def CrosUtilsPath(self, filename):
|
| + return os.path.join(self.cros_root, filename)
|
| +
|
| + def ChrootPath(self, filename):
|
| + return self.CrosUtilsPath(os.path.join('..', '..', 'chroot',
|
| + filename.strip(os.path.sep)))
|
| +
|
| + def FileOneLine(self, filename):
|
| + return file(filename).read().rstrip('\r\n')
|
| +
|
| + def GetLatestImage(self, board):
|
| + return self.cmd.OutputOneLine(self.CrosUtilsPath('get_latest_image.sh'),
|
| + '--board=%s' % board)
|
| +
|
| + def GetCached(self, src, dst):
|
| + return (os.path.exists(dst) and
|
| + os.path.getmtime(dst) >= os.path.getmtime(src))
|
| +
|
| + def GenerateUpdatePayload(self, src, dst):
|
| + """Generate an update image from a build-image output file."""
|
| +
|
| + if self.GetCached(src, dst):
|
| + self.Info('Using cached update image %s' % dst)
|
| + return True
|
| +
|
| + if not self.cmd.Run(self.CrosUtilsPath('cros_generate_update_payload'),
|
| + '--image=%s' % src, '--output=%s' % dst,
|
| + '--patch_kernel'):
|
| + self.Error('generate_payload failed')
|
| + return False
|
| +
|
| + return True
|
| +
|
| + def BuildStateful(self, src, dst):
|
| + """Create a stateful partition update image."""
|
| +
|
| + if self.GetCached(src, dst):
|
| + self.Info('Using cached stateful %s' % dst)
|
| + return True
|
| +
|
| + cgpt = self.ChrootPath('/usr/bin/cgpt')
|
| + offset = self.cmd.OutputOneLine(cgpt, 'show', '-b', '-i', '1', src)
|
| + size = self.cmd.OutputOneLine(cgpt, 'show', '-s', '-i', '1', src)
|
| + if None in (size, offset):
|
| + self.Error('Unable to use cgpt to get image geometry')
|
| + return False
|
| +
|
| + return self.cmd.RunPipe([['dd', 'if=%s' % src, 'bs=512',
|
| + 'skip=%s' % offset, 'count=%s' % size],
|
| + ['gzip', '-c']], outfile=dst)
|
| +
|
| + def GetSize(self, filename):
|
| + return os.path.getsize(filename)
|
| +
|
| + def GetHash(self, filename):
|
| + return self.cmd.RunPipe([['openssl', 'sha1', '-binary'],
|
| + ['openssl', 'base64']],
|
| + infile=filename,
|
| + capture=True, oneline=True)
|
| +
|
| + def GetDefaultBoard(self):
|
| + def_board_file = self.CrosUtilsPath('.default_board')
|
| + if not os.path.exists(def_board_file):
|
| + return None
|
| + return self.FileOneLine(def_board_file)
|
| +
|
| + def SetRemote(self, remote):
|
| + self.ssh_cmd = SSHCommand(self, remote)
|
| +
|
| + def ParseShVars(self, string):
|
| + """Parse an input file into a dict containing all variable assignments."""
|
| +
|
| + ret = {}
|
| + for line in string.splitlines():
|
| + if '=' in line:
|
| + var, sep, val = line.partition('=')
|
| + var = var.strip('\t ').rstrip('\t ')
|
| + if var:
|
| + ret[var] = val.strip('\t ').rstrip('\t ')
|
| + return ret
|
| +
|
| + def GetRemoteRelease(self):
|
| + lsb_release = self.ssh_cmd.Output('cat', '/etc/lsb-release')
|
| + if not lsb_release:
|
| + return None
|
| + return self.ParseShVars(lsb_release)
|
| +
|
| + def CreateServer(self, port, update_file, stateful_file):
|
| + """Start the devserver clone."""
|
| +
|
| + PingUpdateResponse.Setup(self.GetHash(update_file),
|
| + self.GetSize(update_file))
|
| +
|
| + UpdateHandler.SetupUrl('/update', PingUpdateResponse())
|
| + UpdateHandler.SetupUrl('/%s' % UPDATE_FILENAME,
|
| + FileUpdateResponse(update_file,
|
| + verbose=self.verbose))
|
| + UpdateHandler.SetupUrl('/%s' % STATEFUL_FILENAME,
|
| + FileUpdateResponse(stateful_file,
|
| + verbose=self.verbose))
|
| +
|
| + self.http_server = BaseHTTPServer.HTTPServer(('', port), UpdateHandler)
|
| +
|
| + def StartServer(self):
|
| + self.Info('Starting http server')
|
| + self.http_server.serve_forever()
|
| +
|
| + def GetUpdateStatus(self):
|
| + status = self.ssh_cmd.Output('/usr/bin/update_engine_client', '--status')
|
| + if not status:
|
| + self.Error('Cannot get update status')
|
| + return None
|
| +
|
| + return self.ParseShVars(status).get('CURRENT_OP', None)
|
| +
|
| + def ClientReboot(self):
|
| + """Send "reboot" command to the client, and wait for it to return."""
|
| +
|
| + self.ssh_cmd.Reset()
|
| + self.ssh_cmd.Run('reboot')
|
| + self.Info('Waiting for client to reboot')
|
| + time.sleep(self.REBOOT_START_WAIT)
|
| + for attempt in range(self.REBOOT_WAIT_TIME/SSHCommand.CONNECT_TIMEOUT):
|
| + start = time.time()
|
| + if self.ssh_cmd.Run('/bin/true'):
|
| + return True
|
| + # Make sure we wait at least as long as the connect timeout would have,
|
| + # since we calculated our number of attempts based on that
|
| + self.Info('Client has not yet restarted (try %d). Waiting...' % attempt)
|
| + wait_time = SSHCommand.CONNECT_TIMEOUT - (time.time() - start)
|
| + if wait_time > 0:
|
| + time.sleep(wait_time)
|
| +
|
| + return False
|
| +
|
| + def StartClient(self, port):
|
| + """Ask the client machine to update from our server."""
|
| +
|
| + status = self.GetUpdateStatus()
|
| + if status != 'UPDATE_STATUS_IDLE':
|
| + self.Error('Client update status is not IDLE: %s' % status)
|
| + return False
|
| +
|
| + url_base = 'http://localhost:%d' % port
|
| + update_url = '%s/update' % url_base
|
| + fd, update_log = tempfile.mkstemp(prefix='image-to-target-')
|
| + self.Info('Starting update on client. Client output stored to %s' %
|
| + update_log)
|
| + self.ssh_cmd.Run('/usr/bin/update_engine_client', '--update',
|
| + '--omaha_url', update_url, remote_tunnel=(port, port),
|
| + outfile=update_log)
|
| +
|
| + if self.GetUpdateStatus() != 'UPDATE_STATUS_UPDATED_NEED_REBOOT':
|
| + self.Error('Client update failed')
|
| + return False
|
| +
|
| + self.ssh_cmd.Copy(self.CrosUtilsPath('../platform/dev/stateful_update'),
|
| + '/tmp')
|
| + if not self.ssh_cmd.Run('/tmp/stateful_update', url_base,
|
| + remote_tunnel=(port, port)):
|
| + self.Error('Client stateful update failed')
|
| + return False
|
| +
|
| + self.Info('Rebooting client')
|
| + if not self.ClientReboot():
|
| + self.Error('Client may not have successfully rebooted...')
|
| + return False
|
| +
|
| + print 'Client update completed successfully!'
|
| + return True
|
| +
|
| +
|
| +class UpdateResponse(object):
|
| + """Default response is the 404 error response."""
|
| +
|
| + def Reply(self, handler, send_content=True, post_data=None):
|
| + handler.send_Error(404, 'File not found')
|
| + return None
|
| +
|
| +
|
| +class FileUpdateResponse(UpdateResponse):
|
| + """Respond by sending the contents of a file."""
|
| +
|
| + def __init__(self, filename, content_type='application/octet-stream',
|
| + verbose=False, blocksize=16*1024):
|
| + self.filename = filename
|
| + self.content_type = content_type
|
| + self.verbose = verbose
|
| + self.blocksize = blocksize
|
| +
|
| + def Reply(self, handler, send_content=True, post_data=None):
|
| + """Return file contents to the client. Optionally display progress."""
|
| +
|
| + try:
|
| + f = open(self.filename, 'rb')
|
| + except IOError:
|
| + return UpdateResponse.Reply(self, handler)
|
| +
|
| + handler.send_response(200)
|
| + handler.send_header('Content-type', self.content_type)
|
| + filestat = os.fstat(f.fileno())
|
| + filesize = filestat[6]
|
| + handler.send_header('Content-Length', str(filesize))
|
| + handler.send_header('Last-Modified',
|
| + handler.date_time_string(filestat.st_mtime))
|
| + handler.end_headers()
|
| +
|
| + if not send_content:
|
| + return
|
| +
|
| + if filesize <= self.blocksize:
|
| + handler.wfile.write(f.read())
|
| + else:
|
| + sent_size = 0
|
| + sent_percentage = None
|
| + while True:
|
| + buf = f.read(self.blocksize)
|
| + if not buf:
|
| + break
|
| + handler.wfile.write(buf)
|
| + if self.verbose:
|
| + sent_size += len(buf)
|
| + percentage = int(100 * sent_size / filesize)
|
| + if sent_percentage != percentage:
|
| + sent_percentage = percentage
|
| + print '\rSent %d%%' % sent_percentage,
|
| + sys.stdout.flush()
|
| + if self.verbose:
|
| + print '\n'
|
| + f.close()
|
| +
|
| +
|
| +class StringUpdateResponse(UpdateResponse):
|
| + """Respond by sending the contents of a string."""
|
| +
|
| + def __init__(self, string, content_type='text/plain'):
|
| + self.string = string
|
| + self.content_type = content_type
|
| +
|
| + def Reply(self, handler, send_content=True, post_data=None):
|
| + handler.send_response(200)
|
| + handler.send_header('Content-type', self.content_type)
|
| + handler.send_header('Content-Length', len(self.string))
|
| + handler.end_headers()
|
| +
|
| + if not send_content:
|
| + return
|
| +
|
| + handler.wfile.write(self.string)
|
| +
|
| +
|
| +class PingUpdateResponse(StringUpdateResponse):
|
| + """Respond to a client ping with pre-fab XML response."""
|
| +
|
| + app_id = '87efface-864d-49a5-9bb3-4b050a7c227a'
|
| + xmlns = 'http://www.google.com/update2/response'
|
| + payload_success_template = """<?xml version="1.0" encoding="UTF-8"?>
|
| + <gupdate xmlns="%s" protocol="2.0">
|
| + <daystart elapsed_seconds="%s"/>
|
| + <app appid="{%s}" status="ok">
|
| + <ping status="ok"/>
|
| + <updatecheck
|
| + codebase="%s"
|
| + hash="%s"
|
| + needsadmin="false"
|
| + size="%s"
|
| + status="ok"/>
|
| + </app>
|
| + </gupdate>
|
| + """
|
| + payload_failure_template = """<?xml version="1.0" encoding="UTF-8"?>
|
| + <gupdate xmlns="%s" protocol="2.0">
|
| + <daystart elapsed_seconds="%s"/>
|
| + <app appid="{%s}" status="ok">
|
| + <ping status="ok"/>
|
| + <updatecheck status="noupdate"/>
|
| + </app>
|
| + </gupdate>
|
| + """
|
| +
|
| + def __init__(self):
|
| + self.content_type = 'text/xml'
|
| +
|
| + @staticmethod
|
| + def Setup(filehash, filesize):
|
| + PingUpdateResponse.file_hash = filehash
|
| + PingUpdateResponse.file_size = filesize
|
| +
|
| + def Reply(self, handler, send_content=True, post_data=None):
|
| + """Return (using StringResponse) an XML reply to ForcedUpdate clients."""
|
| +
|
| + if not post_data:
|
| + return UpdateResponse.Reply(self, handler)
|
| +
|
| + request_version = (minidom.parseString(post_data).firstChild.
|
| + getElementsByTagName('o:app')[0].
|
| + getAttribute('version'))
|
| +
|
| + if request_version == 'ForcedUpdate':
|
| + host, pdict = cgi.parse_header(handler.headers.getheader('Host'))
|
| + self.string = (self.payload_success_template %
|
| + (self.xmlns, self.SecondsSinceMidnight(),
|
| + self.app_id, 'http://%s/%s' % (host, UPDATE_FILENAME),
|
| + self.file_hash, self.file_size))
|
| + else:
|
| + self.string = (self.payload_failure_template %
|
| + (self.xmlns, self.SecondsSinceMidnight(), self.app_id))
|
| +
|
| + StringUpdateResponse.Reply(self, handler, send_content)
|
| +
|
| + def SecondsSinceMidnight(self):
|
| + now = time.localtime()
|
| + return now[3] * 3600 + now[4] * 60 + now[5]
|
| +
|
| +
|
| +class UpdateHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
| + """Handler for HTTP requests to devserver clone."""
|
| +
|
| + server_version = 'ImageToTargetUpdater/0.0'
|
| + url_mapping = {}
|
| +
|
| + @staticmethod
|
| + def SetupUrl(url, response):
|
| + UpdateHandler.url_mapping[url] = response
|
| +
|
| + def do_GET(self):
|
| + """Serve a GET request."""
|
| + response = UpdateHandler.url_mapping.get(self.path, UpdateResponse())
|
| + response.Reply(self, True)
|
| +
|
| + def do_HEAD(self):
|
| + """Serve a HEAD request."""
|
| + response = UpdateHandler.url_mapping.get(self.path, UpdateResponse())
|
| + response.Reply(self, False)
|
| +
|
| + def do_POST(self):
|
| + content_length = int(self.headers.getheader('Content-Length'))
|
| + request = self.rfile.read(content_length)
|
| + response = UpdateHandler.url_mapping.get(self.path, UpdateResponse())
|
| + response.Reply(self, True, request)
|
| +
|
| +
|
| +class ChildFinished(Exception):
|
| + """Child exit exception."""
|
| +
|
| + def __init__(self, pid):
|
| + Exception.__init__(self)
|
| + self.pid = pid
|
| + self.status = None
|
| +
|
| + def __str__(self):
|
| + return 'Process %d exited status %d' % (self.pid, self.status)
|
| +
|
| + def __nonzero__(self):
|
| + return self.status is not None
|
| +
|
| + def SigHandler(self, signum, frame):
|
| + """Handle SIGCHLD signal, and retreive client exit code."""
|
| +
|
| + while True:
|
| + try:
|
| + (pid, status) = os.waitpid(-1, os.WNOHANG)
|
| + except OSError, e:
|
| + if e.args[0] != errno.ECHILD:
|
| + raise e
|
| +
|
| + # TODO(pstew): returning here won't help -- SocketServer gets EINTR
|
| + return
|
| +
|
| + if pid == self.pid:
|
| + if os.WIFEXITED(status):
|
| + self.status = os.WEXITSTATUS(status)
|
| + else:
|
| + self.status = 255
|
| + raise self
|
| +
|
| +
|
| +def main(argv):
|
| + usage = 'usage: %prog'
|
| + parser = optparse.OptionParser(usage=usage)
|
| + parser.add_option('--board', dest='board', default=None,
|
| + help='Board platform type')
|
| + parser.add_option('--force-mismatch', dest='force_mismatch', default=False,
|
| + action='store_true',
|
| + help='Upgrade even if client arch does not match')
|
| + parser.add_option('--from', dest='src', default=None,
|
| + help='Source image to install')
|
| + parser.add_option('--image-name', dest='image_name',
|
| + default=DEFAULT_IMAGE_NAME,
|
| + help='Filename within image directory to load')
|
| + parser.add_option('--port', dest='port', default=8081, type='int',
|
| + help='TCP port to serve from and tunnel through')
|
| + parser.add_option('--remote', dest='remote', default=None,
|
| + help='Remote device-under-test IP address')
|
| + parser.add_option('--server-only', dest='server_only', default=False,
|
| + action='store_true', help='Do not start client')
|
| + parser.add_option('--verbose', dest='verbose', default=False,
|
| + action='store_true', help='Display running commands')
|
| +
|
| + (options, args) = parser.parse_args(argv)
|
| +
|
| + cros_env = CrosEnv(verbose=options.verbose)
|
| +
|
| + if not options.board:
|
| + options.board = cros_env.GetDefaultBoard()
|
| +
|
| + if not options.src:
|
| + options.src = cros_env.GetLatestImage(options.board)
|
| + if options.src is None:
|
| + parser.error('No --from argument given and no default image found')
|
| +
|
| + cros_env.Info('Performing update from %s' % options.src)
|
| +
|
| + if not os.path.exists(options.src):
|
| + parser.error('Path %s does not exist' % options.src)
|
| +
|
| + if os.path.isdir(options.src):
|
| + image_directory = options.src
|
| + image_file = os.path.join(options.src, options.image_name)
|
| +
|
| + if not os.path.exists(image_file):
|
| + parser.error('Image file %s does not exist' % image_file)
|
| + else:
|
| + image_file = options.src
|
| + image_directory = os.path.dirname(options.src)
|
| +
|
| + if options.remote:
|
| + cros_env.SetRemote(options.remote)
|
| + rel = cros_env.GetRemoteRelease()
|
| + if not rel:
|
| + cros_env.Fatal('Could not retrieve remote lsb-release')
|
| + board = rel.get('CHROMEOS_RELEASE_BOARD', '(None)')
|
| + if board != options.board and not options.force_mismatch:
|
| + cros_env.Error('Board %s does not match expected %s' %
|
| + (board, options.board))
|
| + cros_env.Error('(Use --force-mismatch option to override this)')
|
| + cros_env.Fatal()
|
| +
|
| + elif not options.server_only:
|
| + parser.error('Either --server-only must be specified or '
|
| + '--remote=<client> needs to be given')
|
| +
|
| + update_file = os.path.join(image_directory, UPDATE_FILENAME)
|
| + stateful_file = os.path.join(image_directory, STATEFUL_FILENAME)
|
| +
|
| + if (not cros_env.GenerateUpdatePayload(image_file, update_file) or
|
| + not cros_env.BuildStateful(image_file, stateful_file)):
|
| + cros_env.Fatal()
|
| +
|
| + cros_env.CreateServer(options.port, update_file, stateful_file)
|
| +
|
| + exit_status = 1
|
| + if options.server_only:
|
| + child = None
|
| + else:
|
| + # Start an "image-to-live" instance that will pull bits from the server
|
| + child = os.fork()
|
| + if child:
|
| + signal.signal(signal.SIGCHLD, ChildFinished(child).SigHandler)
|
| + else:
|
| + try:
|
| + time.sleep(SERVER_STARTUP_WAIT)
|
| + if cros_env.StartClient(options.port):
|
| + exit_status = 0
|
| + except KeyboardInterrupt:
|
| + cros_env.Error('Client Exiting on Control-C')
|
| + except:
|
| + cros_env.Error('Exception in client code:')
|
| + traceback.print_exc(file=sys.stdout)
|
| +
|
| + cros_env.ssh_cmd.Cleanup()
|
| + cros_env.Info('Client exiting with status %d' % exit_status)
|
| + sys.exit(exit_status)
|
| +
|
| + try:
|
| + cros_env.StartServer()
|
| + except KeyboardInterrupt:
|
| + cros_env.Info('Server Exiting on Control-C')
|
| + exit_status = 0
|
| + except ChildFinished, e:
|
| + cros_env.Info('Server Exiting on Client Exit (%d)' % e.status)
|
| + exit_status = e.status
|
| + child = None
|
| + except:
|
| + cros_env.Error('Exception in server code:')
|
| + traceback.print_exc(file=sys.stdout)
|
| +
|
| + if child:
|
| + os.kill(child, 15)
|
| +
|
| + cros_env.Info('Server exiting with status %d' % exit_status)
|
| + sys.exit(exit_status)
|
| +
|
| +
|
| +if __name__ == '__main__':
|
| + main(sys.argv)
|
|
|