Module appium.webdriver.appium_service
Expand source code
#!/usr/bin/env python
# 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 os
import subprocess as sp
import sys
import time
from typing import Any, List, Optional, TypeVar, Union
import urllib3
DEFAULT_HOST = '127.0.0.1'
DEFAULT_PORT = 4723
STARTUP_TIMEOUT_MS = 60000
MAIN_SCRIPT_PATH = 'appium/build/lib/main.js'
STATUS_URL = '/wd/hub/status'
def find_executable(executable: str) -> Optional[str]:
path = os.environ['PATH']
paths = path.split(os.pathsep)
base, ext = os.path.splitext(executable)
if sys.platform == 'win32' and not ext:
executable = executable + '.exe'
if os.path.isfile(executable):
return executable
for p in paths:
full_path = os.path.join(p, executable)
if os.path.isfile(full_path):
return full_path
return None
def poll_url(host: str, port: int, path: str, timeout_ms: int) -> bool:
time_started_sec = time.time()
while time.time() < time_started_sec + timeout_ms / 1000.0:
try:
conn = urllib3.PoolManager(timeout=1.0)
resp = conn.request('HEAD', f'http://{host}:{port}{path}')
if resp.status < 400:
return True
except Exception:
pass
time.sleep(1.0)
return False
class AppiumServiceError(RuntimeError):
pass
T = TypeVar('T', bound='AppiumService')
class AppiumService:
def __init__(self) -> None:
self._process: Optional[sp.Popen] = None
self._cmd: Optional[List] = None
def _get_node(self) -> str:
if not hasattr(self, '_node_executable'):
self._node_executable = find_executable('node')
if self._node_executable is None:
raise AppiumServiceError('NodeJS main executable cannot be found. ' +
'Make sure it is installed and present in PATH')
return self._node_executable
def _get_npm(self) -> str:
if not hasattr(self, '_npm_executable'):
self._npm_executable = find_executable('npm.cmd' if sys.platform == 'win32' else 'npm')
if self._npm_executable is None:
raise AppiumServiceError('Node Package Manager executable cannot be found. ' +
'Make sure it is installed and present in PATH')
return self._npm_executable
def _get_main_script(self) -> Union[str, bytes]:
if not hasattr(self, '_main_script'):
for args in [['root', '-g'], ['root']]:
try:
modules_root = sp.check_output([self._get_npm()] + args).strip().decode('utf-8')
if os.path.exists(os.path.join(modules_root, MAIN_SCRIPT_PATH)):
self._main_script: Union[str, bytes] = os.path.join(modules_root, MAIN_SCRIPT_PATH)
break
except sp.CalledProcessError:
continue
if not hasattr(self, '_main_script'):
try:
self._main_script = sp.check_output(
[self._get_node(),
'-e',
'console.log(require.resolve("{}"))'.format(MAIN_SCRIPT_PATH)]).strip()
except sp.CalledProcessError as e:
raise AppiumServiceError(e.output) from e
return self._main_script
@staticmethod
def _parse_port(args: List[str]) -> int:
for idx, arg in enumerate(args or []):
if arg in ('--port', '-p') and idx < len(args) - 1:
return int(args[idx + 1])
return DEFAULT_PORT
@staticmethod
def _parse_host(args: List[str]) -> str:
for idx, arg in enumerate(args or []):
if arg in ('--address', '-a') and idx < len(args) - 1:
return args[idx + 1]
return DEFAULT_HOST
def start(self, **kwargs: Any) -> sp.Popen:
"""Starts Appium service with given arguments.
The service will be forcefully restarted if it is already running.
Keyword Args:
env (dict): Environment variables mapping. The default system environment,
which is inherited from the parent process is assigned by default.
node (str): The full path to the main NodeJS executable. The service will try
to retrieve it automatically by default.
stdout: Check on the documentation for subprocess.Popen for more details.
The default value is subprocess.PIPE.
stderr: Check on the documentation for subprocess.Popen for more details.
The default value is subprocess.PIPE.
timeout_ms (int): The maximum time to wait until Appium process starts listening
for HTTP connections. If set to zero or a negative number then no wait will be applied.
60000 ms by default
main_script (str): The full path to the main Appium executable
(usually located this is build/lib/main.js). If this is not set
then the service tries to detect the path automatically.
args (str): List of Appium arguments (all must be strings). Check on
https://appium.io/docs/en/writing-running-appium/server-args/ for more details
about possible arguments and their values.
Returns:
subprocess.Popen instance: You can use Popen.communicate interface
or stderr/stdout properties of the instance
(stdout/stderr must not be set to None in such case)
in order to retrieve the actual process output.
"""
self.stop()
env = kwargs['env'] if 'env' in kwargs else None
node = kwargs['node'] if 'node' in kwargs else self._get_node()
stdout = kwargs['stdout'] if 'stdout' in kwargs else sp.PIPE
stderr = kwargs['stderr'] if 'stderr' in kwargs else sp.PIPE
timeout_ms = int(kwargs['timeout_ms']) if 'timeout_ms' in kwargs else STARTUP_TIMEOUT_MS
main_script = kwargs['main_script'] if 'main_script' in kwargs else self._get_main_script()
args = [node, main_script]
if 'args' in kwargs:
args.extend(kwargs['args'])
self._cmd = args
self._process = sp.Popen(args=args, stdout=stdout, stderr=stderr, env=env)
host = self._parse_host(args)
port = self._parse_port(args)
error_msg: Optional[str] = None
if not self.is_running or (timeout_ms > 0 and not poll_url(host, port, STATUS_URL, timeout_ms)):
error_msg = f'Appium has failed to start on {host}:{port} within {timeout_ms}ms timeout'
if error_msg is not None:
if stderr == sp.PIPE:
err_output = self._process.stderr.read()
if err_output:
error_msg += f'\nOriginal error: {str(err_output)}'
self.stop()
raise AppiumServiceError(error_msg)
return self._process
def stop(self) -> bool:
"""Stops Appium service if it is running.
The call will be ignored if the service is not running
or has been already stopped.
Returns:
bool: `True` if the service was running before being stopped
"""
is_terminated = False
if self.is_running:
self._process.terminate() # type: ignore
is_terminated = True
self._process = None
self._cmd = None
return is_terminated
@property
def is_running(self) -> bool:
"""Check if the service is running.
Returns:
bool: `True` or `False`
"""
return self._process is not None and self._process.poll() is None
@property
def is_listening(self) -> bool:
"""Check if the service is listening on the given/default host/port.
The fact, that the service is running, does not always mean it is listening.
the default host/port values can be customized by providing --address/--port
command line arguments while starting the service.
Returns:
bool: `True` if the service is running and listening on the given/default host/port
"""
if not self.is_running or self._cmd is None:
return False
host = self._parse_host(self._cmd)
port = self._parse_port(self._cmd)
return self.is_running and poll_url(host, port, STATUS_URL, 1000)
if __name__ == '__main__':
assert(find_executable('node') is not None)
assert(find_executable('npm') is not None)
service = AppiumService()
service.start(args=['--address', '127.0.0.1', '-p', str(DEFAULT_PORT)])
# service.start(args=['--address', '127.0.0.1', '-p', '80'], timeout_ms=2000)
assert(service.is_running)
assert(service.is_listening)
service.stop()
assert(not service.is_running)
assert(not service.is_listening)
Functions
def find_executable(executable)
-
Expand source code
def find_executable(executable: str) -> Optional[str]: path = os.environ['PATH'] paths = path.split(os.pathsep) base, ext = os.path.splitext(executable) if sys.platform == 'win32' and not ext: executable = executable + '.exe' if os.path.isfile(executable): return executable for p in paths: full_path = os.path.join(p, executable) if os.path.isfile(full_path): return full_path return None
def poll_url(host, port, path, timeout_ms)
-
Expand source code
def poll_url(host: str, port: int, path: str, timeout_ms: int) -> bool: time_started_sec = time.time() while time.time() < time_started_sec + timeout_ms / 1000.0: try: conn = urllib3.PoolManager(timeout=1.0) resp = conn.request('HEAD', f'http://{host}:{port}{path}') if resp.status < 400: return True except Exception: pass time.sleep(1.0) return False
Classes
class AppiumService
-
Expand source code
class AppiumService: def __init__(self) -> None: self._process: Optional[sp.Popen] = None self._cmd: Optional[List] = None def _get_node(self) -> str: if not hasattr(self, '_node_executable'): self._node_executable = find_executable('node') if self._node_executable is None: raise AppiumServiceError('NodeJS main executable cannot be found. ' + 'Make sure it is installed and present in PATH') return self._node_executable def _get_npm(self) -> str: if not hasattr(self, '_npm_executable'): self._npm_executable = find_executable('npm.cmd' if sys.platform == 'win32' else 'npm') if self._npm_executable is None: raise AppiumServiceError('Node Package Manager executable cannot be found. ' + 'Make sure it is installed and present in PATH') return self._npm_executable def _get_main_script(self) -> Union[str, bytes]: if not hasattr(self, '_main_script'): for args in [['root', '-g'], ['root']]: try: modules_root = sp.check_output([self._get_npm()] + args).strip().decode('utf-8') if os.path.exists(os.path.join(modules_root, MAIN_SCRIPT_PATH)): self._main_script: Union[str, bytes] = os.path.join(modules_root, MAIN_SCRIPT_PATH) break except sp.CalledProcessError: continue if not hasattr(self, '_main_script'): try: self._main_script = sp.check_output( [self._get_node(), '-e', 'console.log(require.resolve("{}"))'.format(MAIN_SCRIPT_PATH)]).strip() except sp.CalledProcessError as e: raise AppiumServiceError(e.output) from e return self._main_script @staticmethod def _parse_port(args: List[str]) -> int: for idx, arg in enumerate(args or []): if arg in ('--port', '-p') and idx < len(args) - 1: return int(args[idx + 1]) return DEFAULT_PORT @staticmethod def _parse_host(args: List[str]) -> str: for idx, arg in enumerate(args or []): if arg in ('--address', '-a') and idx < len(args) - 1: return args[idx + 1] return DEFAULT_HOST def start(self, **kwargs: Any) -> sp.Popen: """Starts Appium service with given arguments. The service will be forcefully restarted if it is already running. Keyword Args: env (dict): Environment variables mapping. The default system environment, which is inherited from the parent process is assigned by default. node (str): The full path to the main NodeJS executable. The service will try to retrieve it automatically by default. stdout: Check on the documentation for subprocess.Popen for more details. The default value is subprocess.PIPE. stderr: Check on the documentation for subprocess.Popen for more details. The default value is subprocess.PIPE. timeout_ms (int): The maximum time to wait until Appium process starts listening for HTTP connections. If set to zero or a negative number then no wait will be applied. 60000 ms by default main_script (str): The full path to the main Appium executable (usually located this is build/lib/main.js). If this is not set then the service tries to detect the path automatically. args (str): List of Appium arguments (all must be strings). Check on https://appium.io/docs/en/writing-running-appium/server-args/ for more details about possible arguments and their values. Returns: subprocess.Popen instance: You can use Popen.communicate interface or stderr/stdout properties of the instance (stdout/stderr must not be set to None in such case) in order to retrieve the actual process output. """ self.stop() env = kwargs['env'] if 'env' in kwargs else None node = kwargs['node'] if 'node' in kwargs else self._get_node() stdout = kwargs['stdout'] if 'stdout' in kwargs else sp.PIPE stderr = kwargs['stderr'] if 'stderr' in kwargs else sp.PIPE timeout_ms = int(kwargs['timeout_ms']) if 'timeout_ms' in kwargs else STARTUP_TIMEOUT_MS main_script = kwargs['main_script'] if 'main_script' in kwargs else self._get_main_script() args = [node, main_script] if 'args' in kwargs: args.extend(kwargs['args']) self._cmd = args self._process = sp.Popen(args=args, stdout=stdout, stderr=stderr, env=env) host = self._parse_host(args) port = self._parse_port(args) error_msg: Optional[str] = None if not self.is_running or (timeout_ms > 0 and not poll_url(host, port, STATUS_URL, timeout_ms)): error_msg = f'Appium has failed to start on {host}:{port} within {timeout_ms}ms timeout' if error_msg is not None: if stderr == sp.PIPE: err_output = self._process.stderr.read() if err_output: error_msg += f'\nOriginal error: {str(err_output)}' self.stop() raise AppiumServiceError(error_msg) return self._process def stop(self) -> bool: """Stops Appium service if it is running. The call will be ignored if the service is not running or has been already stopped. Returns: bool: `True` if the service was running before being stopped """ is_terminated = False if self.is_running: self._process.terminate() # type: ignore is_terminated = True self._process = None self._cmd = None return is_terminated @property def is_running(self) -> bool: """Check if the service is running. Returns: bool: `True` or `False` """ return self._process is not None and self._process.poll() is None @property def is_listening(self) -> bool: """Check if the service is listening on the given/default host/port. The fact, that the service is running, does not always mean it is listening. the default host/port values can be customized by providing --address/--port command line arguments while starting the service. Returns: bool: `True` if the service is running and listening on the given/default host/port """ if not self.is_running or self._cmd is None: return False host = self._parse_host(self._cmd) port = self._parse_port(self._cmd) return self.is_running and poll_url(host, port, STATUS_URL, 1000)
Instance variables
var is_listening
-
Check if the service is listening on the given/default host/port.
The fact, that the service is running, does not always mean it is listening. the default host/port values can be customized by providing –address/–port command line arguments while starting the service.
Returns
bool
True
if the service is running and listening on the given/default host/port
Expand source code
@property def is_listening(self) -> bool: """Check if the service is listening on the given/default host/port. The fact, that the service is running, does not always mean it is listening. the default host/port values can be customized by providing --address/--port command line arguments while starting the service. Returns: bool: `True` if the service is running and listening on the given/default host/port """ if not self.is_running or self._cmd is None: return False host = self._parse_host(self._cmd) port = self._parse_port(self._cmd) return self.is_running and poll_url(host, port, STATUS_URL, 1000)
var is_running
-
Check if the service is running.
Returns
bool
True
orFalse
Expand source code
@property def is_running(self) -> bool: """Check if the service is running. Returns: bool: `True` or `False` """ return self._process is not None and self._process.poll() is None
Methods
def start(self, **kwargs)
-
Starts Appium service with given arguments.
The service will be forcefully restarted if it is already running.
Keyword Args: env (dict): Environment variables mapping. The default system environment, which is inherited from the parent process is assigned by default. node (str): The full path to the main NodeJS executable. The service will try to retrieve it automatically by default. stdout: Check on the documentation for subprocess.Popen for more details. The default value is subprocess.PIPE. stderr: Check on the documentation for subprocess.Popen for more details. The default value is subprocess.PIPE. timeout_ms (int): The maximum time to wait until Appium process starts listening for HTTP connections. If set to zero or a negative number then no wait will be applied. 60000 ms by default main_script (str): The full path to the main Appium executable (usually located this is build/lib/main.js). If this is not set then the service tries to detect the path automatically. args (str): List of Appium arguments (all must be strings). Check on https://appium.io/docs/en/writing-running-appium/server-args/ for more details about possible arguments and their values.
Returns
subprocess.Popen
instance
:You
can
use
Popen.communicate
interface
- or stderr/stdout properties of the instance (stdout/stderr must not be set to None in such case) in order to retrieve the actual process output.
Expand source code
def start(self, **kwargs: Any) -> sp.Popen: """Starts Appium service with given arguments. The service will be forcefully restarted if it is already running. Keyword Args: env (dict): Environment variables mapping. The default system environment, which is inherited from the parent process is assigned by default. node (str): The full path to the main NodeJS executable. The service will try to retrieve it automatically by default. stdout: Check on the documentation for subprocess.Popen for more details. The default value is subprocess.PIPE. stderr: Check on the documentation for subprocess.Popen for more details. The default value is subprocess.PIPE. timeout_ms (int): The maximum time to wait until Appium process starts listening for HTTP connections. If set to zero or a negative number then no wait will be applied. 60000 ms by default main_script (str): The full path to the main Appium executable (usually located this is build/lib/main.js). If this is not set then the service tries to detect the path automatically. args (str): List of Appium arguments (all must be strings). Check on https://appium.io/docs/en/writing-running-appium/server-args/ for more details about possible arguments and their values. Returns: subprocess.Popen instance: You can use Popen.communicate interface or stderr/stdout properties of the instance (stdout/stderr must not be set to None in such case) in order to retrieve the actual process output. """ self.stop() env = kwargs['env'] if 'env' in kwargs else None node = kwargs['node'] if 'node' in kwargs else self._get_node() stdout = kwargs['stdout'] if 'stdout' in kwargs else sp.PIPE stderr = kwargs['stderr'] if 'stderr' in kwargs else sp.PIPE timeout_ms = int(kwargs['timeout_ms']) if 'timeout_ms' in kwargs else STARTUP_TIMEOUT_MS main_script = kwargs['main_script'] if 'main_script' in kwargs else self._get_main_script() args = [node, main_script] if 'args' in kwargs: args.extend(kwargs['args']) self._cmd = args self._process = sp.Popen(args=args, stdout=stdout, stderr=stderr, env=env) host = self._parse_host(args) port = self._parse_port(args) error_msg: Optional[str] = None if not self.is_running or (timeout_ms > 0 and not poll_url(host, port, STATUS_URL, timeout_ms)): error_msg = f'Appium has failed to start on {host}:{port} within {timeout_ms}ms timeout' if error_msg is not None: if stderr == sp.PIPE: err_output = self._process.stderr.read() if err_output: error_msg += f'\nOriginal error: {str(err_output)}' self.stop() raise AppiumServiceError(error_msg) return self._process
def stop(self)
-
Stops Appium service if it is running.
The call will be ignored if the service is not running or has been already stopped.
Returns
bool
True
if the service was running before being stopped
Expand source code
def stop(self) -> bool: """Stops Appium service if it is running. The call will be ignored if the service is not running or has been already stopped. Returns: bool: `True` if the service was running before being stopped """ is_terminated = False if self.is_running: self._process.terminate() # type: ignore is_terminated = True self._process = None self._cmd = None return is_terminated
class AppiumServiceError (*args, **kwargs)
-
Unspecified run-time error.
Expand source code
class AppiumServiceError(RuntimeError): pass
Ancestors
- builtins.RuntimeError
- builtins.Exception
- builtins.BaseException