Source code for cerium.service

# Licensed to the White Turing under one or more
# contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The SFC licenses this file
# to you 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.

from typing import Any, Dict, Union

from .commands import _PATH, Commands
from .exceptions import DeviceConnectionException
from .utils import free_port, is_connectable, merge_dict


class BaseService(Commands):
    '''Object that manages the starting and stopping of the AndroidDriver.'''

    def __init__(self, executable: _PATH = 'default', port: Union[int, str] = 5037, env: Dict = None) -> None:
        super(BaseService, self).__init__(executable)
        self.port = port
        if self.port == 0:
            self.port = free_port()
        self.options = {'env': env}

    @property
    def service_tcp(self) -> str:
        '''Gets the TCP of the Service.'''
        return f"tcp://localhost:{self.port}"

    def service_args(self) -> NotImplemented:
        raise NotImplemented(
            "This method needs to be implemented in a sub class")

    def _build_cmd(self, args: Union[list, tuple]) -> str:
        '''Build command.'''
        cmd = [self.path]
        cmd.extend(self.service_args())
        cmd.extend(args)
        return cmd

    def _execute(self, *args: str, **kwargs: Any) -> tuple:
        '''Execute command.'''
        return self.execute(args=args, options=merge_dict(self.options, kwargs)).communicate()

    def start(self) -> None:
        '''Starts the Service.'''
        self._execute('start-server')

    def stop(self) -> None:
        '''Stops the service.'''
        self._execute('kill-server')

    def restart(self) -> None:
        '''Restart the server if it is running.'''
        self.stop()
        self.start()

    def version(self) -> str:
        '''Show the version number of Android Debug Bridge.'''
        output, _ = self._execute('version')
        return output.splitlines()[0].split()[-1]

    def devices(self) -> list:
        '''List connected devices.'''
        output, _ = self._execute('devices')
        return output.split()[4::2]

    def devices_l(self) -> Dict:
        '''List connected devices (-l for long output).'''
        output, _ = self._execute('devices', '-l')
        devices = output.split()[4::6]
        models = output.split()[7::6]
        return dict(zip(devices, models))

    def connect(self, host: str = '192.168.0.3', port: Union[int, str] = 5555) -> None:
        '''Connect to a device via TCP/IP directly.'''
        self.device_sn = f'{host}:{port}'
        if not is_connectable(host, port):
            raise ConnectionError(f'Cannot connect to {self.device_sn}.')
        self._execute('connect', self.device_sn)

    def disconnect(self, host: str = '192.168.0.3', port: Union[int, str] = 5555) -> None:
        '''Disconnect from given TCP/IP device [default port=5555].'''
        self.device_sn = None
        self._execute('disconnect', f'{host}:{port}')

    def disconnect_all(self) -> None:
        '''Disconnect all.'''
        self.device_sn = None
        self._execute('disconnect')

    def get_state(self) -> str:
        '''offline | bootloader | device'''
        output, error = self._execute('get-state')
        if error:
            raise DeviceConnectionException(error.split(':', 1)[-1].strip())
        return output.strip()


class Service(BaseService):
    '''Object that manages the starting and stopping of the AndroidDriver.'''

    def __init__(self, executable_path: _PATH = 'default', port: Union[int, str] = 5037, env: Dict = None, service_args: Union[list, tuple] = None) -> None:
        '''Creates a new instance of the Service.

        Args:
            executable_path: Path to the AndroidDriver.
            port: Port the service is running on.
            env: Environment variables.
            service_args: List of args to pass to the androiddriver service.
        '''

        self._service_args = service_args or []

        super(Service, self).__init__(executable_path, port=port, env=env)

    def service_args(self) -> str:
        '''Parameters when starting the service.'''
        return ['-P', str(self.port)] + self._service_args