# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Implements commands for running/interacting with Fuchsia on an emulator."""

import json
import logging
import os
import subprocess
import sys
import tempfile

import boot_data
import common
import ffx_session
import pkg_repo
import target


class EmuTarget(target.Target):
  LOCAL_ADDRESS = 'localhost'

  def __init__(self, out_dir, target_cpu, logs_dir):
    """out_dir: The directory which will contain the files that are
                   generated to support the emulator deployment.
    target_cpu: The emulated target CPU architecture.
                Can be 'x64' or 'arm64'."""

    super(EmuTarget, self).__init__(out_dir, target_cpu, logs_dir)
    self._emu_process = None
    self._pkg_repo = None
    self._target_context = None
    self._ffx_target = None

  def __enter__(self):
    return self

  def _BuildCommand(self):
    """Build the command that will be run to start Fuchsia in the emulator."""
    pass

  def _SetEnv(self):
    return os.environ.copy()

  def Start(self):
    if common.IsRunningUnattended() and not self._HasNetworking():
      # Bots may accumulate stale manually-added targets with the same address
      # as the one to be added here. Preemtively remove any unknown targets at
      # this address before starting the emulator and adding it as a target.
      self._ffx_runner.remove_stale_targets('127.0.0.1')
    emu_command = self._BuildCommand()
    logging.debug(' '.join(emu_command))

    # Zircon sends debug logs to serial port (see kernel.serial=legacy flag
    # above). Serial port is redirected to a file through emulator stdout.
    # If runner_logs are not enabled, we output the kernel serial log
    # to a temporary file, and print that out if we are unable to connect to
    # the emulator guest, to make it easier to diagnose connectivity issues.
    temporary_log_file = None
    if self._log_manager.IsLoggingEnabled():
      stdout = self._log_manager.Open('serial_log')
    else:
      temporary_log_file = tempfile.NamedTemporaryFile('w')
      stdout = temporary_log_file

    self.LogProcessStatistics('proc_stat_start_log')
    self.LogSystemStatistics('system_statistics_start_log')

    self._emu_process = subprocess.Popen(emu_command,
                                         stdin=open(os.devnull),
                                         stdout=stdout,
                                         stderr=subprocess.STDOUT,
                                         env=self._SetEnv())
    try:
      self._ConnectToTarget()
      self.LogProcessStatistics('proc_stat_ready_log')
    except target.FuchsiaTargetException:
      self._DisconnectFromTarget()
      if temporary_log_file:
        logging.info('Kernel logs:\n' +
                     open(temporary_log_file.name, 'r').read())
      raise

  def GetFfxTarget(self):
    assert self._ffx_target
    return self._ffx_target

  def Stop(self):
    try:
      self._DisconnectFromTarget()
      self._Shutdown()
    finally:
      self.LogProcessStatistics('proc_stat_end_log')
      self.LogSystemStatistics('system_statistics_end_log')
      super(EmuTarget, self).Stop()

  def GetPkgRepo(self):
    if not self._pkg_repo:
      self._pkg_repo = pkg_repo.ManagedPkgRepo(self)

    return self._pkg_repo

  def _Shutdown(self):
    """Shuts down the emulator."""
    raise NotImplementedError()

  def _HasNetworking(self):
    """Returns `True` if the emulator will be started with networking (e.g.,
    TUN/TAP emulated networking).
    """
    raise NotImplementedError()

  def _IsEmuStillRunning(self):
    """Returns `True` if the emulator is still running."""
    raise NotImplementedError()

  def _GetEndpoint(self):
    raise NotImplementedError()

  def _ConnectToTarget(self):
    with_network = self._HasNetworking()
    if not with_network:
      # The target was started without networking, so tell ffx how to find it.
      logging.info('Connecting to Fuchsia using ffx.')
      _, host_ssh_port = self._GetEndpoint()
      self._target_context = self._ffx_runner.scoped_target_context(
          '127.0.0.1', host_ssh_port)
      self._ffx_target = self._target_context.__enter__()
      self._ffx_target.wait(common.ATTACH_RETRY_SECONDS)
    super(EmuTarget, self)._ConnectToTarget()
    if with_network:
      # Interact with the target via its node name, which ffx should now know
      # about.
      address, port = self._GetNetworkAddress()
      self._ffx_target = ffx_session.FfxTarget(
          self._ffx_runner, self._ffx_runner.get_node_name(address, port))

  def _DisconnectFromTarget(self):
    self._ffx_target = None
    if self._target_context:
      self._target_context.__exit__(None, None, None)
      self._target_context = None
    super(EmuTarget, self)._DisconnectFromTarget()

  def _GetSshConfigPath(self):
    return boot_data.GetSSHConfigPath()

  def LogSystemStatistics(self, log_file_name):
    self._LaunchSubprocessWithLogs(['top', '-b', '-n', '1'], log_file_name)
    self._LaunchSubprocessWithLogs(['ps', '-ax'], log_file_name)

  def LogProcessStatistics(self, log_file_name):
    self._LaunchSubprocessWithLogs(['cat', '/proc/stat'], log_file_name)

  def _LaunchSubprocessWithLogs(self, command, log_file_name):
    """Launch a subprocess and redirect stdout and stderr to log_file_name.
    Command will not be run if logging directory is not set."""

    if not self._log_manager.IsLoggingEnabled():
      return
    log = self._log_manager.Open(log_file_name)
    subprocess.call(command,
                    stdin=open(os.devnull),
                    stdout=log,
                    stderr=subprocess.STDOUT)
