android-emu-agent 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- android_emu_agent/__init__.py +3 -0
- android_emu_agent/actions/__init__.py +1 -0
- android_emu_agent/actions/executor.py +288 -0
- android_emu_agent/actions/selector.py +122 -0
- android_emu_agent/actions/wait.py +193 -0
- android_emu_agent/artifacts/__init__.py +1 -0
- android_emu_agent/artifacts/manager.py +125 -0
- android_emu_agent/artifacts/py.typed +0 -0
- android_emu_agent/cli/__init__.py +1 -0
- android_emu_agent/cli/commands/__init__.py +1 -0
- android_emu_agent/cli/commands/action.py +158 -0
- android_emu_agent/cli/commands/app_cmd.py +95 -0
- android_emu_agent/cli/commands/artifact.py +81 -0
- android_emu_agent/cli/commands/daemon.py +62 -0
- android_emu_agent/cli/commands/device.py +122 -0
- android_emu_agent/cli/commands/emulator.py +46 -0
- android_emu_agent/cli/commands/file.py +139 -0
- android_emu_agent/cli/commands/reliability.py +310 -0
- android_emu_agent/cli/commands/session.py +65 -0
- android_emu_agent/cli/commands/ui.py +112 -0
- android_emu_agent/cli/commands/wait.py +132 -0
- android_emu_agent/cli/daemon_client.py +185 -0
- android_emu_agent/cli/main.py +52 -0
- android_emu_agent/cli/utils.py +171 -0
- android_emu_agent/daemon/__init__.py +1 -0
- android_emu_agent/daemon/core.py +62 -0
- android_emu_agent/daemon/health.py +177 -0
- android_emu_agent/daemon/models.py +244 -0
- android_emu_agent/daemon/server.py +1644 -0
- android_emu_agent/db/__init__.py +1 -0
- android_emu_agent/db/models.py +229 -0
- android_emu_agent/device/__init__.py +1 -0
- android_emu_agent/device/manager.py +522 -0
- android_emu_agent/device/session.py +129 -0
- android_emu_agent/errors.py +232 -0
- android_emu_agent/files/__init__.py +1 -0
- android_emu_agent/files/manager.py +311 -0
- android_emu_agent/py.typed +0 -0
- android_emu_agent/reliability/__init__.py +1 -0
- android_emu_agent/reliability/manager.py +244 -0
- android_emu_agent/ui/__init__.py +1 -0
- android_emu_agent/ui/context.py +169 -0
- android_emu_agent/ui/ref_resolver.py +149 -0
- android_emu_agent/ui/snapshotter.py +236 -0
- android_emu_agent/validation.py +59 -0
- android_emu_agent-0.1.3.dist-info/METADATA +375 -0
- android_emu_agent-0.1.3.dist-info/RECORD +50 -0
- android_emu_agent-0.1.3.dist-info/WHEEL +4 -0
- android_emu_agent-0.1.3.dist-info/entry_points.txt +2 -0
- android_emu_agent-0.1.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Emulator management CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from android_emu_agent.cli.daemon_client import DaemonClient
|
|
8
|
+
from android_emu_agent.cli.utils import handle_response
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Emulator management commands")
|
|
11
|
+
snapshot_app = typer.Typer(help="Emulator snapshot commands")
|
|
12
|
+
app.add_typer(snapshot_app, name="snapshot")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@snapshot_app.command("save")
|
|
16
|
+
def emulator_snapshot_save(
|
|
17
|
+
serial: str = typer.Argument(..., help="Emulator serial (e.g., emulator-5554)"),
|
|
18
|
+
name: str = typer.Argument(..., help="Snapshot name"),
|
|
19
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Save emulator snapshot."""
|
|
22
|
+
client = DaemonClient()
|
|
23
|
+
resp = client.request(
|
|
24
|
+
"POST",
|
|
25
|
+
"/emulator/snapshot_save",
|
|
26
|
+
json_body={"serial": serial, "name": name},
|
|
27
|
+
)
|
|
28
|
+
client.close()
|
|
29
|
+
handle_response(resp, json_output=json_output)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@snapshot_app.command("restore")
|
|
33
|
+
def emulator_snapshot_restore(
|
|
34
|
+
serial: str = typer.Argument(..., help="Emulator serial (e.g., emulator-5554)"),
|
|
35
|
+
name: str = typer.Argument(..., help="Snapshot name"),
|
|
36
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Restore emulator snapshot."""
|
|
39
|
+
client = DaemonClient()
|
|
40
|
+
resp = client.request(
|
|
41
|
+
"POST",
|
|
42
|
+
"/emulator/snapshot_restore",
|
|
43
|
+
json_body={"serial": serial, "name": name},
|
|
44
|
+
)
|
|
45
|
+
client.close()
|
|
46
|
+
handle_response(resp, json_output=json_output)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""File transfer CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from android_emu_agent.cli.daemon_client import DaemonClient
|
|
8
|
+
from android_emu_agent.cli.utils import (
|
|
9
|
+
FILE_APP_TRANSFER_TIMEOUT,
|
|
10
|
+
FILE_TRANSFER_TIMEOUT,
|
|
11
|
+
handle_output_response,
|
|
12
|
+
handle_response,
|
|
13
|
+
require_target,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="File transfer commands")
|
|
17
|
+
app_private = typer.Typer(help="App-private file operations (rooted/emulator)")
|
|
18
|
+
app.add_typer(app_private, name="app")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command("push")
|
|
22
|
+
def file_push(
|
|
23
|
+
local_path: str = typer.Argument(..., help="Local file or directory"),
|
|
24
|
+
remote_path: str | None = typer.Option(
|
|
25
|
+
None,
|
|
26
|
+
"--remote",
|
|
27
|
+
help="Remote path (default: /sdcard/Download/<name>)",
|
|
28
|
+
),
|
|
29
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
30
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
31
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Push a local file to shared storage (sdcard)."""
|
|
34
|
+
payload = require_target(device, session_id)
|
|
35
|
+
payload.update({"local_path": local_path, "remote_path": remote_path})
|
|
36
|
+
client = DaemonClient(timeout=FILE_TRANSFER_TIMEOUT)
|
|
37
|
+
resp = client.request("POST", "/files/push", json_body=payload)
|
|
38
|
+
client.close()
|
|
39
|
+
handle_response(resp, json_output=json_output)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("pull")
|
|
43
|
+
def file_pull(
|
|
44
|
+
remote_path: str = typer.Argument(..., help="Remote file or directory"),
|
|
45
|
+
local_path: str | None = typer.Option(None, "--local", help="Local output path"),
|
|
46
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
47
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
48
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Pull a file from shared storage (sdcard)."""
|
|
51
|
+
payload = require_target(device, session_id)
|
|
52
|
+
payload.update({"remote_path": remote_path, "local_path": local_path})
|
|
53
|
+
client = DaemonClient(timeout=FILE_TRANSFER_TIMEOUT)
|
|
54
|
+
resp = client.request("POST", "/files/pull", json_body=payload)
|
|
55
|
+
client.close()
|
|
56
|
+
handle_response(resp, json_output=json_output)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("find")
|
|
60
|
+
def file_find(
|
|
61
|
+
path: str = typer.Argument(..., help="Root directory to search"),
|
|
62
|
+
name: str = typer.Option(..., "--name", help="Filename glob (e.g. *.db or cache*)"),
|
|
63
|
+
kind: str = typer.Option("any", "--type", help="file|dir|any"),
|
|
64
|
+
max_depth: int = typer.Option(4, "--max-depth", help="Max directory depth"),
|
|
65
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
66
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
67
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Find files/folders and return metadata (rooted/emulator)."""
|
|
70
|
+
payload = require_target(device, session_id)
|
|
71
|
+
payload.update(
|
|
72
|
+
{
|
|
73
|
+
"path": path,
|
|
74
|
+
"name": name,
|
|
75
|
+
"kind": kind,
|
|
76
|
+
"max_depth": max_depth,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
client = DaemonClient(timeout=FILE_TRANSFER_TIMEOUT)
|
|
80
|
+
resp = client.request("POST", "/files/find", json_body=payload)
|
|
81
|
+
client.close()
|
|
82
|
+
handle_output_response(resp, json_output=json_output)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command("list")
|
|
86
|
+
def file_list(
|
|
87
|
+
path: str = typer.Argument(..., help="Directory to list"),
|
|
88
|
+
kind: str = typer.Option("any", "--type", help="file|dir|any"),
|
|
89
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
90
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
91
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
92
|
+
) -> None:
|
|
93
|
+
"""List files/folders in a directory (rooted/emulator)."""
|
|
94
|
+
payload = require_target(device, session_id)
|
|
95
|
+
payload.update({"path": path, "kind": kind})
|
|
96
|
+
client = DaemonClient(timeout=FILE_TRANSFER_TIMEOUT)
|
|
97
|
+
resp = client.request("POST", "/files/list", json_body=payload)
|
|
98
|
+
client.close()
|
|
99
|
+
handle_output_response(resp, json_output=json_output)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app_private.command("push")
|
|
103
|
+
def file_app_push(
|
|
104
|
+
package: str = typer.Argument(..., help="Package name"),
|
|
105
|
+
local_path: str = typer.Argument(..., help="Local file or directory"),
|
|
106
|
+
remote_path: str | None = typer.Option(
|
|
107
|
+
None,
|
|
108
|
+
"--remote",
|
|
109
|
+
help="App data path (default: files/<name>)",
|
|
110
|
+
),
|
|
111
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
112
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
113
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Push a file into app-private storage (rooted/emulator)."""
|
|
116
|
+
payload = require_target(device, session_id)
|
|
117
|
+
payload.update({"package": package, "local_path": local_path, "remote_path": remote_path})
|
|
118
|
+
client = DaemonClient(timeout=FILE_APP_TRANSFER_TIMEOUT)
|
|
119
|
+
resp = client.request("POST", "/files/app_push", json_body=payload)
|
|
120
|
+
client.close()
|
|
121
|
+
handle_response(resp, json_output=json_output)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@app_private.command("pull")
|
|
125
|
+
def file_app_pull(
|
|
126
|
+
package: str = typer.Argument(..., help="Package name"),
|
|
127
|
+
remote_path: str = typer.Argument(..., help="App data path (relative or absolute)"),
|
|
128
|
+
local_path: str | None = typer.Option(None, "--local", help="Local output path"),
|
|
129
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
130
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
131
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Pull a file from app-private storage (rooted/emulator)."""
|
|
134
|
+
payload = require_target(device, session_id)
|
|
135
|
+
payload.update({"package": package, "remote_path": remote_path, "local_path": local_path})
|
|
136
|
+
client = DaemonClient(timeout=FILE_APP_TRANSFER_TIMEOUT)
|
|
137
|
+
resp = client.request("POST", "/files/app_pull", json_body=payload)
|
|
138
|
+
client.close()
|
|
139
|
+
handle_response(resp, json_output=json_output)
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Reliability and forensics CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from android_emu_agent.cli.daemon_client import DaemonClient
|
|
8
|
+
from android_emu_agent.cli.utils import (
|
|
9
|
+
RELIABILITY_BUGREPORT_TIMEOUT,
|
|
10
|
+
RELIABILITY_DUMPHEAP_TIMEOUT,
|
|
11
|
+
RELIABILITY_PULL_TIMEOUT,
|
|
12
|
+
RELIABILITY_TIMEOUT,
|
|
13
|
+
handle_output_response,
|
|
14
|
+
handle_response,
|
|
15
|
+
require_target,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="Reliability and forensics commands")
|
|
19
|
+
dropbox_app = typer.Typer(help="DropBoxManager commands")
|
|
20
|
+
pull_app = typer.Typer(help="Pull protected artifacts (rooted/emulator)")
|
|
21
|
+
app.add_typer(dropbox_app, name="dropbox")
|
|
22
|
+
app.add_typer(pull_app, name="pull")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("exit-info")
|
|
26
|
+
def reliability_exit_info(
|
|
27
|
+
package: str = typer.Argument(..., help="Package name"),
|
|
28
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
29
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
30
|
+
list_only: bool = typer.Option(False, "--list", help="List exit-info entries only"),
|
|
31
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Show ApplicationExitInfo for a package."""
|
|
34
|
+
payload = require_target(device, session_id)
|
|
35
|
+
payload.update({"package": package, "list_only": list_only})
|
|
36
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
37
|
+
resp = client.request("POST", "/reliability/exit_info", json_body=payload)
|
|
38
|
+
client.close()
|
|
39
|
+
handle_output_response(resp, json_output=json_output)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("bugreport")
|
|
43
|
+
def reliability_bugreport(
|
|
44
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
45
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
46
|
+
output: str | None = typer.Option(None, "--output", help="Output filename (.zip)"),
|
|
47
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Capture a system bugreport."""
|
|
50
|
+
payload = require_target(device, session_id)
|
|
51
|
+
payload.update({"filename": output})
|
|
52
|
+
client = DaemonClient(timeout=RELIABILITY_BUGREPORT_TIMEOUT)
|
|
53
|
+
resp = client.request("POST", "/reliability/bugreport", json_body=payload)
|
|
54
|
+
client.close()
|
|
55
|
+
handle_response(resp, json_output=json_output)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command("events")
|
|
59
|
+
def reliability_events(
|
|
60
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
61
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
62
|
+
pattern: str | None = typer.Option(None, "--pattern", help="Regex filter for events"),
|
|
63
|
+
package: str | None = typer.Option(None, "--package", help="Filter for package name"),
|
|
64
|
+
since: str | None = typer.Option(None, "--since", help="Logcat -t value"),
|
|
65
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Dump ActivityManager events log."""
|
|
68
|
+
payload = require_target(device, session_id)
|
|
69
|
+
payload.update({"pattern": pattern, "package": package, "since": since})
|
|
70
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
71
|
+
resp = client.request("POST", "/reliability/events", json_body=payload)
|
|
72
|
+
client.close()
|
|
73
|
+
handle_output_response(resp, json_output=json_output)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dropbox_app.command("list")
|
|
77
|
+
def reliability_dropbox_list(
|
|
78
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
79
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
80
|
+
package: str | None = typer.Option(None, "--package", help="Filter for package name"),
|
|
81
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
82
|
+
) -> None:
|
|
83
|
+
"""List DropBoxManager entries."""
|
|
84
|
+
payload = require_target(device, session_id)
|
|
85
|
+
payload.update({"package": package})
|
|
86
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
87
|
+
resp = client.request("POST", "/reliability/dropbox_list", json_body=payload)
|
|
88
|
+
client.close()
|
|
89
|
+
handle_output_response(resp, json_output=json_output)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dropbox_app.command("print")
|
|
93
|
+
def reliability_dropbox_print(
|
|
94
|
+
tag: str = typer.Argument(..., help="DropBox tag (e.g., data_app_crash)"),
|
|
95
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
96
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
97
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Print a DropBoxManager entry."""
|
|
100
|
+
payload = require_target(device, session_id)
|
|
101
|
+
payload.update({"tag": tag})
|
|
102
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
103
|
+
resp = client.request("POST", "/reliability/dropbox_print", json_body=payload)
|
|
104
|
+
client.close()
|
|
105
|
+
handle_output_response(resp, json_output=json_output)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command("background")
|
|
109
|
+
def reliability_background(
|
|
110
|
+
package: str = typer.Argument(..., help="Package name"),
|
|
111
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
112
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
113
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Check background restrictions and standby bucket."""
|
|
116
|
+
payload = require_target(device, session_id)
|
|
117
|
+
payload.update({"package": package})
|
|
118
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
119
|
+
resp = client.request("POST", "/reliability/background", json_body=payload)
|
|
120
|
+
client.close()
|
|
121
|
+
handle_output_response(resp, json_output=json_output)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@app.command("last-anr")
|
|
125
|
+
def reliability_last_anr(
|
|
126
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
127
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
128
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Show the last ANR summary from ActivityManager."""
|
|
131
|
+
payload = require_target(device, session_id)
|
|
132
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
133
|
+
resp = client.request("POST", "/reliability/last_anr", json_body=payload)
|
|
134
|
+
client.close()
|
|
135
|
+
handle_output_response(resp, json_output=json_output)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.command("jobscheduler")
|
|
139
|
+
def reliability_jobscheduler(
|
|
140
|
+
package: str = typer.Argument(..., help="Package name"),
|
|
141
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
142
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
143
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Inspect JobScheduler constraints for a package."""
|
|
146
|
+
payload = require_target(device, session_id)
|
|
147
|
+
payload.update({"package": package})
|
|
148
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
149
|
+
resp = client.request("POST", "/reliability/jobscheduler", json_body=payload)
|
|
150
|
+
client.close()
|
|
151
|
+
handle_output_response(resp, json_output=json_output)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.command("compile")
|
|
155
|
+
def reliability_compile(
|
|
156
|
+
package: str = typer.Argument(..., help="Package name"),
|
|
157
|
+
mode: str = typer.Option("reset", "--mode", help="reset|speed"),
|
|
158
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
159
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
160
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Reset or force package compilation."""
|
|
163
|
+
payload = require_target(device, session_id)
|
|
164
|
+
payload.update({"package": package, "mode": mode})
|
|
165
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
166
|
+
resp = client.request("POST", "/reliability/compile", json_body=payload)
|
|
167
|
+
client.close()
|
|
168
|
+
handle_output_response(resp, json_output=json_output)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@app.command("always-finish")
|
|
172
|
+
def reliability_always_finish(
|
|
173
|
+
state: str = typer.Argument(..., help="on|off"),
|
|
174
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
175
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
176
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Toggle always-finish-activities developer setting."""
|
|
179
|
+
payload = require_target(device, session_id)
|
|
180
|
+
payload.update({"state": state})
|
|
181
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
182
|
+
resp = client.request("POST", "/reliability/always_finish", json_body=payload)
|
|
183
|
+
client.close()
|
|
184
|
+
handle_response(resp, json_output=json_output)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.command("run-as-ls")
|
|
188
|
+
def reliability_run_as_ls(
|
|
189
|
+
package: str = typer.Argument(..., help="Package name"),
|
|
190
|
+
path: str = typer.Option("files/", "--path", help="Relative path under app data"),
|
|
191
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
192
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
193
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
194
|
+
) -> None:
|
|
195
|
+
"""List app-private files for debuggable apps using run-as."""
|
|
196
|
+
payload = require_target(device, session_id)
|
|
197
|
+
payload.update({"package": package, "path": path})
|
|
198
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
199
|
+
resp = client.request("POST", "/reliability/run_as_ls", json_body=payload)
|
|
200
|
+
client.close()
|
|
201
|
+
handle_output_response(resp, json_output=json_output)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@app.command("dumpheap")
|
|
205
|
+
def reliability_dumpheap(
|
|
206
|
+
package: str = typer.Argument(..., help="Package name"),
|
|
207
|
+
keep_remote: bool = typer.Option(False, "--keep-remote", help="Keep heap on device"),
|
|
208
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
209
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
210
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Dump a heap profile and pull it locally."""
|
|
213
|
+
payload = require_target(device, session_id)
|
|
214
|
+
payload.update({"package": package, "keep_remote": keep_remote})
|
|
215
|
+
client = DaemonClient(timeout=RELIABILITY_DUMPHEAP_TIMEOUT)
|
|
216
|
+
resp = client.request("POST", "/reliability/dumpheap", json_body=payload)
|
|
217
|
+
client.close()
|
|
218
|
+
handle_response(resp, json_output=json_output)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.command("sigquit")
|
|
222
|
+
def reliability_sigquit(
|
|
223
|
+
package: str = typer.Argument(..., help="Package name"),
|
|
224
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
225
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
226
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Send SIGQUIT to dump thread stacks."""
|
|
229
|
+
payload = require_target(device, session_id)
|
|
230
|
+
payload.update({"package": package})
|
|
231
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
232
|
+
resp = client.request("POST", "/reliability/sigquit", json_body=payload)
|
|
233
|
+
client.close()
|
|
234
|
+
handle_response(resp, json_output=json_output)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@app.command("oom-adj")
|
|
238
|
+
def reliability_oom_adj(
|
|
239
|
+
package: str = typer.Argument(..., help="Package name"),
|
|
240
|
+
score: int = typer.Option(1000, "--score", help="oom_score_adj value"),
|
|
241
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
242
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
243
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Adjust oom_score_adj to make a process more killable (root required)."""
|
|
246
|
+
payload = require_target(device, session_id)
|
|
247
|
+
payload.update({"package": package, "score": score})
|
|
248
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
249
|
+
resp = client.request("POST", "/reliability/oom_adj", json_body=payload)
|
|
250
|
+
client.close()
|
|
251
|
+
handle_response(resp, json_output=json_output)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@app.command("trim-memory")
|
|
255
|
+
def reliability_trim_memory(
|
|
256
|
+
package: str = typer.Argument(..., help="Package name"),
|
|
257
|
+
level: str = typer.Option("RUNNING_CRITICAL", "--level", help="Trim level constant"),
|
|
258
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
259
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
260
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
261
|
+
) -> None:
|
|
262
|
+
"""Send a trim memory signal to the app."""
|
|
263
|
+
payload = require_target(device, session_id)
|
|
264
|
+
payload.update({"package": package, "level": level})
|
|
265
|
+
client = DaemonClient(timeout=RELIABILITY_TIMEOUT)
|
|
266
|
+
resp = client.request("POST", "/reliability/trim_memory", json_body=payload)
|
|
267
|
+
client.close()
|
|
268
|
+
handle_output_response(resp, json_output=json_output)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@pull_app.command("anr")
|
|
272
|
+
def reliability_pull_anr(
|
|
273
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
274
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
275
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Pull /data/anr (root required)."""
|
|
278
|
+
payload = require_target(device, session_id)
|
|
279
|
+
client = DaemonClient(timeout=RELIABILITY_PULL_TIMEOUT)
|
|
280
|
+
resp = client.request("POST", "/reliability/pull_anr", json_body=payload)
|
|
281
|
+
client.close()
|
|
282
|
+
handle_response(resp, json_output=json_output)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@pull_app.command("tombstones")
|
|
286
|
+
def reliability_pull_tombstones(
|
|
287
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
288
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
289
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Pull /data/tombstones (root required)."""
|
|
292
|
+
payload = require_target(device, session_id)
|
|
293
|
+
client = DaemonClient(timeout=RELIABILITY_PULL_TIMEOUT)
|
|
294
|
+
resp = client.request("POST", "/reliability/pull_tombstones", json_body=payload)
|
|
295
|
+
client.close()
|
|
296
|
+
handle_response(resp, json_output=json_output)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@pull_app.command("dropbox")
|
|
300
|
+
def reliability_pull_dropbox(
|
|
301
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
302
|
+
session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
303
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
304
|
+
) -> None:
|
|
305
|
+
"""Pull /data/system/dropbox (root required)."""
|
|
306
|
+
payload = require_target(device, session_id)
|
|
307
|
+
client = DaemonClient(timeout=RELIABILITY_PULL_TIMEOUT)
|
|
308
|
+
resp = client.request("POST", "/reliability/pull_dropbox", json_body=payload)
|
|
309
|
+
client.close()
|
|
310
|
+
handle_response(resp, json_output=json_output)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Session management CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from android_emu_agent.cli.daemon_client import DaemonClient, format_json
|
|
8
|
+
from android_emu_agent.cli.utils import handle_response
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Session management commands")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("start")
|
|
14
|
+
def session_start(
|
|
15
|
+
device: str = typer.Option(..., "--device", "-d", help="Device serial"),
|
|
16
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Start a new session on a device."""
|
|
19
|
+
client = DaemonClient()
|
|
20
|
+
resp = client.request("POST", "/sessions/start", json_body={"device_serial": device})
|
|
21
|
+
client.close()
|
|
22
|
+
|
|
23
|
+
data = resp.json()
|
|
24
|
+
if json_output:
|
|
25
|
+
typer.echo(format_json(data))
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if data.get("status") == "done":
|
|
29
|
+
typer.echo(data.get("session_id"))
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
handle_response(resp, json_output=json_output)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command("stop")
|
|
36
|
+
def session_stop(
|
|
37
|
+
session_id: str = typer.Argument(..., help="Session ID"),
|
|
38
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Stop a session."""
|
|
41
|
+
client = DaemonClient()
|
|
42
|
+
resp = client.request("POST", "/sessions/stop", json_body={"session_id": session_id})
|
|
43
|
+
client.close()
|
|
44
|
+
handle_response(resp, json_output=json_output)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command("info")
|
|
48
|
+
def session_info(
|
|
49
|
+
session_id: str = typer.Argument(..., help="Session ID"),
|
|
50
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Show session info."""
|
|
53
|
+
client = DaemonClient()
|
|
54
|
+
resp = client.request("GET", f"/sessions/{session_id}")
|
|
55
|
+
client.close()
|
|
56
|
+
handle_response(resp, json_output=json_output)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("list")
|
|
60
|
+
def session_list(json_output: bool = typer.Option(False, "--json", help="Output JSON")) -> None:
|
|
61
|
+
"""List active sessions."""
|
|
62
|
+
client = DaemonClient()
|
|
63
|
+
resp = client.request("GET", "/sessions")
|
|
64
|
+
client.close()
|
|
65
|
+
handle_response(resp, json_output=json_output)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""UI observation CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from android_emu_agent.cli.daemon_client import DaemonClient, format_json
|
|
8
|
+
from android_emu_agent.cli.utils import (
|
|
9
|
+
handle_response_with_pull,
|
|
10
|
+
require_target,
|
|
11
|
+
resolve_session_id,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="UI observation commands")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _validate_snapshot_flags(full: bool, raw: bool) -> str:
|
|
18
|
+
"""Validate and determine snapshot mode from CLI flags.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
full: True if --full flag was provided
|
|
22
|
+
raw: True if --raw flag was provided
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The mode string: "compact", "full", or "raw"
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
typer.BadParameter: If both --full and --raw are provided
|
|
29
|
+
"""
|
|
30
|
+
if full and raw:
|
|
31
|
+
raise typer.BadParameter("--full and --raw are mutually exclusive")
|
|
32
|
+
if raw:
|
|
33
|
+
return "raw"
|
|
34
|
+
if full:
|
|
35
|
+
return "full"
|
|
36
|
+
return "compact"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command("snapshot")
|
|
40
|
+
def ui_snapshot(
|
|
41
|
+
session_id: str = typer.Argument(..., help="Session ID"),
|
|
42
|
+
full: bool = typer.Option(False, "--full", help="Include all nodes (JSON)"),
|
|
43
|
+
raw: bool = typer.Option(False, "--raw", help="Return raw XML hierarchy"),
|
|
44
|
+
format_: str = typer.Option("json", "--format", "-f", help="Output format: json|text"),
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Take a UI snapshot.
|
|
47
|
+
|
|
48
|
+
Modes (mutually exclusive):
|
|
49
|
+
- Default (compact): Interactive elements only, JSON format
|
|
50
|
+
- --full: All elements, JSON format
|
|
51
|
+
- --raw: Original XML hierarchy string
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
mode = _validate_snapshot_flags(full, raw)
|
|
55
|
+
except typer.BadParameter as exc:
|
|
56
|
+
typer.echo(f"Error: {exc}")
|
|
57
|
+
raise typer.Exit(code=1) from None
|
|
58
|
+
|
|
59
|
+
client = DaemonClient()
|
|
60
|
+
resp = client.request(
|
|
61
|
+
"POST",
|
|
62
|
+
"/ui/snapshot",
|
|
63
|
+
json_body={"session_id": session_id, "mode": mode},
|
|
64
|
+
)
|
|
65
|
+
client.close()
|
|
66
|
+
|
|
67
|
+
content_type = resp.headers.get("content-type", "")
|
|
68
|
+
if "application/xml" in content_type:
|
|
69
|
+
typer.echo(resp.text)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
data = resp.json()
|
|
73
|
+
if format_ == "text" and not data.get("error"):
|
|
74
|
+
elements = data.get("elements", [])
|
|
75
|
+
header = (
|
|
76
|
+
f"session={data.get('session_id')} generation={data.get('generation')} "
|
|
77
|
+
f"elements={len(elements)}"
|
|
78
|
+
)
|
|
79
|
+
typer.echo(header)
|
|
80
|
+
for elem in elements:
|
|
81
|
+
label = elem.get("label") or elem.get("text") or ""
|
|
82
|
+
role = elem.get("role") or "element"
|
|
83
|
+
rid = elem.get("resource_id") or "-"
|
|
84
|
+
typer.echo(f"{elem['ref']} {role} '{label}' id={rid} bounds={elem.get('bounds')}")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
typer.echo(format_json(data))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.command("screenshot")
|
|
91
|
+
def ui_screenshot(
|
|
92
|
+
session_id: str | None = typer.Argument(None, help="Session ID"),
|
|
93
|
+
device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
|
|
94
|
+
session: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
95
|
+
pull: bool = typer.Option(False, "--pull", help="Copy screenshot to local path"),
|
|
96
|
+
output: str | None = typer.Option(
|
|
97
|
+
None, "--output", "-o", help="Output path (file or directory)"
|
|
98
|
+
),
|
|
99
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Capture a screenshot."""
|
|
102
|
+
try:
|
|
103
|
+
resolved_session = resolve_session_id(session_id, session)
|
|
104
|
+
except typer.BadParameter as exc:
|
|
105
|
+
typer.echo(f"Error: {exc}")
|
|
106
|
+
raise typer.Exit(code=1) from None
|
|
107
|
+
|
|
108
|
+
payload = require_target(device, resolved_session)
|
|
109
|
+
client = DaemonClient()
|
|
110
|
+
resp = client.request("POST", "/ui/screenshot", json_body=payload)
|
|
111
|
+
client.close()
|
|
112
|
+
handle_response_with_pull(resp, json_output=json_output, pull=pull, output=output)
|