зеркало из https://github.com/microsoft/lisa.git
refactoring code for process
1. use spurplus to get unique interface for local and remote processes 2. rename executable to tool 3. other minor changes
This commit is contained in:
Родитель
191e2af054
Коммит
09e060b84b
|
@ -1,7 +1,6 @@
|
|||
from lisa import CaseMetadata, SuiteMetadata
|
||||
from lisa.core.testSuite import TestSuite
|
||||
from lisa.executable import Uname
|
||||
from lisa.executable.echo import Echo
|
||||
from lisa.tool import Echo, Uname
|
||||
from lisa.util.logger import log
|
||||
|
||||
|
||||
|
|
|
@ -4,10 +4,13 @@ import random
|
|||
from timeit import default_timer as timer
|
||||
from typing import Dict, Optional, Type, TypeVar, cast
|
||||
|
||||
from lisa.core.executable import Executable
|
||||
from lisa.core.sshConnection import SshConnection
|
||||
from lisa.executable import Echo, Uname
|
||||
import spur
|
||||
import spurplus
|
||||
|
||||
from lisa.core.tool import Tool
|
||||
from lisa.tool import Echo, Uname
|
||||
from lisa.util import constants
|
||||
from lisa.util.connectionInfo import ConnectionInfo
|
||||
from lisa.util.executableResult import ExecutableResult
|
||||
from lisa.util.logger import log
|
||||
from lisa.util.process import Process
|
||||
|
@ -28,7 +31,12 @@ class Node:
|
|||
self.isDefault = isDefault
|
||||
self.isRemote = isRemote
|
||||
self.spec = spec
|
||||
self.connection: Optional[SshConnection] = None
|
||||
self.connection_info: Optional[ConnectionInfo] = None
|
||||
self.tempFolder: str = ""
|
||||
if self.isRemote:
|
||||
self.shell: Optional[spurplus.SshShell] = None
|
||||
else:
|
||||
self.shell: Optional[spur.LocalShell] = None
|
||||
|
||||
self._isInitialized: bool = False
|
||||
self._isLinux: bool = True
|
||||
|
@ -37,7 +45,7 @@ class Node:
|
|||
self.kernelVersion: str = ""
|
||||
self.hardwarePlatform: str = ""
|
||||
|
||||
self.tools: Dict[Type[Executable], Executable] = dict()
|
||||
self.tools: Dict[Type[Tool], Tool] = dict()
|
||||
for tool_class in self.builtinTools:
|
||||
self.tools[tool_class] = tool_class(self)
|
||||
|
||||
|
@ -59,17 +67,54 @@ class Node:
|
|||
)
|
||||
return node
|
||||
|
||||
def setConnectionInfo(self, **kwargs: str) -> None:
|
||||
if self.connection is not None:
|
||||
def setConnectionInfo(
|
||||
self,
|
||||
address: str = "",
|
||||
port: int = 22,
|
||||
publicAddress: str = "",
|
||||
publicPort: int = 22,
|
||||
username: str = "root",
|
||||
password: str = "",
|
||||
privateKeyFile: str = "",
|
||||
) -> None:
|
||||
if self.connection_info is not None:
|
||||
raise Exception(
|
||||
"node is set connection information already, cannot set again"
|
||||
)
|
||||
self.connection = SshConnection(**kwargs)
|
||||
|
||||
if not address and not publicAddress:
|
||||
raise Exception("at least one of address and publicAddress need to be set")
|
||||
elif not address:
|
||||
address = publicAddress
|
||||
elif not publicAddress:
|
||||
publicAddress = address
|
||||
|
||||
if not port and not publicPort:
|
||||
raise Exception("at least one of port and publicPort need to be set")
|
||||
elif not port:
|
||||
port = publicPort
|
||||
elif not publicPort:
|
||||
publicPort = port
|
||||
|
||||
self.connection_info = ConnectionInfo(
|
||||
publicAddress, publicPort, username, password, privateKeyFile,
|
||||
)
|
||||
self.internalAddress = address
|
||||
self.internalPort = port
|
||||
|
||||
def getTool(self, tool_type: Type[T]) -> T:
|
||||
tool = cast(T, self.tools.get(tool_type))
|
||||
if tool is None:
|
||||
raise Exception(f"Tool {tool_type.__name__} is not found on node")
|
||||
# the Tool is not installed on current node, try to install it.
|
||||
tool = cast(Tool, T(self))
|
||||
if not tool.isInstalled:
|
||||
if tool.canInstall:
|
||||
tool.install()
|
||||
if not tool.isInstalled:
|
||||
raise Exception(
|
||||
f"Tool {tool_type.__name__} is not found on node, "
|
||||
f"and cannot be installed or is install failed."
|
||||
)
|
||||
return tool
|
||||
|
||||
def execute(self, cmd: str, noErrorLog: bool = False) -> ExecutableResult:
|
||||
|
@ -106,24 +151,32 @@ class Node:
|
|||
log.info(f"initialized Windows node '{self.name}', ")
|
||||
|
||||
def _execute(self, cmd: str, noErrorLog: bool = False) -> ExecutableResult:
|
||||
cmd_id = str(random.randint(0, 10000))
|
||||
cmd_prefix = f"cmd[{str(random.randint(0, 10000))}]"
|
||||
start_timer = timer()
|
||||
log.debug(f"cmd[{cmd_id}] remote({self.isRemote}) {cmd}")
|
||||
if self.isRemote:
|
||||
# remote
|
||||
if self.connection is None:
|
||||
raise Exception(f"cmd[{cmd_id}] remote node has no connection info")
|
||||
result: ExecutableResult = self.connection.execute(cmd, cmd_id=cmd_id)
|
||||
else:
|
||||
# local
|
||||
process = Process()
|
||||
with process:
|
||||
process.start(cmd, cmd_id=cmd_id, noErrorLog=noErrorLog)
|
||||
result = process.waitResult()
|
||||
log.debug(f"{cmd_prefix}remote({self.isRemote}) '{cmd}'")
|
||||
|
||||
if self.shell is None:
|
||||
if self.isRemote:
|
||||
assert self.connection_info is not None
|
||||
self.shell = spurplus.connect_with_retries(
|
||||
self.connection_info.address,
|
||||
port=self.connection_info.port,
|
||||
username=self.connection_info.username,
|
||||
password=self.connection_info.password,
|
||||
private_key_file=self.connection_info.privateKeyFile,
|
||||
missing_host_key=spur.ssh.MissingHostKey.accept,
|
||||
)
|
||||
else:
|
||||
self.shell = spur.LocalShell()
|
||||
|
||||
process = Process(cmd_prefix, self.shell)
|
||||
process.start(cmd, noErrorLog=noErrorLog)
|
||||
result = process.waitResult()
|
||||
|
||||
end_timer = timer()
|
||||
log.info(f"cmd[{cmd_id}] executed with {end_timer - start_timer:.3f} sec")
|
||||
log.info(f"{cmd_prefix}executed with {end_timer - start_timer:.3f} sec")
|
||||
return result
|
||||
|
||||
def close(self) -> None:
|
||||
if self.connection is not None:
|
||||
self.connection.close()
|
||||
if self.shell and isinstance(self.shell, spurplus.SshShell):
|
||||
self.shell.close()
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import paramiko # type: ignore
|
||||
|
||||
from lisa.util.connectionInfo import ConnectionInfo
|
||||
from lisa.util.executableResult import ExecutableResult
|
||||
from lisa.util.logger import log_lines
|
||||
|
||||
|
||||
class SshConnection:
|
||||
def __init__(
|
||||
self,
|
||||
address: str = "",
|
||||
port: int = 22,
|
||||
publicAddress: str = "",
|
||||
publicPort: int = 22,
|
||||
username: str = "root",
|
||||
password: str = "",
|
||||
privateKeyFile: str = "",
|
||||
) -> None:
|
||||
self.address = address
|
||||
self.port = port
|
||||
self.publicAddress = publicAddress
|
||||
self.publicPort = publicPort
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.privateKeyFile = privateKeyFile
|
||||
|
||||
if not self.address and not self.publicAddress:
|
||||
raise Exception("at least one of address and publicAddress need to be set")
|
||||
elif not self.address:
|
||||
self.address = self.publicAddress
|
||||
elif not self.publicAddress:
|
||||
self.publicAddress = self.address
|
||||
|
||||
if not self.port and not self.publicPort:
|
||||
raise Exception("at least one of port and publicPort need to be set")
|
||||
elif not self.port:
|
||||
self.port = self.publicPort
|
||||
elif not self.publicPort:
|
||||
self.publicPort = self.port
|
||||
|
||||
self._connectionInfo = ConnectionInfo(
|
||||
self.address, self.port, self.username, self.password, self.privateKeyFile
|
||||
)
|
||||
self._publicConnectionInfo = ConnectionInfo(
|
||||
self.publicAddress,
|
||||
self.publicPort,
|
||||
self.username,
|
||||
self.password,
|
||||
self.privateKeyFile,
|
||||
)
|
||||
|
||||
self._connection: Optional[paramiko.SSHClient] = None
|
||||
self._publicConnection: Optional[paramiko.SSHClient] = None
|
||||
|
||||
self._isConnected: bool = False
|
||||
self._isPublicConnected: bool = False
|
||||
|
||||
@property
|
||||
def connectionInfo(self) -> ConnectionInfo:
|
||||
return self._connectionInfo
|
||||
|
||||
@property
|
||||
def publicConnectionInfo(self) -> ConnectionInfo:
|
||||
return self._publicConnectionInfo
|
||||
|
||||
def execute(
|
||||
self, cmd: str, noErrorLog: bool = False, cmd_id: str = ""
|
||||
) -> ExecutableResult:
|
||||
client = self.connect()
|
||||
_, stdout_file, stderr_file = client.exec_command(cmd)
|
||||
exit_code: int = stdout_file.channel.recv_exit_status()
|
||||
|
||||
stdout: str = stdout_file.read().decode("utf-8").strip()
|
||||
log_lines(logging.INFO, stdout, prefix=f"cmd[{cmd_id}]stdout: ")
|
||||
stderr: str = stderr_file.read().decode("utf-8").strip()
|
||||
if noErrorLog:
|
||||
log_level = logging.INFO
|
||||
else:
|
||||
log_level = logging.ERROR
|
||||
# fix, cannot print them together
|
||||
log_lines(log_level, stderr, prefix=f"cmd[{cmd_id}]stderr: ")
|
||||
result = ExecutableResult(stdout, stderr, exit_code)
|
||||
|
||||
return result
|
||||
|
||||
def connect(self, isPublic: bool = True) -> paramiko.SSHClient:
|
||||
if isPublic:
|
||||
connection = self._publicConnection
|
||||
connectionInfo = self.publicConnectionInfo
|
||||
else:
|
||||
connection = self._connection
|
||||
connectionInfo = self.connectionInfo
|
||||
if connection is None:
|
||||
connection = paramiko.SSHClient()
|
||||
connection.set_missing_host_key_policy(paramiko.client.AutoAddPolicy)
|
||||
connection.connect(
|
||||
connectionInfo.address,
|
||||
port=connectionInfo.port,
|
||||
username=connectionInfo.username,
|
||||
password=connectionInfo.password,
|
||||
key_filename=connectionInfo.privateKeyFile,
|
||||
look_for_keys=False,
|
||||
)
|
||||
if isPublic:
|
||||
self._publicConnection = connection
|
||||
else:
|
||||
self._connection = connection
|
||||
return connection
|
||||
|
||||
def close(self) -> None:
|
||||
if self._connection is not None:
|
||||
self._connection.close()
|
||||
self._connection = None
|
||||
if self._publicConnection is not None:
|
||||
self._publicConnection.close()
|
||||
self._publicConnection = None
|
|
@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
|||
from lisa.core.node import Node
|
||||
|
||||
|
||||
class Executable(ABC):
|
||||
class Tool(ABC):
|
||||
def __init__(self, node: Node) -> None:
|
||||
self.node: Node = node
|
||||
self.initialize()
|
||||
|
@ -30,7 +30,7 @@ class Executable(ABC):
|
|||
def installed(self) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
def install(self) -> None:
|
||||
def install(self) -> bool:
|
||||
pass
|
||||
|
||||
def run(
|
||||
|
@ -42,5 +42,5 @@ class Executable(ABC):
|
|||
|
||||
|
||||
class ExecutableException(Exception):
|
||||
def __init__(self, exe: Executable, message: str):
|
||||
def __init__(self, exe: Tool, message: str):
|
||||
self.message = f"{exe.command}: {message}"
|
|
@ -1,5 +0,0 @@
|
|||
from lisa.core.executable import Executable
|
||||
|
||||
|
||||
class Ping(Executable):
|
||||
pass
|
18
lisa/main.py
18
lisa/main.py
|
@ -1,4 +1,3 @@
|
|||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from logging import DEBUG, INFO
|
||||
|
@ -12,10 +11,10 @@ from lisa.util.logger import init_log, log
|
|||
|
||||
|
||||
@retry(FileExistsError, tries=10, delay=0) # type: ignore
|
||||
def create_result_path() -> Path:
|
||||
def create_run_root_path() -> Path:
|
||||
date = datetime.utcnow().strftime("%Y%m%d")
|
||||
time = datetime.utcnow().strftime("%H%M%S-%f")[:-3]
|
||||
current_path = f"runtime/results/{date}/{date}-{time}"
|
||||
current_path = f"runtime/runs/{date}/{date}-{time}"
|
||||
path_obj = Path(current_path)
|
||||
if path_obj.exists():
|
||||
raise FileExistsError(f"{current_path} exists, and not found an unique path.")
|
||||
|
@ -23,10 +22,10 @@ def create_result_path() -> Path:
|
|||
|
||||
|
||||
def main() -> None:
|
||||
# create result path
|
||||
result_path = create_result_path().absolute()
|
||||
result_path.mkdir(parents=True)
|
||||
env.set_env(env.RESULT_PATH, str(result_path))
|
||||
# create run root path
|
||||
run_root_path = create_run_root_path().absolute()
|
||||
run_root_path.mkdir(parents=True)
|
||||
env.set_env(env.KEY_RUN_ROOT_PATH, str(run_root_path))
|
||||
|
||||
args = parse_args()
|
||||
|
||||
|
@ -34,7 +33,7 @@ def main() -> None:
|
|||
log.info(f"Python version: {sys.version}")
|
||||
log.info(f"local time: {datetime.now().astimezone()}")
|
||||
log.info(f"command line args: {sys.argv}")
|
||||
log.info(f"result path: {env.get_env(env.RESULT_PATH)}")
|
||||
log.info(f"run root path: {env.get_env(env.KEY_RUN_ROOT_PATH)}")
|
||||
|
||||
if args.debug:
|
||||
log_level = DEBUG
|
||||
|
@ -53,5 +52,4 @@ if __name__ == "__main__":
|
|||
log.exception(exception)
|
||||
exitCode = -1
|
||||
finally:
|
||||
# force all threads end.
|
||||
os._exit(exitCode)
|
||||
sys.exit(exitCode)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from lisa.core.executable import Executable
|
||||
from lisa.core.tool import Tool
|
||||
|
||||
|
||||
class Echo(Executable):
|
||||
class Echo(Tool):
|
||||
@property
|
||||
def command(self) -> str:
|
||||
return "echo"
|
|
@ -0,0 +1,5 @@
|
|||
from lisa.core.tool import Tool
|
||||
|
||||
|
||||
class Ping(Tool):
|
||||
pass
|
|
@ -1,10 +1,10 @@
|
|||
import re
|
||||
from typing import Tuple
|
||||
|
||||
from lisa.core.executable import Executable
|
||||
from lisa.core.tool import Tool
|
||||
|
||||
|
||||
class Uname(Executable):
|
||||
class Uname(Tool):
|
||||
def initialize(self) -> None:
|
||||
self.key_info_pattern = re.compile(
|
||||
r"(?P<release>[^ ]*?) (?P<version>[\w\W]*) (?P<platform>[\w_]+?)$"
|
|
@ -1,10 +1,21 @@
|
|||
import os
|
||||
from pathlib import PurePath
|
||||
|
||||
RESULT_PATH = "RESULT_PATH"
|
||||
WORKING_PATH = "working"
|
||||
|
||||
KEY_RUN_ROOT_PATH = "RUN_ROOT_PATH"
|
||||
|
||||
__prefix = "LISA_"
|
||||
|
||||
|
||||
def get_run_root_path() -> PurePath:
|
||||
return get_env(KEY_RUN_ROOT_PATH)
|
||||
|
||||
|
||||
def get_working_path() -> PurePath:
|
||||
return get_run_root_path().joinpath(WORKING_PATH)
|
||||
|
||||
|
||||
def set_env(name: str, value: str, isSecret: bool = False) -> None:
|
||||
name = f"{__prefix}{name}"
|
||||
os.environ[name] = value
|
||||
|
|
|
@ -3,13 +3,13 @@ import os
|
|||
import time
|
||||
|
||||
# to prevent circular import, hard code it here.
|
||||
env_result_path = "LISA_RESULT_PATH"
|
||||
env_key_run_root_path = "LISA_RUN_ROOT_PATH"
|
||||
|
||||
|
||||
def log_lines(logLevel: int, content: str, prefix: str = "") -> None:
|
||||
for line in content.splitlines(False):
|
||||
if prefix:
|
||||
log.log(logLevel, f"{prefix} {line}")
|
||||
log.log(logLevel, f"{prefix}{line}")
|
||||
else:
|
||||
log.log(logLevel, line)
|
||||
|
||||
|
@ -21,7 +21,7 @@ def init_log() -> None:
|
|||
format=format,
|
||||
datefmt="%m%d %H:%M:%S",
|
||||
handlers=[
|
||||
logging.FileHandler(f"{os.getenv(env_result_path)}/lisa-host.log"),
|
||||
logging.FileHandler(f"{os.getenv(env_key_run_root_path)}/lisa-host.log"),
|
||||
logging.StreamHandler(),
|
||||
],
|
||||
)
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import shlex
|
||||
import time
|
||||
from threading import Thread
|
||||
from timeit import default_timer as timer
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, cast
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
|
||||
|
||||
import psutil # type: ignore
|
||||
import spur
|
||||
from spurplus import SshShell # type: ignore
|
||||
|
||||
from lisa.util.executableResult import ExecutableResult
|
||||
from lisa.util.logger import log, log_lines
|
||||
from lisa.util.logger import log
|
||||
|
||||
if TYPE_CHECKING:
|
||||
BaseExceptionType = Type[BaseException]
|
||||
|
@ -18,96 +16,70 @@ else:
|
|||
BaseExceptionType = bool
|
||||
|
||||
|
||||
class LogPipe(Thread):
|
||||
def __init__(self, level: int, cmd_id: str = ""):
|
||||
"""Setup the object with a logger and a loglevel
|
||||
and start the thread
|
||||
"""
|
||||
Thread.__init__(self)
|
||||
self.output: str = ""
|
||||
self.daemon = False
|
||||
class LogWriter:
|
||||
def __init__(self, level: int, cmd_prefix: str = ""):
|
||||
self.level = level
|
||||
self.fdRead, self.fdWrite = os.pipe()
|
||||
self.pipeReader = os.fdopen(self.fdRead)
|
||||
self.isReadCompleted = False
|
||||
self.isClosed = False
|
||||
self.cmd_id = cmd_id
|
||||
self.start()
|
||||
self.cmd_prefix = cmd_prefix
|
||||
self.buffer: str = ""
|
||||
|
||||
def fileno(self) -> int:
|
||||
"""Return the write file descriptor of the pipe
|
||||
"""
|
||||
return self.fdWrite
|
||||
def write(self, message: str):
|
||||
if message == "\n":
|
||||
log.log(self.level, f"{self. cmd_prefix}{self.buffer}")
|
||||
self.buffer = ""
|
||||
else:
|
||||
self.buffer = "".join([self.buffer, message])
|
||||
|
||||
def run(self) -> None:
|
||||
"""Run the thread, logging everything.
|
||||
"""
|
||||
output = self.pipeReader.read()
|
||||
self.output = "".join([self.output, output])
|
||||
log_lines(self.level, output, prefix=f"cmd[{self.cmd_id}]")
|
||||
|
||||
self.pipeReader.close()
|
||||
self.isReadCompleted = True
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the write end of the pipe.
|
||||
"""
|
||||
if not self.isClosed:
|
||||
os.close(self.fdWrite)
|
||||
self.isClosed = True
|
||||
def close(self):
|
||||
if len(self.buffer) > 0:
|
||||
log.log(self.level, f"{self.cmd_prefix}{self.buffer}")
|
||||
|
||||
|
||||
class Process:
|
||||
def __init__(self) -> None:
|
||||
self.process: Optional[subprocess.Popen[Any]] = None
|
||||
self.exitCode: Optional[int] = None
|
||||
self.log_pipe: Optional[LogPipe] = None
|
||||
|
||||
self._running: bool = False
|
||||
|
||||
def __enter__(self) -> None:
|
||||
pass
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[BaseExceptionType],
|
||||
exc_value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
def __init__(
|
||||
self, cmd_prefix: str, shell: Union[SshShell, spur.LocalShell]
|
||||
) -> None:
|
||||
self.close()
|
||||
# the shell can be LocalShell or SshShell
|
||||
self.shell = shell
|
||||
self.cmd_prefix = cmd_prefix
|
||||
self._running: bool = False
|
||||
|
||||
def start(
|
||||
self,
|
||||
command: str,
|
||||
cwd: Optional[str] = None,
|
||||
new_envs: Optional[Dict[str, str]] = None,
|
||||
cmd_id: str = "",
|
||||
noErrorLog: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
command include all parameters also.
|
||||
"""
|
||||
dictEnv = dict(os.environ.copy())
|
||||
if new_envs is not None:
|
||||
for key, value in new_envs.items():
|
||||
dictEnv[key] = value
|
||||
self.stdout_pipe = LogPipe(logging.INFO, cmd_id)
|
||||
self.stdout_writer = LogWriter(logging.INFO, f"{self.cmd_prefix}stdout: ")
|
||||
if noErrorLog:
|
||||
logLevel = logging.INFO
|
||||
else:
|
||||
logLevel = logging.ERROR
|
||||
self.stderr_pipe = LogPipe(logLevel, cmd_id)
|
||||
self.process = subprocess.Popen(
|
||||
command,
|
||||
shell=True,
|
||||
stdout=cast(int, self.stdout_pipe),
|
||||
stderr=cast(int, self.stderr_pipe),
|
||||
cwd=cwd,
|
||||
env=cast(Optional[Dict[str, str]], dictEnv),
|
||||
)
|
||||
self._running = True
|
||||
if self.process is not None:
|
||||
log.debug(f"process {self.process.pid} started")
|
||||
self.stderr_writer = LogWriter(logLevel, f"{self.cmd_prefix}stderr: ")
|
||||
|
||||
split_command = shlex.split(command)
|
||||
log.debug(f"split command: {split_command}")
|
||||
try:
|
||||
self.process = self.shell.spawn(
|
||||
command=split_command,
|
||||
stdout=self.stdout_writer,
|
||||
stderr=self.stderr_writer,
|
||||
cwd=cwd,
|
||||
update_env=new_envs,
|
||||
allow_error=True,
|
||||
store_pid=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
self._running = True
|
||||
log.debug(f"{self.cmd_prefix}started")
|
||||
except (FileNotFoundError, spur.errors.NoSuchCommandError) as identifier:
|
||||
# FileNotFoundError: not found command on Windows
|
||||
# NoSuchCommandError: not found command on remote Linux
|
||||
self.process = ExecutableResult("", identifier.strerror, 1,)
|
||||
log.debug(f"{self.cmd_prefix} not found command")
|
||||
|
||||
def waitResult(self, timeout: float = 600) -> ExecutableResult:
|
||||
budget_time = timeout
|
||||
|
@ -120,48 +92,29 @@ class Process:
|
|||
|
||||
if budget_time < 0:
|
||||
if self.process is not None:
|
||||
log.warn(f"process {self.process.pid} timeout in {timeout} sec")
|
||||
log.warn(f"{self.cmd_prefix}timeout in {timeout} sec, and killed")
|
||||
self.stop()
|
||||
|
||||
# close to get pipe complete
|
||||
self.close()
|
||||
if not isinstance(self.process, ExecutableResult):
|
||||
assert self.process
|
||||
proces_result = self.process.wait_for_result()
|
||||
self.stdout_writer.close()
|
||||
self.stderr_writer.close()
|
||||
result = ExecutableResult(
|
||||
proces_result.output.strip(),
|
||||
proces_result.stderr_output.strip(),
|
||||
proces_result.return_code,
|
||||
)
|
||||
else:
|
||||
result = self.process
|
||||
|
||||
# wait all content flushed
|
||||
while (
|
||||
not self.stdout_pipe.isReadCompleted or not self.stderr_pipe.isReadCompleted
|
||||
):
|
||||
time.sleep(0.01)
|
||||
return ExecutableResult(
|
||||
self.stdout_pipe.output.strip(),
|
||||
self.stderr_pipe.output.strip(),
|
||||
self.exitCode,
|
||||
)
|
||||
return result
|
||||
|
||||
def stop(self) -> None:
|
||||
if self.process is not None:
|
||||
children = cast(
|
||||
List[psutil.Process], psutil.Process(self.process.pid).children(True)
|
||||
)
|
||||
for child in children:
|
||||
child.terminate()
|
||||
self.process.terminate()
|
||||
log.debug(f"process {self.process.pid} stopped")
|
||||
|
||||
def close(self) -> None:
|
||||
if self.stdout_pipe is not None:
|
||||
self.stdout_pipe.close()
|
||||
if self.stderr_pipe is not None:
|
||||
self.stderr_pipe.close()
|
||||
if self.process and not isinstance(self.process, ExecutableResult):
|
||||
self.process.send_signal(9)
|
||||
|
||||
def isRunning(self) -> bool:
|
||||
self.exitCode = self.getExitCode()
|
||||
if self.exitCode is not None and self.process is not None:
|
||||
if self._running:
|
||||
log.debug(f"process {self.process.pid} exited: {self.exitCode}")
|
||||
self._running = False
|
||||
if self._running:
|
||||
self._running = self.process.is_running()
|
||||
return self._running
|
||||
|
||||
def getExitCode(self) -> Optional[int]:
|
||||
if self.process is not None:
|
||||
self.exitCode = self.process.poll()
|
||||
return self.exitCode
|
||||
|
|
|
@ -6,6 +6,20 @@ optional = false
|
|||
python-versions = "*"
|
||||
version = "1.4.4"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Annotate AST trees with source code positions"
|
||||
name = "asttokens"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2.0.4"
|
||||
|
||||
[package.dependencies]
|
||||
six = "*"
|
||||
|
||||
[package.extras]
|
||||
test = ["astroid", "pytest"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Classes Without Boilerplate"
|
||||
|
@ -158,6 +172,20 @@ version = ">=4.3.5,<5"
|
|||
[package.extras]
|
||||
test = ["pytest (>=4.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Provide design-by-contract with informative violation messages."
|
||||
name = "icontract"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2.3.4"
|
||||
|
||||
[package.dependencies]
|
||||
asttokens = ">=2,<3"
|
||||
|
||||
[package.extras]
|
||||
dev = ["mypy (0.750)", "pylint (2.3.1)", "yapf (0.20.2)", "tox (>=3.0.0)", "pydocstyle (>=2.1.1,<3)", "coverage (>=4.5.1,<5)", "docutils (>=0.14,<1)", "pygments (>=2.2.0,<3)", "dpcontracts (0.6.0)", "tabulate (>=0.8.7,<1)", "py-cpuinfo (>=5.0.0,<6)"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
|
@ -466,6 +494,46 @@ optional = false
|
|||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
version = "1.15.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Run commands and manipulate files locally or over SSH using the same interface"
|
||||
name = "spur"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.3.20"
|
||||
|
||||
[package.dependencies]
|
||||
paramiko = ">=1.13.1,<3"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Manage remote machines and file operations over SSH."
|
||||
name = "spurplus"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2.3.3"
|
||||
|
||||
[package.dependencies]
|
||||
icontract = ">=2.0.1,<3"
|
||||
spur = "0.3.20"
|
||||
temppathlib = ">=1.0.3,<2"
|
||||
typing_extensions = ">=3.6.2.1"
|
||||
|
||||
[package.extras]
|
||||
dev = ["mypy (0.620)", "pylint (1.8.2)", "yapf (0.20.2)", "tox (>=3.0.0)", "coverage (>=4.5.1,<5)", "pydocstyle (>=2.1.1,<3)"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Wraps tempfile to give you pathlib.Path."
|
||||
name = "temppathlib"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.0.3"
|
||||
|
||||
[package.extras]
|
||||
dev = ["mypy (0.570)", "pylint (1.8.2)", "yapf (0.20.2)", "tox (>=3.0.0)"]
|
||||
test = ["tox (>=3.0.0)"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A collection of helpers and mock objects for unit tests and doc tests."
|
||||
|
@ -496,7 +564,7 @@ python-versions = "*"
|
|||
version = "1.4.1"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
category = "main"
|
||||
description = "Backported and Experimental Type Hints for Python 3.5+"
|
||||
name = "typing-extensions"
|
||||
optional = false
|
||||
|
@ -513,7 +581,7 @@ python-versions = "*"
|
|||
version = "1.35"
|
||||
|
||||
[metadata]
|
||||
content-hash = "59cde0dfa40d3e9d1494ed53142ff22b698a890bb024defd051d4fd1145caa0e"
|
||||
content-hash = "47bdcdb43ab99e6e4c725e65fd612356d1f74c86032b19d610aa27503d88f560"
|
||||
lock-version = "1.0"
|
||||
python-versions = "^3.8"
|
||||
|
||||
|
@ -522,6 +590,10 @@ appdirs = [
|
|||
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
|
||||
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
|
||||
]
|
||||
asttokens = [
|
||||
{file = "asttokens-2.0.4-py2.py3-none-any.whl", hash = "sha256:766d3352908730efb20b95ae22db0f1cb1bedb67c6071fcffb5c236ea673f2f7"},
|
||||
{file = "asttokens-2.0.4.tar.gz", hash = "sha256:a42e57e28f2ac1c85ed9b1f84109401427e5c63c04f61d15b8842b027eec5128"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
|
||||
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
|
||||
|
@ -624,6 +696,9 @@ flake8-isort = [
|
|||
{file = "flake8-isort-3.0.1.tar.gz", hash = "sha256:5d976da513cc390232ad5a9bb54aee8a092466a15f442d91dfc525834bee727a"},
|
||||
{file = "flake8_isort-3.0.1-py2.py3-none-any.whl", hash = "sha256:df1dd6dd73f6a8b128c9c783356627231783cccc82c13c6dc343d1a5a491699b"},
|
||||
]
|
||||
icontract = [
|
||||
{file = "icontract-2.3.4.tar.gz", hash = "sha256:5e45f7fcf957375163d63ef9b34a0413a15024f35029fd4b1f2ec21d8463879f"},
|
||||
]
|
||||
isort = [
|
||||
{file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"},
|
||||
{file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"},
|
||||
|
@ -788,6 +863,16 @@ six = [
|
|||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
||||
]
|
||||
spur = [
|
||||
{file = "spur-0.3.20-py2.py3-none-any.whl", hash = "sha256:f9446f418f419c046f49543ca55b35eff2e96b13cf7e0cf8605d657e5fa530ee"},
|
||||
{file = "spur-0.3.20.tar.gz", hash = "sha256:f359e0573c0e4aaf8427494d6e67bc2bfac4f0c719e47f05e4750921b5804760"},
|
||||
]
|
||||
spurplus = [
|
||||
{file = "spurplus-2.3.3.tar.gz", hash = "sha256:71c734a0827a68235d5b610c3570c3abc0c6be56708a340ba7bebc0a6eb77e92"},
|
||||
]
|
||||
temppathlib = [
|
||||
{file = "temppathlib-1.0.3.tar.gz", hash = "sha256:58eaea9190639591f5005289e128b3b822eb5a3341d538ffdb7e67a73526421a"},
|
||||
]
|
||||
testfixtures = [
|
||||
{file = "testfixtures-6.14.1-py2.py3-none-any.whl", hash = "sha256:30566e24a1b34e4d3f8c13abf62557d01eeb4480bcb8f1745467bfb0d415a7d9"},
|
||||
{file = "testfixtures-6.14.1.tar.gz", hash = "sha256:58d2b3146d93bc5ddb0cd24e0ccacb13e29bdb61e5c81235c58f7b8ee4470366"},
|
||||
|
|
|
@ -13,6 +13,7 @@ regex = "^2020.7.14"
|
|||
retry = "^0.9.2"
|
||||
paramiko = "^2.7.1"
|
||||
singleton-decorator = "^1.0.0"
|
||||
spurplus = "^2.3.3"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^19.10b0"
|
||||
|
|
Загрузка…
Ссылка в новой задаче