avoid AttributeError when shutting down plotting (#3954)

* Remove valid # type: ignore

A user ran into an exception on this line that mypy should have caught.  Let's see what it says.

```python-traceback
2021-05-06T07:31:41.595 full_node full_node_server        : ERROR    Exception , exception Stack: Traceback (most recent call last):
  File "chia\server\server.py", line 356, in start_client
  File "aiohttp\client.py", line 763, in _ws_connect
  File "aiohttp\client.py", line 521, in _request
  File "aiohttp\connector.py", line 535, in connect
  File "aiohttp\connector.py", line 892, in _create_connection
  File "aiohttp\connector.py", line 1032, in _create_direct_connection
  File "aiohttp\connector.py", line 969, in _wrap_create_connection
  File "asyncio\base_events.py", line 949, in create_connection
  File "asyncio\selector_events.py", line 473, in sock_connect
concurrent.futures._base.CancelledError

2021-05-06T07:31:45.016 daemon asyncio                    : ERROR    Task exception was never retrieved
future: <Task finished coro=<kill_service() done, defined at chia\daemon\server.py:833> exception=AttributeError("'list' object has no attribute 'pid'")>
Traceback (most recent call last):
  File "chia\daemon\server.py", line 841, in kill_service
  File "chia\daemon\server.py", line 805, in kill_process
AttributeError: 'list' object has no attribute 'pid'
2021-05-06T07:32:09.965 full_node full_node_server        : ERROR    Exception:  <class 'concurrent.futures._base.CancelledError'>, closing connection None. Traceback (most recent call last):
  File "chia\server\server.py", line 531, in api_call
  File "asyncio\tasks.py", line 435, in wait_for
concurrent.futures._base.CancelledError

2021-05-06T07:33:20.573 full_node full_node_server        : ERROR    Exception , exception Stack: Traceback (most recent call last):
  File "chia\server\server.py", line 356, in start_client
  File "aiohttp\client.py", line 763, in _ws_connect
  File "aiohttp\client.py", line 521, in _request
  File "aiohttp\connector.py", line 535, in connect
  File "aiohttp\connector.py", line 892, in _create_connection
  File "aiohttp\connector.py", line 1032, in _create_direct_connection
  File "aiohttp\connector.py", line 969, in _wrap_create_connection
```

* fix plotter service killing

* just make it always be a list of processes for all services

* catch up tests

* Update chia/daemon/server.py
This commit is contained in:
Kyle Altendorf 2023-03-09 05:22:38 -05:00 committed by GitHub
parent d84917f5df
commit c4c165eabd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 36 additions and 28 deletions

View File

@ -133,7 +133,7 @@ class WebSocketServer:
):
self.root_path = root_path
self.log = log
self.services: Dict = dict()
self.services: Dict[str, List[subprocess.Popen]] = dict()
self.plots_queue: List[Dict] = []
self.connections: Dict[str, Set[WebSocketResponse]] = dict() # service name : {WebSocketResponse}
self.ping_job: Optional[asyncio.Task] = None
@ -1053,7 +1053,7 @@ class WebSocketServer:
run_next = True
config["state"] = PlotState.REMOVING
self.state_changed(service_plotter, self.prepare_plot_state_message(PlotEvent.STATE_CHANGED, id))
await kill_process(process, self.root_path, service_plotter, id)
await kill_processes([process], self.root_path, service_plotter, id)
config["state"] = PlotState.FINISHED
config["deleted"] = True
@ -1089,9 +1089,8 @@ class WebSocketServer:
error = "unknown service"
if service_command in self.services:
service = self.services[service_command]
r = service is not None and service.poll() is None
if r is False:
processes = self.services[service_command]
if all(process.poll() is not None for process in processes):
self.services.pop(service_command)
error = None
else:
@ -1111,7 +1110,7 @@ class WebSocketServer:
if testing is True:
exe_command = f"{service_command} --testing=true"
process, pid_path = launch_service(self.root_path, exe_command)
self.services[service_command] = process
self.services[service_command] = [process]
success = True
except (subprocess.SubprocessError, IOError):
log.exception(f"problem starting {service_command}")
@ -1127,12 +1126,13 @@ class WebSocketServer:
return response
def is_service_running(self, service_name: str) -> bool:
processes: List[subprocess.Popen]
if service_name == service_plotter:
processes = self.services.get(service_name)
is_running = processes is not None and len(processes) > 0
processes = self.services.get(service_name, [])
is_running = len(processes) > 0
else:
process = self.services.get(service_name)
is_running = process is not None and process.poll() is None
processes = self.services.get(service_name, [])
is_running = any(process.poll() is None for process in processes)
if not is_running:
# Check if we have a connection to the requested service. This might be the
# case if the service was started manually (i.e. not started by the daemon).
@ -1297,31 +1297,39 @@ def launch_service(root_path: Path, service_command) -> Tuple[subprocess.Popen,
return process, pid_path
async def kill_process(
process: subprocess.Popen, root_path: Path, service_name: str, id: str, delay_before_kill: int = 15
async def kill_processes(
processes: List[subprocess.Popen],
root_path: Path,
service_name: str,
id: str,
delay_before_kill: int = 15,
) -> bool:
pid_path = pid_path_for_service(root_path, service_name, id)
if sys.platform == "win32" or sys.platform == "cygwin":
log.info("sending CTRL_BREAK_EVENT signal to %s", service_name)
# pylint: disable=E1101
kill(process.pid, signal.SIGBREAK)
for process in processes:
kill(process.pid, signal.SIGBREAK)
else:
log.info("sending term signal to %s", service_name)
process.terminate()
for process in processes:
process.terminate()
count: float = 0
while count < delay_before_kill:
if process.poll() is not None:
if all(process.poll() is not None for process in processes):
break
await asyncio.sleep(0.5)
count += 0.5
else:
process.kill()
for process in processes:
process.kill()
log.info("sending kill signal to %s", service_name)
r = process.wait()
log.info("process %s returned %d", service_name, r)
for process in processes:
r = process.wait()
log.info("process %s returned %d", service_name, r)
try:
pid_path_killed = pid_path.with_suffix(".pid-killed")
if pid_path_killed.exists():
@ -1334,13 +1342,13 @@ async def kill_process(
async def kill_service(
root_path: Path, services: Dict[str, subprocess.Popen], service_name: str, delay_before_kill: int = 15
root_path: Path, services: Dict[str, List[subprocess.Popen]], service_name: str, delay_before_kill: int = 15
) -> bool:
process = services.get(service_name)
if process is None:
processes = services.get(service_name)
if processes is None:
return False
del services[service_name]
result = await kill_process(process, root_path, service_name, "", delay_before_kill)
result = await kill_processes(processes, root_path, service_name, "", delay_before_kill)
return result

View File

@ -165,9 +165,9 @@ def mock_daemon_with_services():
# Mock daemon server with a couple running services, a plotter, and one stopped service
return Daemon(
services={
"my_refrigerator": Service(True),
"the_river": Service(True),
"your_nose": Service(False),
"my_refrigerator": [Service(True)],
"the_river": [Service(True)],
"your_nose": [Service(False)],
"chia_plotter": [Service(True), Service(True)],
},
connections={},
@ -179,9 +179,9 @@ def mock_daemon_with_services_and_connections():
# Mock daemon server with a couple running services, a plotter, and a couple active connections
return Daemon(
services={
"my_refrigerator": Service(True),
"my_refrigerator": [Service(True)],
"chia_plotter": [Service(True), Service(True)],
"apple": Service(True),
"apple": [Service(True)],
},
connections={
"apple": [1],