| # |
| # Copyright (C) 2017 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| |
| import cmd |
| import ctypes |
| import datetime |
| import imp # Python v2 compatibility |
| import logging |
| import multiprocessing |
| import multiprocessing.pool |
| import os |
| import re |
| import shutil |
| import signal |
| import socket |
| import sys |
| import tempfile |
| import threading |
| import time |
| import urlparse |
| |
| from host_controller import common |
| from host_controller.command_processor import command_adb |
| from host_controller.command_processor import command_build |
| from host_controller.command_processor import command_config |
| from host_controller.command_processor import command_config_local |
| from host_controller.command_processor import command_copy |
| from host_controller.command_processor import command_device |
| from host_controller.command_processor import command_dut |
| from host_controller.command_processor import command_exit |
| from host_controller.command_processor import command_fastboot |
| from host_controller.command_processor import command_fetch |
| from host_controller.command_processor import command_flash |
| from host_controller.command_processor import command_gsispl |
| from host_controller.command_processor import command_info |
| from host_controller.command_processor import command_lease |
| from host_controller.command_processor import command_list |
| from host_controller.command_processor import command_password |
| from host_controller.command_processor import command_release |
| from host_controller.command_processor import command_retry |
| from host_controller.command_processor import command_request |
| from host_controller.command_processor import command_repack |
| from host_controller.command_processor import command_sheet |
| from host_controller.command_processor import command_shell |
| from host_controller.command_processor import command_sleep |
| from host_controller.command_processor import command_test |
| from host_controller.command_processor import command_reproduce |
| from host_controller.command_processor import command_upload |
| from host_controller.build import build_info |
| from host_controller.build import build_provider_ab |
| from host_controller.build import build_provider_gcs |
| from host_controller.build import build_provider_local_fs |
| from host_controller.build import build_provider_pab |
| from host_controller.utils.ipc import file_lock |
| from host_controller.utils.ipc import shared_dict |
| from host_controller.vti_interface import vti_endpoint_client |
| from vts.runners.host import logger |
| from vts.utils.python.common import cmd_utils |
| |
| COMMAND_PROCESSORS = [ |
| command_adb.CommandAdb, |
| command_build.CommandBuild, |
| command_config.CommandConfig, |
| command_config_local.CommandConfigLocal, |
| command_copy.CommandCopy, |
| command_device.CommandDevice, |
| command_dut.CommandDUT, |
| command_exit.CommandExit, |
| command_fastboot.CommandFastboot, |
| command_fetch.CommandFetch, |
| command_flash.CommandFlash, |
| command_gsispl.CommandGsispl, |
| command_info.CommandInfo, |
| command_lease.CommandLease, |
| command_list.CommandList, |
| command_password.CommandPassword, |
| command_release.CommandRelease, |
| command_retry.CommandRetry, |
| command_request.CommandRequest, |
| command_repack.CommandRepack, |
| command_sheet.CommandSheet, |
| command_shell.CommandShell, |
| command_sleep.CommandSleep, |
| command_test.CommandTest, |
| command_reproduce.CommandReproduce, |
| command_upload.CommandUpload, |
| ] |
| |
| |
| class NonDaemonizedProcess(multiprocessing.Process): |
| """Process class which is not daemonized.""" |
| |
| def _get_daemon(self): |
| return False |
| |
| def _set_daemon(self, value): |
| pass |
| |
| daemon = property(_get_daemon, _set_daemon) |
| |
| |
| class NonDaemonizedPool(multiprocessing.pool.Pool): |
| """Pool class which is not daemonized.""" |
| |
| Process = NonDaemonizedProcess |
| |
| |
| def JobMain(vti_address, in_queue, out_queue, device_status, password, hosts): |
| """Main() for a child process that executes a leased job. |
| |
| Currently, lease jobs must use VTI (not TFC). |
| |
| Args: |
| vti_client: VtiEndpointClient needed to create Console. |
| in_queue: Queue to get new jobs. |
| out_queue: Queue to put execution results. |
| device_status: SharedDict, contains device status information. |
| shared between processes. |
| password: multiprocessing.managers.ValueProxy, a proxy instance of a |
| string(ctypes.c_char_p) represents the password which is |
| to be passed to the prompt when executing certain command |
| as root user. |
| hosts: A list of HostController objects. Needed for the device command. |
| """ |
| |
| def SigTermHandler(signum, frame): |
| """Signal handler for exiting pool process explicitly. |
| |
| Added to resolve orphaned pool process issue. |
| """ |
| sys.exit(0) |
| |
| signal.signal(signal.SIGTERM, SigTermHandler) |
| |
| vti_client = vti_endpoint_client.VtiEndpointClient(vti_address) |
| console = Console(vti_client, None, None, hosts, job_pool=True) |
| console.device_status = device_status |
| console.password = password |
| multiprocessing.util.Finalize(console, console.__exit__, exitpriority=0) |
| |
| while True: |
| command = in_queue.get() |
| if command == "exit": |
| break |
| elif command == "lease": |
| filepath, kwargs = vti_client.LeaseJob(socket.gethostname(), True) |
| logging.debug("Job %s -> %s" % (os.getpid(), kwargs)) |
| if filepath is not None: |
| # TODO: redirect console output and add |
| # console command to access them. |
| |
| console._build_provider[ |
| "pab"] = build_provider_pab.BuildProviderPAB() |
| console._build_provider[ |
| "gcs"] = build_provider_gcs.BuildProviderGCS() |
| |
| for serial in kwargs["serial"]: |
| console.ChangeDeviceState( |
| serial, common._DEVICE_STATUS_DICT["use"]) |
| print_to_console = True |
| if not print_to_console: |
| sys.stdout = out |
| sys.stderr = err |
| |
| ret, gcs_log_url = console.ProcessConfigurableScript( |
| os.path.join(os.getcwd(), "host_controller", "campaigns", |
| filepath), **kwargs) |
| if ret: |
| job_status = "complete" |
| else: |
| job_status = "infra-err" |
| |
| vti_client.StopHeartbeat(job_status, gcs_log_url) |
| logging.info("Job execution complete. " |
| "Setting job status to {}".format(job_status)) |
| |
| if not print_to_console: |
| sys.stdout = sys.__stdout__ |
| sys.stderr = sys.__stderr__ |
| |
| for serial in kwargs["serial"]: |
| console.ChangeDeviceState( |
| serial, common._DEVICE_STATUS_DICT["ready"]) |
| |
| del console._build_provider["pab"] |
| del console._build_provider["gcs"] |
| console.fetch_info = {} |
| console._detailed_fetch_info = {} |
| else: |
| logging.error("Unknown job command %s", command) |
| |
| out_queue.put("exit") |
| |
| |
| class Console(cmd.Cmd): |
| """The console for host controllers. |
| |
| Attributes: |
| command_processors: dict of string:BaseCommandProcessor, |
| map between command string and command processors. |
| device_image_info: dict containing info about device image files. |
| prompt: The prompt string at the beginning of each command line. |
| test_result: dict containing info about the last test result. |
| test_suite_info: dict containing info about test suite package files. |
| tools_info: dict containing info about custom tool files. |
| scheduler_thread: dict containing threading.Thread instances(s) that |
| update configs regularly. |
| _build_provider_pab: The BuildProviderPAB used to download artifacts. |
| _vti_address: string, VTI service URI. |
| _vti_client: VtiEndpoewrClient, used to upload data to a test |
| scheduling infrastructure. |
| _tfc_client: The TfcClient that the host controllers connect to. |
| _hosts: A list of HostController objects. |
| _in_file: The input file object. |
| _out_file: The output file object. |
| _serials: A list of string where each string is a device serial. |
| _device_status: SharedDict, shared with process pool. |
| contains status data on each devices. |
| _job_pool: bool, True if Console is created from job pool process |
| context. |
| _password: multiprocessing.managers.ValueProxy, a proxy instance of a |
| string(ctypes.c_char_p) represents the password which is |
| to be passed to the prompt when executing certain command |
| as root user. |
| _manager: SyncManager. an instance of a manager for shared objects and |
| values between processes. |
| _vtslab_version: string, contains version information of vtslab package. |
| (<git commit timestamp>:<git commit hash value>) |
| _detailed_fetch_info: A nested dict, holds the branch and target value |
| of the device, gsi, or test suite artifact. |
| _file_lock: FileLock, an instance used for synchronizing the devices' |
| use when the automated self-update happens. |
| """ |
| |
| def __init__(self, |
| vti_endpoint_client, |
| tfc, |
| pab, |
| host_controllers, |
| vti_address=None, |
| in_file=sys.stdin, |
| out_file=sys.stdout, |
| job_pool=False, |
| password=None): |
| """Initializes the attributes and the parsers.""" |
| # cmd.Cmd is old-style class. |
| cmd.Cmd.__init__(self, stdin=in_file, stdout=out_file) |
| self._build_provider = {} |
| self._job_pool = job_pool |
| if not self._job_pool: |
| self._build_provider["pab"] = pab |
| self._build_provider["gcs"] = build_provider_gcs.BuildProviderGCS() |
| self._build_provider[ |
| "local_fs"] = build_provider_local_fs.BuildProviderLocalFS() |
| self._build_provider["ab"] = build_provider_ab.BuildProviderAB() |
| self._manager = multiprocessing.Manager() |
| self._device_status = shared_dict.SharedDict(self._manager) |
| self._password = self._manager.Value(ctypes.c_char_p, password) |
| try: |
| with open(common._VTSLAB_VERSION_TXT, "r") as file: |
| self._vtslab_version = file.readline().strip() |
| file.close() |
| logging.info("VTSLAB version: %s" % self._vtslab_version) |
| except IOError as e: |
| logging.exception(e) |
| logging.error("Version info missing in vtslab package. " |
| "Setting version as %s", |
| common._VTSLAB_VERSION_DEFAULT_VALUE) |
| self._vtslab_version = common._VTSLAB_VERSION_DEFAULT_VALUE |
| self._logfile_upload_path = "" |
| |
| self._vti_endpoint_client = vti_endpoint_client |
| self._vti_address = vti_address |
| self._tfc_client = tfc |
| self._hosts = host_controllers |
| self._in_file = in_file |
| self._out_file = out_file |
| self.prompt = "> " |
| self.command_processors = {} |
| self.device_image_info = build_info.BuildInfo() |
| self.test_result = {} |
| self.test_suite_info = build_info.BuildInfo() |
| self.tools_info = build_info.BuildInfo() |
| self.fetch_info = {} |
| self._detailed_fetch_info = {} |
| self.test_results = {} |
| self._file_lock = file_lock.FileLock() |
| self.repack_dest_path = "" |
| |
| if common._ANDROID_SERIAL in os.environ: |
| self._serials = [os.environ[common._ANDROID_SERIAL]] |
| else: |
| self._serials = [] |
| |
| self.InitCommandModuleParsers() |
| self.SetUpCommandProcessors() |
| |
| tempdir_base = os.path.join(os.getcwd(), "tmp") |
| if not os.path.exists(tempdir_base): |
| os.mkdir(tempdir_base) |
| self._tmpdir_default = tempfile.mkdtemp(dir=tempdir_base) |
| self._tmp_logdir = tempfile.mkdtemp(dir=tempdir_base) |
| if not self._job_pool: |
| self._logfile_path = logger.setupTestLogger( |
| self._tmp_logdir, create_symlink=False) |
| |
| def __exit__(self): |
| """Finalizes the build provider attributes explicitly when exited.""" |
| for bp in self._build_provider: |
| self._build_provider[bp].__del__() |
| if os.path.exists(self._tmp_logdir): |
| shutil.rmtree(self._tmp_logdir) |
| |
| @property |
| def job_pool(self): |
| """getter for self._job_pool""" |
| return self._job_pool |
| |
| @property |
| def device_status(self): |
| """getter for self._device_status""" |
| return self._device_status |
| |
| @device_status.setter |
| def device_status(self, device_status): |
| """setter for self._device_status""" |
| self._device_status = device_status |
| |
| @property |
| def build_provider(self): |
| """getter for self._build_provider""" |
| return self._build_provider |
| |
| @property |
| def tmpdir_default(self): |
| """getter for self._password""" |
| return self._tmpdir_default |
| |
| @tmpdir_default.setter |
| def tmpdir_default(self, tmpdir): |
| """getter for self._password""" |
| self._tmpdir_default = tmpdir |
| |
| @property |
| def password(self): |
| """getter for self._password""" |
| return self._password |
| |
| @password.setter |
| def password(self, password): |
| """getter for self._password""" |
| self._password = password |
| |
| @property |
| def logfile_path(self): |
| """getter for self._logfile_path""" |
| return self._logfile_path |
| |
| @property |
| def tmp_logdir(self): |
| """getter for self._tmp_logdir""" |
| return self._tmp_logdir |
| |
| @property |
| def vti_endpoint_client(self): |
| """getter for self._vti_endpoint_client""" |
| return self._vti_endpoint_client |
| |
| @property |
| def vtslab_version(self): |
| """getter for self._vtslab_version""" |
| return self._vtslab_version |
| |
| @property |
| def detailed_fetch_info(self): |
| return self._detailed_fetch_info |
| |
| def UpdateFetchInfo(self, artifact_type): |
| if artifact_type in common._ARTIFACT_TYPE_LIST: |
| self._detailed_fetch_info[artifact_type] = {} |
| self._detailed_fetch_info[artifact_type].update(self.fetch_info) |
| else: |
| logging.error("Unrecognized artifact type: %s", artifact_type) |
| |
| @property |
| def file_lock(self): |
| """getter for self._file_lock""" |
| return self._file_lock |
| |
| def ChangeDeviceState(self, serial, state): |
| """Changes a device's state and (un)locks the file lock if necessary. |
| |
| Args: |
| serial: string, serial number of a device. |
| state: int, devices' status value pre-defined in |
| common._DEVICE_STATUS_DICT. |
| Returns: |
| True if the state change and locking/unlocking are successful. |
| False otherwise. |
| """ |
| if state == common._DEVICE_STATUS_DICT["use"]: |
| ret = self._file_lock.LockDevice(serial) |
| if ret == False: |
| return False |
| |
| current_status = self.device_status[serial] |
| self.device_status[serial] = state |
| |
| if (current_status in (common._DEVICE_STATUS_DICT["use"], |
| common._DEVICE_STATUS_DICT["error"]) |
| and current_status != state): |
| self._file_lock.UnlockDevice(serial) |
| |
| def InitCommandModuleParsers(self): |
| """Init all console command modules""" |
| for name in dir(self): |
| if name.startswith('_Init') and name.endswith('Parser'): |
| attr_func = getattr(self, name) |
| if hasattr(attr_func, '__call__'): |
| attr_func() |
| |
| def SetUpCommandProcessors(self): |
| """Sets up all command processors.""" |
| for command_processor in COMMAND_PROCESSORS: |
| cp = command_processor() |
| cp._SetUp(self) |
| do_text = "do_%s" % cp.command |
| help_text = "help_%s" % cp.command |
| setattr(self, do_text, cp._Run) |
| setattr(self, help_text, cp._Help) |
| self.command_processors[cp.command] = cp |
| |
| def TearDown(self): |
| """Removes all command processors.""" |
| for command_processor in self.command_processors.itervalues(): |
| command_processor._TearDown() |
| self.command_processors.clear() |
| self.__exit__() |
| |
| def FormatString(self, format_string): |
| """Replaces variables with the values in the console's dictionaries. |
| |
| Args: |
| format_string: The string containing variables enclosed in {}. |
| |
| Returns: |
| The formatted string. |
| |
| Raises: |
| KeyError if a variable is not found in the dictionaries or the |
| value is empty. |
| """ |
| |
| def ReplaceVariable(match): |
| """Replacement functioon for re.sub(). |
| |
| replaces string encased in braces with values in the console's dict. |
| |
| Args: |
| match: regex, used for extracting the variable name. |
| |
| Returns: |
| string value corresponding to the input variable name. |
| """ |
| name = match.group(1) |
| if name in ("build_id", "branch", "target", "account_id"): |
| value = self.fetch_info[name] |
| elif name in ("result_full", "result_zip", "suite_plan", |
| "suite_name"): |
| value = self.test_result[name] |
| elif "timestamp" in name: |
| current_datetime = datetime.datetime.now() |
| value_date = current_datetime.strftime("%Y%m%d") |
| value_time = current_datetime.strftime("%H%M%S") |
| if "_date" in name: |
| value = value_date |
| elif "_time" in name: |
| value = value_time |
| elif "_year" in name: |
| value = value_date[0:4] |
| elif "_month" in name: |
| value = value_date[4:6] |
| elif "_day" in name: |
| value = value_date[6:8] |
| else: |
| value = "%s-%s" % (value_date, value_time) |
| elif name in ("hc_log", "hc_log_file", "hc_log_upload_path"): |
| # hc_log: full abs path to the current process's infra log. |
| # hc_log_file: infra log file name, with no path information. |
| # hc_log_upload_path: path of the infra log file in GCS. |
| value = self._logfile_path |
| if name == "hc_log_file": |
| value = os.path.basename(value) |
| elif name == "hc_log_upload_path": |
| value = self._logfile_upload_path |
| elif name in ("repack_path"): |
| value = self.repack_dest_path |
| self.repack_dest_path = "" |
| elif name in ("hostname"): |
| value = socket.gethostname() |
| elif "." in name and name.split(".")[0] in self.command_processors: |
| command, arg = name.split(".") |
| try: |
| value = self.command_processors[command].arg_buffer[arg] |
| except KeyError as e: |
| logging.exception(e) |
| value = "" |
| if value is None: |
| value = "" |
| else: |
| value = None |
| |
| if value is None: |
| raise KeyError(name) |
| |
| return value |
| |
| return re.sub("{([^}]+)}", ReplaceVariable, format_string) |
| |
| def ProcessScript(self, script_file_path): |
| """Processes a .py script file. |
| |
| A script file implements a function which emits a list of console |
| commands to execute. That function emits an empty list or None if |
| no more command needs to be processed. |
| |
| Args: |
| script_file_path: string, the path of a script file (.py file). |
| |
| Returns: |
| True if successful; False otherwise |
| """ |
| if not script_file_path.endswith(".py"): |
| logging.error("Script file is not .py file: %s" % script_file_path) |
| return False |
| |
| script_module = imp.load_source('script_module', script_file_path) |
| |
| commands = script_module.EmitConsoleCommands() |
| if commands: |
| for command in commands: |
| ret = self.onecmd(command) |
| if ret == False: |
| return False |
| return True |
| |
| def ProcessConfigurableScript(self, script_file_path, **kwargs): |
| """Processes a .py script file. |
| |
| A script file implements a function which emits a list of console |
| commands to execute. That function emits an empty list or None if |
| no more command needs to be processed. |
| |
| Args: |
| script_file_path: string, the path of a script file (.py file). |
| kwargs: extra args for the interface function defined in |
| the script file. |
| |
| Returns: |
| True if successful; False otherwise |
| String which represents URL to the upload infra log file. |
| """ |
| if script_file_path and not script_file_path.endswith(".py"): |
| script_file_path += ".py" |
| |
| if not script_file_path.endswith(".py"): |
| logging.error("Script file is not .py file: %s", script_file_path) |
| return False |
| |
| ret = True |
| |
| self._logfile_path, file_handler = logger.addLogFile(self._tmp_logdir) |
| src = self.FormatString("{hc_log}") |
| dest = self.FormatString( |
| "gs://vts-report/infra_log/{hostname}/%s_{timestamp}/{hc_log_file}" |
| % kwargs["build_target"]) |
| self._logfile_upload_path = dest |
| |
| script_module = imp.load_source('script_module', script_file_path) |
| |
| commands = script_module.EmitConsoleCommands(**kwargs) |
| logging.info("Command list: %s", commands) |
| if commands: |
| logging.info("Console commands: %s", commands) |
| for command in commands: |
| ret = self.onecmd(command) |
| if ret == False: |
| break |
| else: |
| ret = False |
| |
| file_handler.flush() |
| infra_log_upload_command = "upload" |
| infra_log_upload_command += " --src=%s" % src |
| infra_log_upload_command += " --dest=%s" % dest |
| for serial in kwargs["serial"]: |
| if self.device_status[serial] == common._DEVICE_STATUS_DICT[ |
| "error"]: |
| self.vti_endpoint_client.SetJobStatusFromLeasedTo("bootup-err") |
| break |
| if not self.vti_endpoint_client.CheckBootUpStatus(): |
| infra_log_upload_command += (" --report_path=gs://vts-report/" |
| "suite_result/{timestamp_year}/" |
| "{timestamp_month}/{timestamp_day}") |
| suite_name, plan_name = kwargs["test_name"].split("/") |
| infra_log_upload_command += ( |
| " --result_from_suite=%s" % suite_name) |
| infra_log_upload_command += (" --result_from_plan=%s" % plan_name) |
| self.onecmd(infra_log_upload_command) |
| if self.GetSerials(): |
| self.onecmd("device --update=stop") |
| logging.getLogger().removeHandler(file_handler) |
| os.remove(self._logfile_path) |
| return (ret != False), dest |
| |
| def _Print(self, string): |
| """Prints a string and a new line character. |
| |
| Args: |
| string: The string to be printed. |
| """ |
| self._out_file.write(string + "\n") |
| |
| def _PrintObjects(self, objects, attr_names): |
| """Shows objects as a table. |
| |
| Args: |
| object: The objects to be shown, one object in a row. |
| attr_names: The attributes to be shown, one attribute in a column. |
| """ |
| width = [len(name) for name in attr_names] |
| rows = [attr_names] |
| for dev_info in objects: |
| attrs = [ |
| _ToPrintString(getattr(dev_info, name, "")) |
| for name in attr_names |
| ] |
| rows.append(attrs) |
| for index, attr in enumerate(attrs): |
| width[index] = max(width[index], len(attr)) |
| |
| for row in rows: |
| self._Print(" ".join( |
| attr.ljust(width[index]) for index, attr in enumerate(row))) |
| |
| def DownloadTestResources(self, request_id): |
| """Download all of the test resources for a TFC request id. |
| |
| Args: |
| request_id: int, TFC request id |
| """ |
| resources = self._tfc_client.TestResourceList(request_id) |
| for resource in resources: |
| self.DownloadTestResource(resource['url']) |
| |
| def DownloadTestResource(self, url): |
| """Download a test resource with build provider, given a url. |
| |
| Args: |
| url: a resource locator (not necessarily HTTP[s]) |
| with the scheme specifying the build provider. |
| """ |
| parsed = urlparse.urlparse(url) |
| path = (parsed.netloc + parsed.path).split('/') |
| if parsed.scheme == "pab": |
| if len(path) != 5: |
| logging.error("Invalid pab resource locator: %s", url) |
| return |
| account_id, branch, target, build_id, artifact_name = path |
| cmd = ("fetch" |
| " --type=pab" |
| " --account_id=%s" |
| " --branch=%s" |
| " --target=%s" |
| " --build_id=%s" |
| " --artifact_name=%s") % (account_id, branch, target, |
| build_id, artifact_name) |
| self.onecmd(cmd) |
| elif parsed.scheme == "ab": |
| if len(path) != 4: |
| logging.error("Invalid ab resource locator: %s", url) |
| return |
| branch, target, build_id, artifact_name = path |
| cmd = ("fetch" |
| "--type=ab" |
| " --branch=%s" |
| " --target=%s" |
| " --build_id=%s" |
| " --artifact_name=%s") % (branch, target, build_id, |
| artifact_name) |
| self.onecmd(cmd) |
| elif parsed.scheme == gcs: |
| cmd = "fetch --type=gcs --path=%s" % url |
| self.onecmd(cmd) |
| else: |
| logging.error("Invalid URL: %s", url) |
| |
| def SetSerials(self, serials): |
| """Sets the default serial numbers for flashing and testing. |
| |
| Args: |
| serials: A list of strings, the serial numbers. |
| """ |
| self._serials = serials |
| |
| def FlashImgPackage(self, package_path_gcs): |
| """Fetches a repackaged image set from GCS and flashes to the device(s). |
| |
| Args: |
| package_path_gcs: GCS URL to the packaged img zip file. May contain |
| the GSI imgs. |
| """ |
| self.onecmd("fetch --type=gcs --path=%s --full_device_images=True" % |
| package_path_gcs) |
| if common.FULL_ZIPFILE not in self.device_image_info: |
| logging.error("Failed to fetch the given file: %s", |
| package_path_gcs) |
| return False |
| |
| if not self._serials: |
| logging.error("Please specify the serial number(s) of target " |
| "device(s) for flashing.") |
| return False |
| |
| campaign_common = imp.load_source( |
| 'campaign_common', |
| os.path.join(os.getcwd(), "host_controller", "campaigns", |
| "campaign_common.py")) |
| flash_command_list = [] |
| |
| for serial in self._serials: |
| flash_commands = [] |
| cmd_utils.ExecuteOneShellCommand( |
| "adb -s %s reboot bootloader" % serial) |
| _, stderr, retcode = cmd_utils.ExecuteOneShellCommand( |
| "fastboot -s %s getvar product" % serial) |
| if retcode == 0: |
| res = stderr.splitlines()[0].rstrip() |
| if ":" in res: |
| product = res.split(":")[1].strip() |
| elif "waiting for %s" % serial in res: |
| res = stderr.splitlines()[1].rstrip() |
| product = res.split(":")[1].strip() |
| else: |
| product = "error" |
| else: |
| product = "error" |
| logging.info("Device %s product type: %s", serial, product) |
| if product in campaign_common.FLASH_COMMAND_EMITTER: |
| flash_commands.append( |
| campaign_common.FLASH_COMMAND_EMITTER[product]( |
| serial, repacked_imageset=True)) |
| elif product != "error": |
| flash_commands.append( |
| "flash --current --serial %s --skip-vbmeta=True" % serial) |
| else: |
| logging.error( |
| "Device %s does not exist. Omitting the flashing " |
| "to the device.", serial) |
| continue |
| flash_command_list.append(flash_commands) |
| |
| ret = self.onecmd(flash_command_list) |
| if ret == False: |
| logging.error("Flash failed on device %s.", self._serials) |
| else: |
| logging.info("Flash succeeded on device %s.", self._serials) |
| |
| return ret |
| |
| def GetSerials(self): |
| """Returns the serial numbers saved in the console. |
| |
| Returns: |
| A list of strings, the serial numbers. |
| """ |
| return self._serials |
| |
| def ResetSerials(self): |
| """Clears all the serial numbers set to this console obj.""" |
| self._serials = [] |
| |
| def JobThread(self): |
| """Job thread which monitors and uploads results.""" |
| thread = threading.currentThread() |
| while getattr(thread, "keep_running", True): |
| time.sleep(1) |
| |
| if self._job_pool: |
| self._job_pool.close() |
| self._job_pool.terminate() |
| self._job_pool.join() |
| |
| def StartJobThreadAndProcessPool(self): |
| """Starts a background thread to control leased jobs.""" |
| self._job_in_queue = multiprocessing.Queue() |
| self._job_out_queue = multiprocessing.Queue() |
| self._job_pool = NonDaemonizedPool( |
| common._MAX_LEASED_JOBS, JobMain, |
| (self._vti_address, self._job_in_queue, self._job_out_queue, |
| self._device_status, self._password, self._hosts)) |
| |
| self._job_thread = threading.Thread(target=self.JobThread) |
| self._job_thread.daemon = True |
| self._job_thread.start() |
| |
| def StopJobThreadAndProcessPool(self): |
| """Terminates the thread and processes that runs the leased job.""" |
| if hasattr(self, "_job_thread"): |
| self._job_thread.keep_running = False |
| self._job_thread.join() |
| |
| def WaitForJobsToExit(self): |
| """Wait for the running jobs to complete before exiting HC.""" |
| if self._job_pool: |
| pool_process_count = common._MAX_LEASED_JOBS |
| for _ in range(common._MAX_LEASED_JOBS): |
| self._job_in_queue.put("exit") |
| |
| while True: |
| response = self._job_out_queue.get() |
| if response == "exit": |
| pool_process_count -= 1 |
| if pool_process_count <= 0: |
| break |
| |
| # @Override |
| def onecmd(self, line, depth=1, ret_out_queue=None): |
| """Executes command(s) and prints any exception. |
| |
| Parallel execution only for 2nd-level list element. |
| |
| Args: |
| line: a list of string or string which keeps the command to run. |
| """ |
| if not line: |
| return |
| |
| if type(line) == list: |
| if depth == 1: # 1 to use multi-threading |
| jobs = [] |
| ret_queue = multiprocessing.Queue() |
| for sub_command in line: |
| p = multiprocessing.Process( |
| target=self.onecmd, |
| args=( |
| sub_command, |
| depth + 1, |
| ret_queue, |
| )) |
| jobs.append(p) |
| p.start() |
| for job in jobs: |
| job.join() |
| |
| ret_cmd_list = True |
| while not ret_queue.empty(): |
| ret_from_subprocess = ret_queue.get() |
| ret_cmd_list = ret_cmd_list and ret_from_subprocess |
| if ret_cmd_list == False: |
| return False |
| else: |
| for sub_command in line: |
| ret_cmd_list = self.onecmd(sub_command, depth + 1) |
| if ret_cmd_list == False and ret_out_queue: |
| ret_out_queue.put(False) |
| return False |
| return |
| |
| logging.info("Command: %s", line) |
| try: |
| ret_cmd = cmd.Cmd.onecmd(self, line) |
| if ret_cmd == False and ret_out_queue: |
| ret_out_queue.put(ret_cmd) |
| return ret_cmd |
| except Exception as e: |
| self._Print("%s: %s" % (type(e).__name__, e)) |
| if ret_out_queue: |
| ret_out_queue.put(False) |
| return False |
| |
| # @Override |
| def emptyline(self): |
| """Ignores empty lines.""" |
| pass |
| |
| # @Override |
| def default(self, line): |
| """Handles unrecognized commands. |
| |
| Returns: |
| True if receives EOF; otherwise delegates to default handler. |
| """ |
| if line == "EOF": |
| return self.do_exit(line) |
| return cmd.Cmd.default(self, line) |
| |
| |
| def _ToPrintString(obj): |
| """Converts an object to printable string on console. |
| |
| Args: |
| obj: The object to be printed. |
| """ |
| if isinstance(obj, (list, tuple, set)): |
| return ",".join(str(x) for x in obj) |
| return str(obj) |