fh_tool-cli 0.2.3__tar.gz → 0.2.4__tar.gz
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.
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/PKG-INFO +17 -1
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/README.md +16 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/pyproject.toml +1 -1
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/cli.py +42 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/click_cli.py +9 -1
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/parser.py +36 -0
- fh_tool_cli-0.2.4/src/fh_tool_cli/vm_build.py +458 -0
- fh_tool_cli-0.2.4/src/fh_tool_cli/vm_collect.py +330 -0
- fh_tool_cli-0.2.4/src/fh_tool_cli/vm_common.py +143 -0
- fh_tool_cli-0.2.4/src/fh_tool_cli/vm_verify.py +273 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli.egg-info/PKG-INFO +17 -1
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli.egg-info/SOURCES.txt +7 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_parser_registration.py +3 -0
- fh_tool_cli-0.2.4/tests/test_vm_build.py +126 -0
- fh_tool_cli-0.2.4/tests/test_vm_collect.py +115 -0
- fh_tool_cli-0.2.4/tests/test_vm_verify.py +73 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/setup.cfg +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/__init__.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/account.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/argparse_utils.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/backends/__init__.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/backends/cfg_cmd.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/backends/fh_tool.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/backends/local_vm.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/backends/telnet.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/backends/web_ajax.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/backup.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/client.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/commands/__init__.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/commands/web.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/config_decrypt.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/config_store.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/credential_sources.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/credentials.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/crypto.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/diagnostics.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/errors.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/output.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/remote.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/risk.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/upload.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/web_discovery.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli/web_writes.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli.egg-info/dependency_links.txt +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli.egg-info/entry_points.txt +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli.egg-info/requires.txt +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/src/fh_tool_cli.egg-info/top_level.txt +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_account.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_backup.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_cfg_cmd.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_cli_risk.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_click_cli.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_config_decrypt.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_credentials.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_diagnostics.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_extracted_helpers.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_local_vm_integration.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_remote.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_telnet_backend.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_upload.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_web_ajax.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_web_ajax_post.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_web_discovery.py +0 -0
- {fh_tool_cli-0.2.3 → fh_tool_cli-0.2.4}/tests/test_web_write_payloads.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fh_tool-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: 用于本地管理 FiberHome fh_tool 接口的 Python CLI
|
|
5
5
|
Author: Gxxk
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -151,6 +151,18 @@ fh-tool wan list --backend local-vm
|
|
|
151
151
|
|
|
152
152
|
`local-vm` 只是在本机 proot VM 内执行厂商 `cfg_cmd`。`--vm-root` 必须指向 VM 工作区目录,也就是包含 `bin/proot-shell` 和 `rootfs-vm/fhrom/bin/cfg_cmd` 的目录;默认是 `/mnt/dev-cold/HG5143F-ONU-vm`。不要把它指到里面的 `rootfs-vm/`。
|
|
153
153
|
|
|
154
|
+
也可以从 HG5143F 原始 MTD dump 构建同类 userspace VM。`collect` 默认只输出 dry-run 计划,不读 flash;真正采集必须加 `--confirm`,自动开启 Telnet 也必须显式加 `--auto-enable-telnet --confirm`:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
fh-tool vm collect --ip 192.168.1.1 --output ./hg5143f-dumps
|
|
158
|
+
fh-tool vm collect --ip 192.168.1.1 --output ./hg5143f-dumps --auto-enable-telnet --confirm
|
|
159
|
+
fh-tool vm build --dump-dir ./hg5143f-dumps --output ./HG5143F-ONU-vm
|
|
160
|
+
fh-tool vm verify --vm-root ./HG5143F-ONU-vm
|
|
161
|
+
fh-tool vm verify --vm-root ./HG5143F-ONU-vm --with-fhapi --with-http
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`vm build` 需要 `ubireader_extract_images`、`ubireader_extract_files`、`unsquashfs`、`jefferson`、`qemu-arm-static` 和 `proot`。输出目录会包含 `rootfs-vm/`、`source/`、`logs/`、`bin/`、`build-manifest.json`、`verify-manifest.json` 和 `events.ndjson`。生成的 VM 是 32-bit ARM userspace under `qemu-arm-static`/`proot -0`,不是完整板级 QEMU 启动;默认不会运行完整 `/etc/rc.d/rcS`。
|
|
165
|
+
|
|
154
166
|
Web AJAX 读取命令只读。需要复用已有 Web session 时加 `--sessionid`;需要登录时可显式传 `--password-stdin` 或 `--password`。未显式提供密码时默认 `--username useradmin --password-source auto`,会依次尝试 `GetAdminAccount` 和 cfg 路径读取 Web superadmin 密码;cfg 来源需要 Telnet 时会默认使用 HG5143F 派生 Telnet 凭据 fallback,可用 `--no-derived-credentials` 关闭。失败不会阻塞只读抓取,并会在结果里返回脱敏 login summary。sessionid、密码、LOID、PPPoE 等敏感字段默认会脱敏;只有显式加 `--reveal-secrets` 才输出明文。
|
|
155
167
|
|
|
156
168
|
后台 AJAX 接口发现以 live discovery 为主路径,不需要 HAR,也不需要 rootfs:
|
|
@@ -346,3 +358,7 @@ fh-tool telnet disable --confirm
|
|
|
346
358
|
## License
|
|
347
359
|
|
|
348
360
|
AGPL-3.0-or-later
|
|
361
|
+
|
|
362
|
+
## 其它
|
|
363
|
+
|
|
364
|
+
[认可 LINUX DO](https://linux.do)
|
|
@@ -135,6 +135,18 @@ fh-tool wan list --backend local-vm
|
|
|
135
135
|
|
|
136
136
|
`local-vm` 只是在本机 proot VM 内执行厂商 `cfg_cmd`。`--vm-root` 必须指向 VM 工作区目录,也就是包含 `bin/proot-shell` 和 `rootfs-vm/fhrom/bin/cfg_cmd` 的目录;默认是 `/mnt/dev-cold/HG5143F-ONU-vm`。不要把它指到里面的 `rootfs-vm/`。
|
|
137
137
|
|
|
138
|
+
也可以从 HG5143F 原始 MTD dump 构建同类 userspace VM。`collect` 默认只输出 dry-run 计划,不读 flash;真正采集必须加 `--confirm`,自动开启 Telnet 也必须显式加 `--auto-enable-telnet --confirm`:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
fh-tool vm collect --ip 192.168.1.1 --output ./hg5143f-dumps
|
|
142
|
+
fh-tool vm collect --ip 192.168.1.1 --output ./hg5143f-dumps --auto-enable-telnet --confirm
|
|
143
|
+
fh-tool vm build --dump-dir ./hg5143f-dumps --output ./HG5143F-ONU-vm
|
|
144
|
+
fh-tool vm verify --vm-root ./HG5143F-ONU-vm
|
|
145
|
+
fh-tool vm verify --vm-root ./HG5143F-ONU-vm --with-fhapi --with-http
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`vm build` 需要 `ubireader_extract_images`、`ubireader_extract_files`、`unsquashfs`、`jefferson`、`qemu-arm-static` 和 `proot`。输出目录会包含 `rootfs-vm/`、`source/`、`logs/`、`bin/`、`build-manifest.json`、`verify-manifest.json` 和 `events.ndjson`。生成的 VM 是 32-bit ARM userspace under `qemu-arm-static`/`proot -0`,不是完整板级 QEMU 启动;默认不会运行完整 `/etc/rc.d/rcS`。
|
|
149
|
+
|
|
138
150
|
Web AJAX 读取命令只读。需要复用已有 Web session 时加 `--sessionid`;需要登录时可显式传 `--password-stdin` 或 `--password`。未显式提供密码时默认 `--username useradmin --password-source auto`,会依次尝试 `GetAdminAccount` 和 cfg 路径读取 Web superadmin 密码;cfg 来源需要 Telnet 时会默认使用 HG5143F 派生 Telnet 凭据 fallback,可用 `--no-derived-credentials` 关闭。失败不会阻塞只读抓取,并会在结果里返回脱敏 login summary。sessionid、密码、LOID、PPPoE 等敏感字段默认会脱敏;只有显式加 `--reveal-secrets` 才输出明文。
|
|
139
151
|
|
|
140
152
|
后台 AJAX 接口发现以 live discovery 为主路径,不需要 HAR,也不需要 rootfs:
|
|
@@ -330,3 +342,7 @@ fh-tool telnet disable --confirm
|
|
|
330
342
|
## License
|
|
331
343
|
|
|
332
344
|
AGPL-3.0-or-later
|
|
345
|
+
|
|
346
|
+
## 其它
|
|
347
|
+
|
|
348
|
+
[认可 LINUX DO](https://linux.do)
|
|
@@ -101,6 +101,9 @@ from .upload import (
|
|
|
101
101
|
upload_workflow_plan,
|
|
102
102
|
)
|
|
103
103
|
from .output import emit
|
|
104
|
+
from .vm_build import build_vm
|
|
105
|
+
from .vm_collect import collect_vm_dumps
|
|
106
|
+
from .vm_verify import verify_vm
|
|
104
107
|
|
|
105
108
|
|
|
106
109
|
SENSITIVE_PLAN_KEY_RE = re.compile(
|
|
@@ -294,6 +297,42 @@ def command_backup_verify(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
294
297
|
return verify_backup(Path(args.path).expanduser())
|
|
295
298
|
|
|
296
299
|
|
|
300
|
+
def command_vm_collect(args: argparse.Namespace) -> dict[str, Any]:
|
|
301
|
+
confirmed = is_confirmed(args)
|
|
302
|
+
result_prefix: dict[str, Any] = {}
|
|
303
|
+
if confirmed and args.auto_enable_telnet:
|
|
304
|
+
result_prefix["auto_enable_telnet"] = call_method(args, "TelnetEnable", {"telnet": "1"})
|
|
305
|
+
result = collect_vm_dumps(
|
|
306
|
+
shell_runner=_telnet_root_shell_runner_from_args(args) if confirmed else (lambda _command: ""),
|
|
307
|
+
output_dir=Path(args.output).expanduser(),
|
|
308
|
+
confirmed=confirmed,
|
|
309
|
+
all_mtd=args.all_mtd,
|
|
310
|
+
requested_partitions=args.partition,
|
|
311
|
+
chunk_size=args.chunk_size,
|
|
312
|
+
retries=args.retries,
|
|
313
|
+
)
|
|
314
|
+
if result_prefix:
|
|
315
|
+
result.update(result_prefix)
|
|
316
|
+
return result
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def command_vm_build(args: argparse.Namespace) -> dict[str, Any]:
|
|
320
|
+
return build_vm(
|
|
321
|
+
dump_dir=Path(args.dump_dir).expanduser(),
|
|
322
|
+
output_dir=Path(args.output).expanduser(),
|
|
323
|
+
rootfs_slot=args.rootfs_slot,
|
|
324
|
+
force=args.force,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def command_vm_verify(args: argparse.Namespace) -> dict[str, Any]:
|
|
329
|
+
return verify_vm(
|
|
330
|
+
vm_root=Path(args.vm_root).expanduser(),
|
|
331
|
+
with_fhapi=args.with_fhapi,
|
|
332
|
+
with_http=args.with_http,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
297
336
|
def command_restore_backup(args: argparse.Namespace) -> dict[str, Any]:
|
|
298
337
|
dry_run = not is_confirmed(args)
|
|
299
338
|
if args.target == "device":
|
|
@@ -981,6 +1020,9 @@ def _command_handlers() -> dict[str, Any]:
|
|
|
981
1020
|
"command_config_decrypt": command_config_decrypt,
|
|
982
1021
|
"command_backup_create": command_backup_create,
|
|
983
1022
|
"command_backup_verify": command_backup_verify,
|
|
1023
|
+
"command_vm_collect": command_vm_collect,
|
|
1024
|
+
"command_vm_build": command_vm_build,
|
|
1025
|
+
"command_vm_verify": command_vm_verify,
|
|
984
1026
|
"command_restore_backup": command_restore_backup,
|
|
985
1027
|
"command_credentials_derive": command_credentials_derive,
|
|
986
1028
|
"command_cfg_get": command_cfg_get,
|
|
@@ -257,6 +257,14 @@ def _parameter_from_action(action: argparse.Action) -> click.Parameter | None:
|
|
|
257
257
|
|
|
258
258
|
|
|
259
259
|
def _option_from_action(action: argparse.Action) -> click.Option:
|
|
260
|
+
option_strings = list(action.option_strings)
|
|
261
|
+
inferred_names = {
|
|
262
|
+
option.lstrip("-").replace("-", "_")
|
|
263
|
+
for option in option_strings
|
|
264
|
+
if option.startswith("--")
|
|
265
|
+
}
|
|
266
|
+
if action.dest not in inferred_names:
|
|
267
|
+
option_strings.append(action.dest)
|
|
260
268
|
kwargs: dict[str, Any] = {
|
|
261
269
|
"help": None if action.help is argparse.SUPPRESS else action.help,
|
|
262
270
|
"hidden": action.help is argparse.SUPPRESS,
|
|
@@ -279,7 +287,7 @@ def _option_from_action(action: argparse.Action) -> click.Option:
|
|
|
279
287
|
type=_type_from_action(action),
|
|
280
288
|
)
|
|
281
289
|
|
|
282
|
-
return click.Option(
|
|
290
|
+
return click.Option(option_strings, **kwargs)
|
|
283
291
|
|
|
284
292
|
|
|
285
293
|
def _argument_from_action(action: argparse.Action) -> click.Argument:
|
|
@@ -217,6 +217,42 @@ def build_parser(handlers: HandlerMap) -> argparse.ArgumentParser:
|
|
|
217
217
|
backup_verify.add_argument("--json", action="store_true", help="输出 machine-readable JSON")
|
|
218
218
|
backup_verify.set_defaults(handler=_handler(handlers, "command_backup_verify"))
|
|
219
219
|
|
|
220
|
+
vm = subparsers.add_parser("vm", help="采集、构建和验证 HG5143F userspace VM")
|
|
221
|
+
vm_subparsers = vm.add_subparsers(
|
|
222
|
+
dest="vm_command",
|
|
223
|
+
required=True,
|
|
224
|
+
metavar="SUBCOMMAND",
|
|
225
|
+
title="vm commands",
|
|
226
|
+
)
|
|
227
|
+
vm_collect = vm_subparsers.add_parser("collect", help="通过 Telnet 安全采集 VM 所需 MTD dump")
|
|
228
|
+
add_telnet_options(vm_collect)
|
|
229
|
+
for action in vm_collect._actions:
|
|
230
|
+
if action.dest == "timeout":
|
|
231
|
+
action.default = 60.0
|
|
232
|
+
action.help = "Telnet command timeout 秒数,默认 60"
|
|
233
|
+
break
|
|
234
|
+
add_write_confirmation(vm_collect)
|
|
235
|
+
vm_collect.add_argument("--output", required=True, help="写入 dump 目录")
|
|
236
|
+
vm_collect.add_argument("--auto-enable-telnet", action="store_true", help="确认后先调用 TelnetEnable=1")
|
|
237
|
+
vm_collect.add_argument("--all-mtd", action="store_true", help="采集 mtd0-mtd7 物理分区")
|
|
238
|
+
vm_collect.add_argument("--partition", action="append", default=[], help="只采集指定 mtdN/name,可重复")
|
|
239
|
+
vm_collect.add_argument("--chunk-size", type=int, default=262144, help="nanddump/base64 chunk bytes,默认 262144")
|
|
240
|
+
vm_collect.add_argument("--retries", type=int, default=3, help="chunk decode/长度不匹配重试次数,默认 3")
|
|
241
|
+
vm_collect.set_defaults(handler=_handler(handlers, "command_vm_collect"))
|
|
242
|
+
vm_build = vm_subparsers.add_parser("build", help="从 dump 构建本地 proot/qemu-arm-static VM")
|
|
243
|
+
vm_build.add_argument("--dump-dir", required=True, help="包含 MTD dump 的目录")
|
|
244
|
+
vm_build.add_argument("--output", required=True, help="写入 VM 工作区目录")
|
|
245
|
+
vm_build.add_argument("--rootfs-slot", choices=["active", "A", "B"], default="active", help="rootfs slot,默认 active")
|
|
246
|
+
vm_build.add_argument("--force", action="store_true", help="覆盖已有输出目录")
|
|
247
|
+
vm_build.add_argument("--json", action="store_true", help="输出 machine-readable JSON")
|
|
248
|
+
vm_build.set_defaults(handler=_handler(handlers, "command_vm_build"))
|
|
249
|
+
vm_verify = vm_subparsers.add_parser("verify", help="验证本地 VM 工作区")
|
|
250
|
+
vm_verify.add_argument("--vm-root", required=True, help="VM 工作区目录,不是 rootfs-vm 子目录")
|
|
251
|
+
vm_verify.add_argument("--with-fhapi", action="store_true", help="启动 ubusd + cfgmgr 并验证 cfg_cmd read")
|
|
252
|
+
vm_verify.add_argument("--with-http", action="store_true", help="端口空闲时启动并验证 HTTP stack")
|
|
253
|
+
vm_verify.add_argument("--json", action="store_true", help="输出 machine-readable JSON")
|
|
254
|
+
vm_verify.set_defaults(handler=_handler(handlers, "command_vm_verify"))
|
|
255
|
+
|
|
220
256
|
restore_backup = subparsers.add_parser("restore", help="从 backup archive 安全恢复 allowlist 配置")
|
|
221
257
|
add_telnet_options(restore_backup)
|
|
222
258
|
restore_backup.add_argument("backup", help="fh-tool backup 生成的 backup.tgz")
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .errors import FHToolError
|
|
10
|
+
from .vm_common import (
|
|
11
|
+
VM_SCHEMA_VERSION,
|
|
12
|
+
append_event,
|
|
13
|
+
copy_tree_contents,
|
|
14
|
+
require_empty_or_force,
|
|
15
|
+
require_host_tools,
|
|
16
|
+
run_command,
|
|
17
|
+
sha256_file,
|
|
18
|
+
utc_now,
|
|
19
|
+
write_json,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
BUILD_TOOLS = [
|
|
24
|
+
"ubireader_extract_images",
|
|
25
|
+
"ubireader_extract_files",
|
|
26
|
+
"unsquashfs",
|
|
27
|
+
"jefferson",
|
|
28
|
+
"qemu-arm-static",
|
|
29
|
+
"proot",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
FACTORY_JFFS2_OFFSET = 0x380000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_vm(
|
|
36
|
+
*,
|
|
37
|
+
dump_dir: Path,
|
|
38
|
+
output_dir: Path,
|
|
39
|
+
rootfs_slot: str,
|
|
40
|
+
force: bool,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
dump_dir = dump_dir.resolve()
|
|
43
|
+
if not dump_dir.is_dir():
|
|
44
|
+
raise FHToolError(f"dump dir is not a directory: {dump_dir}")
|
|
45
|
+
if rootfs_slot not in {"active", "A", "B"}:
|
|
46
|
+
raise FHToolError("--rootfs-slot must be active, A, or B")
|
|
47
|
+
require_empty_or_force(output_dir, force=force)
|
|
48
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
rootfs_dir = output_dir / "rootfs-vm"
|
|
50
|
+
source_dir = output_dir / "source"
|
|
51
|
+
logs_dir = output_dir / "logs"
|
|
52
|
+
bin_dir = output_dir / "bin"
|
|
53
|
+
for directory in (rootfs_dir, source_dir, logs_dir, bin_dir):
|
|
54
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
|
|
56
|
+
events_path = output_dir / "events.ndjson"
|
|
57
|
+
append_event(events_path, "build.start", dump_dir=str(dump_dir), output_dir=str(output_dir))
|
|
58
|
+
tools = require_host_tools(BUILD_TOOLS)
|
|
59
|
+
|
|
60
|
+
manifest = _load_dump_manifest(dump_dir)
|
|
61
|
+
selected_slot, slot_warning = select_rootfs_slot(rootfs_slot, manifest=manifest)
|
|
62
|
+
warnings: list[str] = []
|
|
63
|
+
if slot_warning:
|
|
64
|
+
warnings.append(slot_warning)
|
|
65
|
+
rootfs_dump = find_dump(dump_dir, index=0 if selected_slot == "A" else 1, name_contains=f"rootfs{selected_slot}")
|
|
66
|
+
data_dump = find_dump(dump_dir, index=2, name_contains="data", prefer_contains="padbad")
|
|
67
|
+
factory_dump = find_dump(dump_dir, index=6, name_contains="factory")
|
|
68
|
+
append_event(
|
|
69
|
+
events_path,
|
|
70
|
+
"build.dumps.selected",
|
|
71
|
+
rootfs=str(rootfs_dump),
|
|
72
|
+
data=str(data_dump),
|
|
73
|
+
factory=str(factory_dump),
|
|
74
|
+
slot=selected_slot,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
rootfs_image = _extract_rootfs_image(rootfs_dump, logs_dir, events_path)
|
|
78
|
+
_extract_squashfs_root(rootfs_image, rootfs_dir, logs_dir, events_path)
|
|
79
|
+
_extract_data_volumes(data_dump, rootfs_dir, logs_dir, events_path)
|
|
80
|
+
_extract_factory(factory_dump, source_dir, rootfs_dir, logs_dir, events_path)
|
|
81
|
+
_normalize_runtime(rootfs_dir)
|
|
82
|
+
_write_scripts(bin_dir)
|
|
83
|
+
_write_workspace_readme(output_dir)
|
|
84
|
+
|
|
85
|
+
outputs = {
|
|
86
|
+
"rootfs": str(rootfs_dir),
|
|
87
|
+
"source": str(source_dir),
|
|
88
|
+
"logs": str(logs_dir),
|
|
89
|
+
"bin": str(bin_dir),
|
|
90
|
+
"manifest": str(output_dir / "build-manifest.json"),
|
|
91
|
+
"events": str(events_path),
|
|
92
|
+
}
|
|
93
|
+
source_sha = {
|
|
94
|
+
"rootfs": sha256_file(rootfs_dump),
|
|
95
|
+
"data": sha256_file(data_dump),
|
|
96
|
+
"factory": sha256_file(factory_dump),
|
|
97
|
+
}
|
|
98
|
+
result: dict[str, Any] = {
|
|
99
|
+
"schema_version": VM_SCHEMA_VERSION,
|
|
100
|
+
"command": "vm build",
|
|
101
|
+
"created_at": utc_now(),
|
|
102
|
+
"ok": True,
|
|
103
|
+
"dump_dir": str(dump_dir),
|
|
104
|
+
"output_dir": str(output_dir),
|
|
105
|
+
"rootfs_slot": selected_slot,
|
|
106
|
+
"host_tools": tools,
|
|
107
|
+
"source_dumps": {
|
|
108
|
+
"rootfs": str(rootfs_dump),
|
|
109
|
+
"data": str(data_dump),
|
|
110
|
+
"factory": str(factory_dump),
|
|
111
|
+
},
|
|
112
|
+
"source_sha256": source_sha,
|
|
113
|
+
"outputs": outputs,
|
|
114
|
+
"warnings": warnings,
|
|
115
|
+
"limitations": [
|
|
116
|
+
"This is a userspace proot/qemu-arm-static VM, not a full board-level QEMU boot.",
|
|
117
|
+
"Full /etc/rc.d/rcS is intentionally not used by default.",
|
|
118
|
+
"mtd3 nvram is not merged into the VM rootfs.",
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
write_json(output_dir / "build-manifest.json", result)
|
|
122
|
+
append_event(events_path, "build.done", ok=True)
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def select_rootfs_slot(rootfs_slot: str, *, manifest: dict[str, Any] | None = None) -> tuple[str, str | None]:
|
|
127
|
+
if rootfs_slot in {"A", "B"}:
|
|
128
|
+
return rootfs_slot, None
|
|
129
|
+
text = json.dumps(manifest or {}, ensure_ascii=False)
|
|
130
|
+
lowered = text.lower()
|
|
131
|
+
if "rootfsb" in lowered or "rootfs_b" in lowered or "active_flag=b" in lowered or '"active_flag": "b"' in lowered:
|
|
132
|
+
return "B", None
|
|
133
|
+
if "rootfsa" in lowered or "rootfs_a" in lowered or "active_flag=a" in lowered or '"active_flag": "a"' in lowered:
|
|
134
|
+
return "A", None
|
|
135
|
+
return "A", "active rootfs slot was not found in dump metadata; defaulting to rootfsA"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def find_dump(dump_dir: Path, *, index: int, name_contains: str, prefer_contains: str | None = None) -> Path:
|
|
139
|
+
candidates = [
|
|
140
|
+
path
|
|
141
|
+
for path in dump_dir.rglob("*.bin")
|
|
142
|
+
if path.is_file()
|
|
143
|
+
and (path.name.startswith(f"mtd{index}_") or path.name.startswith(f"mtd{index:02d}_"))
|
|
144
|
+
and name_contains.lower() in path.name.lower()
|
|
145
|
+
]
|
|
146
|
+
if not candidates:
|
|
147
|
+
raise FHToolError(f"missing mtd{index} {name_contains} dump in {dump_dir}")
|
|
148
|
+
if prefer_contains:
|
|
149
|
+
preferred = [path for path in candidates if prefer_contains.lower() in path.name.lower()]
|
|
150
|
+
if preferred:
|
|
151
|
+
return sorted(preferred, key=lambda item: (len(str(item)), str(item)))[0]
|
|
152
|
+
return sorted(candidates, key=lambda item: (len(str(item)), str(item)))[0]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _load_dump_manifest(dump_dir: Path) -> dict[str, Any] | None:
|
|
156
|
+
for name in ("dump-manifest.json", "manifest.json"):
|
|
157
|
+
path = dump_dir / name
|
|
158
|
+
if path.is_file():
|
|
159
|
+
try:
|
|
160
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
161
|
+
except json.JSONDecodeError:
|
|
162
|
+
return None
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _extract_rootfs_image(rootfs_dump: Path, logs_dir: Path, events_path: Path) -> Path:
|
|
167
|
+
output = logs_dir / "rootfs_ubi_images"
|
|
168
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
run_command(
|
|
170
|
+
["ubireader_extract_images", "-o", str(output), str(rootfs_dump)],
|
|
171
|
+
log_path=logs_dir / "ubireader-rootfs-images.log",
|
|
172
|
+
)
|
|
173
|
+
images = sorted(output.rglob("*vol-rootfs_ubifs*.ubifs"))
|
|
174
|
+
if not images:
|
|
175
|
+
raise FHToolError("rootfs_ubifs volume image was not extracted")
|
|
176
|
+
append_event(events_path, "build.rootfs_image.extracted", image=str(images[0]))
|
|
177
|
+
return images[0]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _extract_squashfs_root(rootfs_image: Path, rootfs_dir: Path, logs_dir: Path, events_path: Path) -> None:
|
|
181
|
+
run_command(
|
|
182
|
+
["unsquashfs", "-f", "-d", str(rootfs_dir), str(rootfs_image)],
|
|
183
|
+
log_path=logs_dir / "unsquashfs-rootfs.log",
|
|
184
|
+
)
|
|
185
|
+
append_event(events_path, "build.rootfs.extracted", rootfs=str(rootfs_dir))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _extract_data_volumes(data_dump: Path, rootfs_dir: Path, logs_dir: Path, events_path: Path) -> None:
|
|
189
|
+
output = logs_dir / "data_ubi_files"
|
|
190
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
run_command(
|
|
192
|
+
["ubireader_extract_files", "-o", str(output), str(data_dump)],
|
|
193
|
+
log_path=logs_dir / "ubireader-data-files.log",
|
|
194
|
+
)
|
|
195
|
+
apps = _find_extracted_volume(output, "apps")
|
|
196
|
+
userdata = _find_extracted_volume(output, "userdata")
|
|
197
|
+
copy_tree_contents(apps, rootfs_dir / "opt" / "upt" / "apps")
|
|
198
|
+
copy_tree_contents(userdata, rootfs_dir / "fhconf")
|
|
199
|
+
append_event(events_path, "build.data.extracted", apps=str(apps), userdata=str(userdata))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _extract_factory(factory_dump: Path, source_dir: Path, rootfs_dir: Path, logs_dir: Path, events_path: Path) -> None:
|
|
203
|
+
factory_jffs2 = source_dir / "mtd6_factory_0x380000.jffs2"
|
|
204
|
+
with factory_dump.open("rb") as source, factory_jffs2.open("wb") as target:
|
|
205
|
+
source.seek(FACTORY_JFFS2_OFFSET)
|
|
206
|
+
shutil.copyfileobj(source, target)
|
|
207
|
+
factory_out = source_dir / "factory_jffs2"
|
|
208
|
+
run_command(
|
|
209
|
+
["jefferson", "-f", "-d", str(factory_out), str(factory_jffs2)],
|
|
210
|
+
log_path=logs_dir / "jefferson-factory.log",
|
|
211
|
+
)
|
|
212
|
+
copy_tree_contents(factory_out, rootfs_dir / "fhdata")
|
|
213
|
+
append_event(events_path, "build.factory.extracted", factory=str(factory_out))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _find_extracted_volume(root: Path, name: str) -> Path:
|
|
217
|
+
matches = sorted(path for path in root.rglob(name) if path.is_dir() and path.name == name)
|
|
218
|
+
if not matches:
|
|
219
|
+
raise FHToolError(f"UBI volume was not extracted: {name}")
|
|
220
|
+
return matches[0]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _normalize_runtime(rootfs_dir: Path) -> None:
|
|
224
|
+
for directory in (
|
|
225
|
+
rootfs_dir / "tmp",
|
|
226
|
+
rootfs_dir / "var" / "run",
|
|
227
|
+
rootfs_dir / "var" / "log",
|
|
228
|
+
rootfs_dir / "opt" / "upt" / "apps",
|
|
229
|
+
rootfs_dir / "fhconf",
|
|
230
|
+
rootfs_dir / "fhdata",
|
|
231
|
+
):
|
|
232
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
busybox = rootfs_dir / "bin" / "busybox"
|
|
234
|
+
if busybox.exists():
|
|
235
|
+
for relative in (
|
|
236
|
+
"bin/sh",
|
|
237
|
+
"bin/ash",
|
|
238
|
+
"bin/cat",
|
|
239
|
+
"bin/chmod",
|
|
240
|
+
"bin/cp",
|
|
241
|
+
"bin/grep",
|
|
242
|
+
"bin/ln",
|
|
243
|
+
"bin/ls",
|
|
244
|
+
"bin/mkdir",
|
|
245
|
+
"bin/mount",
|
|
246
|
+
"bin/rm",
|
|
247
|
+
"bin/sleep",
|
|
248
|
+
"bin/test",
|
|
249
|
+
"usr/bin/env",
|
|
250
|
+
"usr/bin/awk",
|
|
251
|
+
"usr/bin/head",
|
|
252
|
+
"sbin/ifconfig",
|
|
253
|
+
):
|
|
254
|
+
path = rootfs_dir / relative
|
|
255
|
+
if path.exists() and (path.is_symlink() or path.stat().st_size > 0):
|
|
256
|
+
continue
|
|
257
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
if path.exists():
|
|
259
|
+
path.unlink()
|
|
260
|
+
target = os.path.relpath(busybox, start=path.parent)
|
|
261
|
+
path.symlink_to(target)
|
|
262
|
+
for preserved in ("passwd_old", "shadow_old", "group_old"):
|
|
263
|
+
path = rootfs_dir / "etc" / preserved
|
|
264
|
+
if not path.exists() and (rootfs_dir / "etc" / preserved.removesuffix("_old")).exists():
|
|
265
|
+
shutil.copy2(rootfs_dir / "etc" / preserved.removesuffix("_old"), path)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _write_scripts(bin_dir: Path) -> None:
|
|
269
|
+
scripts = {
|
|
270
|
+
"qemu-exec": QEMU_EXEC_SCRIPT,
|
|
271
|
+
"prepare-runtime": PREPARE_RUNTIME_SCRIPT,
|
|
272
|
+
"proot-shell": PROOT_SHELL_SCRIPT,
|
|
273
|
+
"start-fhapi-proot": START_FHAPI_SCRIPT,
|
|
274
|
+
"stop-fhapi-proot": STOP_FHAPI_SCRIPT,
|
|
275
|
+
"start-http-stack-proot": START_HTTP_SCRIPT,
|
|
276
|
+
"verify": VERIFY_SCRIPT,
|
|
277
|
+
}
|
|
278
|
+
for name, content in scripts.items():
|
|
279
|
+
path = bin_dir / name
|
|
280
|
+
path.write_text(content, encoding="utf-8")
|
|
281
|
+
path.chmod(0o755)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _write_workspace_readme(output_dir: Path) -> None:
|
|
285
|
+
(output_dir / "README.md").write_text(
|
|
286
|
+
"# HG5143F userspace VM\n\n"
|
|
287
|
+
"Run `./bin/verify` for local checks. Use `./bin/proot-shell` for a root shell in the extracted firmware.\n"
|
|
288
|
+
"This workspace is a proot/qemu-arm-static userspace VM, not a full board-level QEMU boot.\n",
|
|
289
|
+
encoding="utf-8",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
QEMU_EXEC_SCRIPT = """#!/usr/bin/env sh
|
|
294
|
+
set -eu
|
|
295
|
+
BASE_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
296
|
+
ROOTFS="$BASE_DIR/rootfs-vm"
|
|
297
|
+
QEMU=${QEMU:-qemu-arm-static}
|
|
298
|
+
command -v "$QEMU" >/dev/null 2>&1 || { echo "missing $QEMU" >&2; exit 127; }
|
|
299
|
+
[ "$#" -gt 0 ] || set -- /bin/busybox --help
|
|
300
|
+
case "$1" in /*) guest_path=$1 ;; *) guest_path="/$1" ;; esac
|
|
301
|
+
shift
|
|
302
|
+
exec "$QEMU" -L "$ROOTFS" \
|
|
303
|
+
-E PATH=/fhrom/fhshell:/usr/bin:/usr/sbin:/bin:/sbin:/fhrom/bin \
|
|
304
|
+
-E LD_LIBRARY_PATH=/lib:/usr/lib:/fhrom/lib:/usr/lib/glib-2.0:/opt/upt/apps/lib:/opt/upt/apps/usr/lib \
|
|
305
|
+
"$ROOTFS$guest_path" "$@"
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
PREPARE_RUNTIME_SCRIPT = """#!/usr/bin/env sh
|
|
309
|
+
set -eu
|
|
310
|
+
BASE_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
311
|
+
ROOTFS="$BASE_DIR/rootfs-vm"
|
|
312
|
+
mkdir -p "$ROOTFS/tmp" "$ROOTFS/var/run" "$ROOTFS/var/log"
|
|
313
|
+
[ ! -f "$ROOTFS/etc/passwd_old" ] || cp -f "$ROOTFS/etc/passwd_old" "$ROOTFS/tmp/passwd"
|
|
314
|
+
[ ! -f "$ROOTFS/etc/shadow_old" ] || cp -f "$ROOTFS/etc/shadow_old" "$ROOTFS/tmp/shadow"
|
|
315
|
+
[ ! -f "$ROOTFS/etc/group_old" ] || cp -f "$ROOTFS/etc/group_old" "$ROOTFS/tmp/group"
|
|
316
|
+
if [ ! -f "$ROOTFS/tmp/InternetGatewayDevice/DeviceInfo/property.conf" ] && [ -x "$ROOTFS/fhrom/bin/cfg_tool" ]; then
|
|
317
|
+
proot -0 -q "${QEMU:-qemu-arm-static}" -r "$ROOTFS" -w / \
|
|
318
|
+
-b /dev/null:/dev/null -b /dev/zero:/dev/zero -b /dev/random:/dev/random -b /dev/urandom:/dev/urandom \
|
|
319
|
+
/usr/bin/env PATH=/fhrom/fhshell:/usr/bin:/usr/sbin:/bin:/sbin:/fhrom/bin \
|
|
320
|
+
LD_LIBRARY_PATH=/lib:/usr/lib:/fhrom/lib:/usr/lib/glib-2.0:/opt/upt/apps/lib:/opt/upt/apps/usr/lib \
|
|
321
|
+
/fhrom/bin/cfg_tool /fhrom/fhconf/param.tar.gz.enc >/dev/null 2>&1 || true
|
|
322
|
+
fi
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
PROOT_SHELL_SCRIPT = """#!/usr/bin/env sh
|
|
326
|
+
set -eu
|
|
327
|
+
BASE_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
328
|
+
ROOTFS="$BASE_DIR/rootfs-vm"
|
|
329
|
+
QEMU=${QEMU:-qemu-arm-static}
|
|
330
|
+
command -v proot >/dev/null 2>&1 || { echo "missing proot" >&2; exit 127; }
|
|
331
|
+
command -v "$QEMU" >/dev/null 2>&1 || { echo "missing $QEMU" >&2; exit 127; }
|
|
332
|
+
"$BASE_DIR/bin/prepare-runtime"
|
|
333
|
+
exec proot -0 -q "$QEMU" -r "$ROOTFS" -w / \
|
|
334
|
+
-b /dev/null:/dev/null -b /dev/zero:/dev/zero -b /dev/random:/dev/random -b /dev/urandom:/dev/urandom \
|
|
335
|
+
-b /proc:/proc -b /sys:/sys /bin/busybox sh -l "$@"
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
START_FHAPI_SCRIPT = """#!/usr/bin/env sh
|
|
339
|
+
set -eu
|
|
340
|
+
BASE_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
341
|
+
LOG_DIR="$BASE_DIR/logs"
|
|
342
|
+
mkdir -p "$LOG_DIR"
|
|
343
|
+
"$BASE_DIR/bin/prepare-runtime"
|
|
344
|
+
if pgrep -f 'qemu-arm-static .* /fhrom/bin/cfgmgr' >/dev/null 2>&1; then
|
|
345
|
+
echo "cfgmgr already appears to be running"
|
|
346
|
+
exit 0
|
|
347
|
+
fi
|
|
348
|
+
exec "$BASE_DIR/bin/proot-shell" -c '
|
|
349
|
+
set -eu
|
|
350
|
+
export PATH=/fhrom/fhshell:/usr/bin:/usr/sbin:/bin:/sbin:/fhrom/bin
|
|
351
|
+
export LD_LIBRARY_PATH=/lib:/usr/lib:/fhrom/lib:/usr/lib/glib-2.0:/opt/upt/apps/lib:/opt/upt/apps/usr/lib
|
|
352
|
+
mkdir -p /var/run /var/log /tmp
|
|
353
|
+
rm -f /var/run/ubus.sock
|
|
354
|
+
/fhrom/bin/ubusd >/tmp/ubusd.log 2>&1 &
|
|
355
|
+
sleep 1
|
|
356
|
+
/fhrom/bin/cfgmgr >/tmp/cfgmgr.log 2>&1 &
|
|
357
|
+
sleep 3
|
|
358
|
+
echo "FHAPI runtime started: ubusd + cfgmgr"
|
|
359
|
+
while :; do sleep 3600; done
|
|
360
|
+
'
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
STOP_FHAPI_SCRIPT = """#!/usr/bin/env sh
|
|
364
|
+
set -eu
|
|
365
|
+
FOUND=0
|
|
366
|
+
stop_pattern() {
|
|
367
|
+
pids=$(pgrep -f "$1" || true)
|
|
368
|
+
[ -n "$pids" ] || return 0
|
|
369
|
+
FOUND=1
|
|
370
|
+
kill $pids 2>/dev/null || true
|
|
371
|
+
}
|
|
372
|
+
stop_pattern 'qemu-arm-static .* /fhrom/bin/cfgmgr'
|
|
373
|
+
stop_pattern 'qemu-arm-static .* /fhrom/bin/ubusd'
|
|
374
|
+
stop_pattern 'qemu-arm-static .* sleep /bin/sleep 3600'
|
|
375
|
+
sleep 1
|
|
376
|
+
if [ "$FOUND" = 1 ]; then echo "FHAPI runtime stopped"; else echo "FHAPI runtime was not running"; fi
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
START_HTTP_SCRIPT = """#!/usr/bin/env sh
|
|
380
|
+
set -eu
|
|
381
|
+
BASE_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
382
|
+
if command -v ss >/dev/null 2>&1; then
|
|
383
|
+
ss -ltn '( sport = :8840 )' | grep -q ':8840' && { echo "port 8840 is already in use" >&2; exit 1; }
|
|
384
|
+
ss -ltn '( sport = :8080 )' | grep -q ':8080' && { echo "port 8080 is already in use" >&2; exit 1; }
|
|
385
|
+
fi
|
|
386
|
+
exec "$BASE_DIR/bin/proot-shell" -c '
|
|
387
|
+
set -eu
|
|
388
|
+
export PATH=/fhrom/fhshell:/usr/bin:/usr/sbin:/bin:/sbin:/fhrom/bin
|
|
389
|
+
export LD_LIBRARY_PATH=/lib:/usr/lib:/fhrom/lib:/usr/lib/glib-2.0:/opt/upt/apps/lib:/opt/upt/apps/usr/lib
|
|
390
|
+
mkdir -p /var/run /tmp /var/log
|
|
391
|
+
/fhrom/bin/thttpd -C /fhrom/fhconf/thttpd_nginx.conf
|
|
392
|
+
sleep 1
|
|
393
|
+
exec /fhrom/bin/nginx -c /fhconf/nginx.conf
|
|
394
|
+
'
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
VERIFY_SCRIPT = """#!/usr/bin/env sh
|
|
398
|
+
set -eu
|
|
399
|
+
BASE_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
400
|
+
ROOTFS="$BASE_DIR/rootfs-vm"
|
|
401
|
+
|
|
402
|
+
echo "[1/7] host tools"
|
|
403
|
+
command -v qemu-arm-static >/dev/null
|
|
404
|
+
command -v unsquashfs >/dev/null
|
|
405
|
+
command -v ubireader_extract_images >/dev/null
|
|
406
|
+
command -v proot >/dev/null
|
|
407
|
+
command -v jefferson >/dev/null
|
|
408
|
+
|
|
409
|
+
echo "[2/7] rootfs files"
|
|
410
|
+
test -x "$ROOTFS/bin/busybox"
|
|
411
|
+
test -x "$ROOTFS/sbin/uci"
|
|
412
|
+
test -f "$ROOTFS/lib/ld-linux.so.3"
|
|
413
|
+
test -x "$ROOTFS/fhrom/bin/cfg_cmd"
|
|
414
|
+
test -f "$ROOTFS/fhconf/sysinfo_conf"
|
|
415
|
+
test -f "$ROOTFS/fhconf/usrconfig_conf"
|
|
416
|
+
test -f "$ROOTFS/fhconf/attrconfig_conf"
|
|
417
|
+
test -d "$ROOTFS/opt/upt/apps"
|
|
418
|
+
test -f "$ROOTFS/fhdata/factory_conf"
|
|
419
|
+
test -f "$ROOTFS/fhdata/pre_usrconfig_conf"
|
|
420
|
+
test -f "$ROOTFS/fhdata/pre_attrconfig_conf"
|
|
421
|
+
|
|
422
|
+
echo "[3/7] ARM busybox executes"
|
|
423
|
+
"$BASE_DIR/bin/qemu-exec" /bin/busybox --help >/dev/null
|
|
424
|
+
|
|
425
|
+
echo "[4/7] proot runtime paths work"
|
|
426
|
+
"$BASE_DIR/bin/prepare-runtime"
|
|
427
|
+
"$BASE_DIR/bin/proot-shell" -c '/bin/busybox test "$(id -u)" = "0"'
|
|
428
|
+
"$BASE_DIR/bin/proot-shell" -c '/sbin/uci -c /fhconf get sysinfo_conf.area_code.value >/dev/null'
|
|
429
|
+
"$BASE_DIR/bin/proot-shell" -c '/sbin/uci -c /fhdata get factory_conf.SerialNumber.value >/dev/null'
|
|
430
|
+
|
|
431
|
+
echo "[5/7] vendor cfg_cmd status"
|
|
432
|
+
if pgrep -f 'qemu-arm-static .* /fhrom/bin/cfgmgr' >/dev/null 2>&1; then
|
|
433
|
+
"$BASE_DIR/bin/proot-shell" -c 'cfg_cmd get InternetGatewayDevice.DeviceInfo.Manufacturer' >/tmp/hg5143f-cfg_cmd.out
|
|
434
|
+
grep -Eq 'FiberHome|get success' /tmp/hg5143f-cfg_cmd.out
|
|
435
|
+
else
|
|
436
|
+
if "$BASE_DIR/bin/qemu-exec" /fhrom/bin/cfg_cmd get InternetGatewayDevice.DeviceInfo.Manufacturer >/tmp/hg5143f-cfg_cmd.out 2>/tmp/hg5143f-cfg_cmd.err; then
|
|
437
|
+
echo "cfg_cmd returned successfully without a detected cfgmgr runtime"
|
|
438
|
+
elif grep -q 'FHAPI_INIT Error' /tmp/hg5143f-cfg_cmd.err /tmp/hg5143f-cfg_cmd.out; then
|
|
439
|
+
echo "cfg_cmd binary loads; start FHAPI with ./bin/start-fhapi-proot for live cfg_cmd reads"
|
|
440
|
+
else
|
|
441
|
+
cat /tmp/hg5143f-cfg_cmd.out >&2
|
|
442
|
+
cat /tmp/hg5143f-cfg_cmd.err >&2
|
|
443
|
+
exit 1
|
|
444
|
+
fi
|
|
445
|
+
fi
|
|
446
|
+
|
|
447
|
+
echo "[6/7] nginx config parses under proot"
|
|
448
|
+
"$BASE_DIR/bin/proot-shell" -c '/fhrom/bin/nginx -t -c /fhconf/nginx.conf >/dev/null'
|
|
449
|
+
|
|
450
|
+
echo "[7/7] optional HTTP stack probe"
|
|
451
|
+
if command -v curl >/dev/null 2>&1 && curl -fsS --max-time 2 http://127.0.0.1:8080/ >/dev/null 2>&1; then
|
|
452
|
+
echo "HTTP stack is running at http://127.0.0.1:8080/"
|
|
453
|
+
else
|
|
454
|
+
echo "HTTP stack is not running; start it with ./bin/start-http-stack-proot"
|
|
455
|
+
fi
|
|
456
|
+
|
|
457
|
+
echo "verify ok"
|
|
458
|
+
"""
|