Skip to content

Commit 277a197

Browse files
lawrencelomaxfacebook-github-bot
authored andcommitted
Flatten CompanionSpawner into Companion
Summary: This will allow re-use of the companion spawning flow, which is currently bizzarely split. Reviewed By: jbardini Differential Revision: D29559094 fbshipit-source-id: 4bf2cef45a01064644a0e547fabf122d3fce4877
1 parent 4ed5933 commit 277a197

File tree

4 files changed

+177
-185
lines changed

4 files changed

+177
-185
lines changed

idb/common/companion.py

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,23 @@
55
# LICENSE file in the root directory of this source tree.
66

77
import asyncio
8+
import errno
89
import json
910
import logging
11+
import os
1012
import subprocess
1113
from datetime import timedelta
1214
from logging import Logger, DEBUG as LOG_LEVEL_DEBUG
13-
from typing import AsyncGenerator, Dict, List, Optional, Sequence, Union
15+
from typing import AsyncGenerator, Dict, List, Optional, Sequence, Union, Tuple
1416

17+
from idb.common.constants import IDB_LOCAL_TARGETS_FILE, IDB_LOGS_PATH
18+
from idb.common.file import get_last_n_lines
1519
from idb.common.format import (
1620
target_description_from_json,
1721
target_descriptions_from_json,
1822
)
1923
from idb.common.logging import log_call
24+
from idb.common.pid_saver import PidSaver
2025
from idb.common.types import (
2126
Companion as CompanionBase,
2227
ECIDFilter,
@@ -38,6 +43,10 @@ class IdbJsonException(Exception):
3843
pass
3944

4045

46+
class CompanionSpawnerException(Exception):
47+
pass
48+
49+
4150
async def _terminate_process(
4251
process: asyncio.subprocess.Process, timeout: timedelta, logger: logging.Logger
4352
) -> None:
@@ -73,13 +82,84 @@ def parse_json_line(line: bytes) -> Dict[str, Union[int, str]]:
7382
raise IdbJsonException(f"Failed to parse json from: {decoded_line}")
7483

7584

85+
async def _extract_port_from_spawned_companion(stream: asyncio.StreamReader) -> int:
86+
# The first line of stdout should contain launch info,
87+
# otherwise something bad has happened
88+
line = await stream.readline()
89+
logging.debug(f"Read line from companion: {line}")
90+
update = parse_json_line(line)
91+
logging.debug(f"Got update from companion: {update}")
92+
return int(update["grpc_port"])
93+
94+
95+
async def do_spawn_companion(
96+
path: str,
97+
udid: str,
98+
log_file_path: str,
99+
device_set_path: Optional[str],
100+
port: Optional[int],
101+
cwd: Optional[str],
102+
tmp_path: Optional[str],
103+
reparent: bool,
104+
tls_cert_path: Optional[str] = None,
105+
) -> Tuple[asyncio.subprocess.Process, int]:
106+
arguments: List[str] = [
107+
path,
108+
"--udid",
109+
udid,
110+
"--grpc-port",
111+
str(port) if port is not None else "0",
112+
]
113+
if tls_cert_path is not None:
114+
arguments.extend(["--tls-cert-path", tls_cert_path])
115+
if device_set_path is not None:
116+
arguments.extend(["--device-set-path", device_set_path])
117+
118+
env = dict(os.environ)
119+
if tmp_path:
120+
env["TMPDIR"] = tmp_path
121+
122+
with open(log_file_path, "a") as log_file:
123+
process = await asyncio.create_subprocess_exec(
124+
*arguments,
125+
stdout=asyncio.subprocess.PIPE,
126+
stdin=asyncio.subprocess.PIPE if reparent else None,
127+
stderr=log_file,
128+
cwd=cwd,
129+
env=env,
130+
preexec_fn=os.setpgrp if reparent else None,
131+
)
132+
logging.debug(f"started companion at process id {process.pid}")
133+
stdout = none_throws(process.stdout)
134+
try:
135+
extracted_port = await _extract_port_from_spawned_companion(stdout)
136+
except Exception as e:
137+
raise CompanionSpawnerException(
138+
f"Failed to spawn companion, couldn't read port "
139+
f"stderr: {get_last_n_lines(log_file_path, 30)}"
140+
) from e
141+
if extracted_port == 0:
142+
raise CompanionSpawnerException(
143+
f"Failed to spawn companion, port is zero"
144+
f"stderr: {get_last_n_lines(log_file_path, 30)}"
145+
)
146+
if port is not None and extracted_port != port:
147+
raise CompanionSpawnerException(
148+
"Failed to spawn companion, port is not correct "
149+
f"(expected {port} got {extracted_port})"
150+
f"stderr: {get_last_n_lines(log_file_path, 30)}"
151+
)
152+
return (process, extracted_port)
153+
154+
76155
class Companion(CompanionBase):
77156
def __init__(
78157
self, companion_path: str, device_set_path: Optional[str], logger: Logger
79158
) -> None:
80159
self._companion_path = companion_path
81160
self._device_set_path = device_set_path
82161
self._logger = logger
162+
self._pid_saver = PidSaver(logger=logger)
83163

84164
@asynccontextmanager
85165
async def _start_companion_command(
@@ -142,6 +222,81 @@ async def _run_udid_command(
142222
arguments=[f"--{command}", udid], timeout=timeout
143223
)
144224

225+
self._pid_saver = PidSaver(logger=self.logger)
226+
227+
def _log_file_path(self, target_udid: str) -> str:
228+
os.makedirs(name=IDB_LOGS_PATH, exist_ok=True)
229+
return IDB_LOGS_PATH + "/" + target_udid
230+
231+
def _is_notifier_running(self) -> bool:
232+
pid = self._pid_saver.get_notifier_pid()
233+
# Taken from https://fburl.com/ibk820b6
234+
if pid <= 0:
235+
return False
236+
try:
237+
# no-op if process exists
238+
os.kill(pid, 0)
239+
return True
240+
except OSError as err:
241+
# EPERM clearly means there's a process to deny access to
242+
# otherwise proc doesn't exist
243+
return err.errno == errno.EPERM
244+
except Exception:
245+
return False
246+
247+
async def _read_notifier_output(self, stream: asyncio.StreamReader) -> None:
248+
while True:
249+
line = await stream.readline()
250+
if line is None:
251+
return
252+
update = parse_json_line(line)
253+
if update["report_initial_state"]:
254+
return
255+
256+
def check_okay_to_spawn(self) -> None:
257+
if os.getuid() == 0:
258+
logging.warning(
259+
"idb should not be run as root. "
260+
"Listing available targets on this host and spawning "
261+
"companions will not work"
262+
)
263+
264+
async def spawn_companion(self, target_udid: str) -> int:
265+
self.check_okay_to_spawn()
266+
(process, port) = await do_spawn_companion(
267+
path=self._companion_path,
268+
udid=target_udid,
269+
log_file_path=self._log_file_path(target_udid),
270+
device_set_path=None,
271+
port=None,
272+
cwd=None,
273+
tmp_path=None,
274+
reparent=True,
275+
)
276+
self._pid_saver.save_companion_pid(pid=process.pid)
277+
return port
278+
279+
async def spawn_notifier(self, targets_file: str = IDB_LOCAL_TARGETS_FILE) -> None:
280+
if self._is_notifier_running():
281+
return
282+
283+
self.check_okay_to_spawn()
284+
cmd = [self._companion_path, "--notify", targets_file]
285+
log_path = self._log_file_path("notifier")
286+
with open(log_path, "a") as log_file:
287+
process = await asyncio.create_subprocess_exec(
288+
*cmd, stdout=asyncio.subprocess.PIPE, stderr=log_file
289+
)
290+
try:
291+
self._pid_saver.save_notifier_pid(pid=process.pid)
292+
await self._read_notifier_output(stream=none_throws(process.stdout))
293+
logging.debug(f"started notifier at process id {process.pid}")
294+
except Exception as e:
295+
raise CompanionSpawnerException(
296+
"Failed to spawn the idb notifier. "
297+
f"Stderr: {get_last_n_lines(log_path, 30)}"
298+
) from e
299+
145300
@log_call()
146301
async def create(
147302
self, device_type: str, os_version: str, timeout: Optional[timedelta] = None

idb/common/companion_spawner.py

Lines changed: 0 additions & 171 deletions
This file was deleted.

0 commit comments

Comments
 (0)