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

import contextlib
import json
import os
import pathlib
import socket
import platform
import sys
import subprocess
import struct
import time

sys.path.insert(1, os.path.join(os.path.dirname(__file__), '..'))
from util import build_utils

# Use a unix abstract domain socket:
# https://man7.org/linux/man-pages/man7/unix.7.html#:~:text=abstract:
SOCKET_ADDRESS = '\0chromium_build_server_socket'
BUILD_SERVER_ENV_VARIABLE = 'INVOKED_BY_BUILD_SERVER'

ADD_TASK = 'add_task'
QUERY_BUILD = 'query_build'
POLL_HEARTBEAT = 'poll_heartbeat'
REGISTER_BUILDER = 'register_builder'
CANCEL_BUILD = 'cancel_build'

SERVER_SCRIPT = pathlib.Path(
    build_utils.DIR_SOURCE_ROOT
) / 'build' / 'android' / 'fast_local_dev_server.py'


def MaybeRunCommand(name, argv, stamp_file, force, experimental=False):
  """Returns True if the command was successfully sent to the build server."""

  if platform.system() == "Darwin":
    # Build server does not support Mac.
    return False

  # When the build server runs a command, it sets this environment variable.
  # This prevents infinite recursion where the script sends a request to the
  # build server, then the build server runs the script, and then the script
  # sends another request to the build server.
  if BUILD_SERVER_ENV_VARIABLE in os.environ:
    return False

  autoninja_tty = os.environ.get('AUTONINJA_STDOUT_NAME')
  autoninja_build_id = os.environ.get('AUTONINJA_BUILD_ID')
  if experimental and not autoninja_tty:
    raise RuntimeError('experimental_build_server=true is set but autoninja '
                       'is not patched. Please make sure you are using a '
                       'patched autoninja script.')

  with contextlib.closing(socket.socket(socket.AF_UNIX)) as sock:
    try:
      sock.connect(SOCKET_ADDRESS)
    except socket.error as e:
      # [Errno 111] Connection refused. Either the server has not been started
      #             or the server is not currently accepting new connections.
      if e.errno == 111:
        if force:
          raise RuntimeError(
              '\n\nBuild server is not running and '
              'android_static_analysis="build_server" is set.\nPlease run '
              'this command in a separate terminal:\n\n'
              '$ build/android/fast_local_dev_server.py\n\n') from None
        return False
      raise e

    SendMessage(
        sock,
        json.dumps({
            'name': name,
            'message_type': ADD_TASK,
            'cmd': argv,
            'cwd': os.getcwd(),
            'tty': autoninja_tty,
            'build_id': autoninja_build_id,
            'experimental': experimental,
            'stamp_file': stamp_file,
        }).encode('utf8'))

  # Siso needs the stamp file to be created in order for the build step to
  # complete. If the task fails when the build server runs it, the build server
  # will delete the stamp file so that it will be run again next build.
  pathlib.Path(stamp_file).touch()
  return True


def SendMessage(sock: socket.socket, message: bytes):
  size_prefix = struct.pack('!i', len(message))
  sock.sendall(size_prefix + message)


def ReceiveMessage(sock: socket.socket):
  size_prefix = b''
  remaining = 4  # sizeof(int)
  while remaining > 0:
    data = sock.recv(remaining)
    if not data:
      return None
    remaining -= len(data)
    size_prefix += data
  remaining, = struct.unpack('!i', size_prefix)
  received = []
  while remaining > 0:
    data = sock.recv(remaining)
    if not data:
      break
    received.append(data)
    remaining -= len(data)
  return b''.join(received)
