@oniroproject/oniro-app 0.14.0 → 0.16.0
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.
- package/README.md +51 -1
- package/dist/oniro-app.js +649 -29
- package/dist/oniro-app.js.map +1 -1
- package/package.json +2 -2
package/dist/oniro-app.js
CHANGED
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
// src/bin/oniro-app.ts
|
|
4
4
|
import { Command as Command4 } from "commander";
|
|
5
|
-
import { OniroError as
|
|
5
|
+
import { OniroError as OniroError3 } from "@oniroproject/core";
|
|
6
6
|
|
|
7
7
|
// src/commands/sdk.ts
|
|
8
8
|
import * as fs from "fs";
|
|
9
9
|
import * as path from "path";
|
|
10
10
|
import {
|
|
11
11
|
ALL_SDKS,
|
|
12
|
+
OHOS_DAILY_BUILDS_URL,
|
|
12
13
|
downloadAndInstallSdk,
|
|
13
14
|
getOsFolder,
|
|
14
15
|
getSdkRootDir,
|
|
15
16
|
getSupportedSdksForUi,
|
|
17
|
+
installSdkFromArchive,
|
|
16
18
|
removeSdk
|
|
17
19
|
} from "@oniroproject/core";
|
|
18
20
|
|
|
@@ -104,13 +106,42 @@ function getRuntime() {
|
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
// src/commands/sdk.ts
|
|
109
|
+
var FULL_SDK_TIP = `Tip: the full SDK (with system APIs) is not on the release mirror. Download an ohos-sdk-full archive from the OpenHarmony daily builds, ${OHOS_DAILY_BUILDS_URL}, and install it with \`oniro-app sdk install --from-archive <archive>\`.`;
|
|
107
110
|
function registerSdkCommand(program) {
|
|
108
111
|
const sdk = program.command("sdk").description("Manage OpenHarmony SDK installs.");
|
|
109
|
-
sdk.command("install
|
|
112
|
+
sdk.command("install [version]").description(
|
|
113
|
+
"Download and install an OpenHarmony SDK (skips if already installed), or install one from a local archive with --from-archive."
|
|
114
|
+
).option(
|
|
115
|
+
"--from-archive <path>",
|
|
116
|
+
"Install from a local SDK .tar.gz instead of downloading, e.g. the full SDK. The API level is read from the archive."
|
|
117
|
+
).option("--force", "Reinstall even if the SDK is already present.").option(
|
|
110
118
|
"--tmp-dir <path>",
|
|
111
119
|
"Directory for download/extract temporaries (default: ONIRO_TMP_DIR, else .oniro-tmp next to the install target). Use this when that filesystem is short on space."
|
|
112
|
-
).
|
|
120
|
+
).addHelpText("after", `
|
|
121
|
+
${FULL_SDK_TIP}`).action(async (version, opts) => {
|
|
113
122
|
const { config, logger, progress } = getRuntime();
|
|
123
|
+
if (opts.fromArchive) {
|
|
124
|
+
if (version) throw new Error("Pass either a version or --from-archive, not both.");
|
|
125
|
+
const archivePath = path.resolve(opts.fromArchive);
|
|
126
|
+
logger.info(`Installing OpenHarmony SDK from ${archivePath}...`);
|
|
127
|
+
const installed = await installSdkFromArchive({
|
|
128
|
+
config,
|
|
129
|
+
archivePath,
|
|
130
|
+
force: opts.force,
|
|
131
|
+
progress,
|
|
132
|
+
logger,
|
|
133
|
+
tmpDir: opts.tmpDir
|
|
134
|
+
});
|
|
135
|
+
const label = installed.version ? `SDK ${installed.version} (api ${installed.api})` : `SDK api ${installed.api}`;
|
|
136
|
+
logger.info(`${label} installed at ${installed.dir}.`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!version) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Name an SDK version (${ALL_SDKS.map((s) => s.version).join(", ")}) or pass --from-archive <path>.
|
|
142
|
+
` + FULL_SDK_TIP
|
|
143
|
+
);
|
|
144
|
+
}
|
|
114
145
|
const release = ALL_SDKS.find((s) => s.version === version);
|
|
115
146
|
if (!release) {
|
|
116
147
|
throw new Error(
|
|
@@ -488,7 +519,17 @@ function registerAuthCommand(program) {
|
|
|
488
519
|
|
|
489
520
|
// src/commands/app.ts
|
|
490
521
|
import * as path4 from "path";
|
|
491
|
-
import {
|
|
522
|
+
import {
|
|
523
|
+
installApp,
|
|
524
|
+
launchApp,
|
|
525
|
+
uninstallApp,
|
|
526
|
+
forceStop,
|
|
527
|
+
applyChanges,
|
|
528
|
+
existingHapOrUnsigned,
|
|
529
|
+
getBundleName,
|
|
530
|
+
readHapBundleName,
|
|
531
|
+
OniroError as OniroError2
|
|
532
|
+
} from "@oniroproject/core";
|
|
492
533
|
function registerAppCommand(program) {
|
|
493
534
|
const app = program.command("app").description("Install, launch, and manage built apps on a connected device/emulator.");
|
|
494
535
|
app.command("install [project-dir-or-hap]").description(
|
|
@@ -516,16 +557,29 @@ function registerAppCommand(program) {
|
|
|
516
557
|
}
|
|
517
558
|
logger.info("App launched.");
|
|
518
559
|
});
|
|
519
|
-
app.command("apply [project-dir]").description(
|
|
520
|
-
"Install a HAP and verify the running process took the change. Handles sign-info mismatch, asset-cache invalidation, and persistent-bundle restart."
|
|
521
|
-
).option(
|
|
560
|
+
app.command("apply [project-dir-or-hap]").description(
|
|
561
|
+
"Install a HAP (a project dir, default the current one, or a .hap file) and verify the running process took the change. Refuses a HAP of another bundle. Handles sign-info mismatch, asset-cache invalidation, and persistent-bundle restart."
|
|
562
|
+
).option(
|
|
563
|
+
"--bundle <bundle>",
|
|
564
|
+
"Bundle name to apply changes to. Defaults to the bundleName in the .hap file, else in the project AppScope/app.json5."
|
|
565
|
+
).option("--module <module>", "Module to resolve the HAP from (multi-module projects).").option("--hap <path>", "Explicit .hap path (relative to project-dir or absolute).").option("--installed-hap <path>", "Local path to the currently-installed HAP, to enable the asset-cache reboot.").option("--system", "Treat as a persistent/system bundle (refuses uninstall on sign-info mismatch).").option("--allow-uninstall", "Permit uninstalling a system bundle on sign-info mismatch (dangerous).").option("--device <serial>", "Target device serial.").option("--json", "Emit the result as JSON.").action(async (target, opts) => {
|
|
522
566
|
const { config, logger } = getRuntime();
|
|
523
|
-
const
|
|
524
|
-
|
|
567
|
+
const isHap = target !== void 0 && /\.hap$/i.test(target);
|
|
568
|
+
if (isHap && opts.hap) throw new OniroError2("Pass the HAP either as the argument or with --hap, not both.");
|
|
569
|
+
const dir = isHap ? process.cwd() : path4.resolve(target ?? process.cwd());
|
|
570
|
+
const hapPath = isHap ? path4.resolve(target) : opts.hap;
|
|
571
|
+
let bundle = opts.bundle;
|
|
572
|
+
if (!bundle && isHap) {
|
|
573
|
+
const hap = existingHapOrUnsigned(hapPath);
|
|
574
|
+
if (!hap) throw new OniroError2(`HAP not found at ${hapPath}.`);
|
|
575
|
+
bundle = await readHapBundleName(hap.path).catch(() => null) ?? void 0;
|
|
576
|
+
if (!bundle) throw new OniroError2(`Could not read the bundle name from ${hap.path}. Pass --bundle.`);
|
|
577
|
+
}
|
|
578
|
+
bundle ??= getBundleName(dir);
|
|
525
579
|
const result = await applyChanges({
|
|
526
580
|
config,
|
|
527
581
|
bundle,
|
|
528
|
-
hapPath
|
|
582
|
+
hapPath,
|
|
529
583
|
projectDir: dir,
|
|
530
584
|
module: opts.module,
|
|
531
585
|
installedHapPath: opts.installedHap,
|
|
@@ -685,16 +739,31 @@ function targetIndexFrom(opts) {
|
|
|
685
739
|
return n;
|
|
686
740
|
}
|
|
687
741
|
|
|
742
|
+
// src/lib/logSince.ts
|
|
743
|
+
function parseSince(value) {
|
|
744
|
+
if (value === void 0) return void 0;
|
|
745
|
+
if (value === "all") return Infinity;
|
|
746
|
+
const seconds = Number(value);
|
|
747
|
+
if (value.trim() === "" || !Number.isFinite(seconds) || seconds < 0) {
|
|
748
|
+
throw new Error(`--since takes a number of seconds or "all", not "${value}".`);
|
|
749
|
+
}
|
|
750
|
+
return seconds * 1e3;
|
|
751
|
+
}
|
|
752
|
+
|
|
688
753
|
// src/commands/wait.ts
|
|
689
754
|
function registerWaitCommand(program) {
|
|
690
755
|
const wait = program.command("wait").description(
|
|
691
756
|
"Wait for a device condition: a log line (--log), boot (--boot), a bundle to start (--bundle), or an on-screen element (--target / --target-id / --target-type)."
|
|
692
|
-
).option("--log <pattern>", "Resolve when a hilog line (tag: message)
|
|
757
|
+
).option("--log <pattern>", "Resolve when a hilog line (tag: message) logged after the command starts matches this regex.").option(
|
|
758
|
+
"--since <seconds>",
|
|
759
|
+
'With --log: also match lines logged up to N seconds before the command started, measured on the device clock; "all" matches everything still in the device log buffer.'
|
|
760
|
+
).option("--boot", "Resolve when the device is reachable (optionally also --pid-of).").option("--bundle <bundle>", "With --log: filter to this bundle. Alone: wait until this bundle is running.").option("--pid-of <name>", "With --boot: also require this process to have a pid.").option("--domain <domain>", "With --log: hilog domain filter (e.g. 0xD003900).");
|
|
693
761
|
addTargetOptions(wait, "Resolve when", { index: false });
|
|
694
762
|
wait.option("--gone", "With --target*: resolve when no uncovered element matches any more.").option("--interval <ms>", "With --target*: poll interval, ms (each poll is a layout dump).", "500").option("--timeout <ms>", "Max wait, ms.", "30000").option("--device <serial>", "Target device serial.").option("--json", "With --log or --target*: emit the matched entry/element as JSON.").action(async (opts) => {
|
|
695
763
|
const { config, logger } = getRuntime();
|
|
696
764
|
const timeoutMs = Number(opts.timeout);
|
|
697
765
|
const selector = selectorFrom(opts);
|
|
766
|
+
if (opts.since !== void 0 && !opts.log) throw new Error("--since applies only to --log.");
|
|
698
767
|
if (opts.log) {
|
|
699
768
|
const entry = await waitForLog({
|
|
700
769
|
config,
|
|
@@ -703,11 +772,12 @@ function registerWaitCommand(program) {
|
|
|
703
772
|
bundle: opts.bundle,
|
|
704
773
|
domain: opts.domain,
|
|
705
774
|
deviceSerial: opts.device,
|
|
775
|
+
lookbackMs: parseSince(opts.since),
|
|
706
776
|
logger
|
|
707
777
|
});
|
|
708
778
|
if (opts.json) process.stdout.write(`${JSON.stringify(entry, null, 2)}
|
|
709
779
|
`);
|
|
710
|
-
else logger.info(`matched: ${entry.tag}: ${entry.message}`);
|
|
780
|
+
else logger.info(`matched: ${entry.time} ${entry.tag}: ${entry.message}`);
|
|
711
781
|
return;
|
|
712
782
|
}
|
|
713
783
|
if (opts.boot) {
|
|
@@ -746,7 +816,10 @@ function registerWaitCommand(program) {
|
|
|
746
816
|
// src/commands/watch.ts
|
|
747
817
|
import { watchLog } from "@oniroproject/core";
|
|
748
818
|
function registerWatchCommand(program) {
|
|
749
|
-
program.command("watch").description("Collect hilog lines matching a pattern
|
|
819
|
+
program.command("watch").description("Collect the hilog lines matching a pattern that are logged during a fixed duration.").option("--log <pattern>", "Regex tested against each line (tag: message).").option("--for <ms>", "Duration to watch, ms.", "10000").option(
|
|
820
|
+
"--since <seconds>",
|
|
821
|
+
'Also collect lines logged up to N seconds before the command started, measured on the device clock; "all" adds everything still in the device log buffer.'
|
|
822
|
+
).option("--bundle <bundle>", "Filter to this bundle.").option("--domain <domain>", "hilog domain filter (e.g. 0xD003900).").option("--no-dedup", "Do not collapse consecutive duplicate lines.").option("--device <serial>", "Target device serial.").option("--json", "Emit the entries as JSON.").action(async (opts) => {
|
|
750
823
|
const { config, logger } = getRuntime();
|
|
751
824
|
if (!opts.log) throw new Error("watch requires --log <pattern>.");
|
|
752
825
|
const entries = await watchLog({
|
|
@@ -757,6 +830,7 @@ function registerWatchCommand(program) {
|
|
|
757
830
|
domain: opts.domain,
|
|
758
831
|
dedup: opts.dedup,
|
|
759
832
|
deviceSerial: opts.device,
|
|
833
|
+
lookbackMs: parseSince(opts.since),
|
|
760
834
|
logger
|
|
761
835
|
});
|
|
762
836
|
if (opts.json) {
|
|
@@ -1122,9 +1196,9 @@ import { getDeviceInfo, listDevices } from "@oniroproject/core";
|
|
|
1122
1196
|
function formatLongDevice(d, info) {
|
|
1123
1197
|
const head = `${d.serial} ${d.connection ?? ""} ${d.status}`;
|
|
1124
1198
|
if (!info) return head;
|
|
1125
|
-
const
|
|
1199
|
+
const os3 = [info.osFamily, info.osFamily === "HarmonyOS" ? info.osDistVersion : void 0].filter(Boolean).join(" ");
|
|
1126
1200
|
const fields = [
|
|
1127
|
-
["os",
|
|
1201
|
+
["os", os3 || void 0],
|
|
1128
1202
|
["api", info.sdkApiVersion],
|
|
1129
1203
|
["abi", info.abi],
|
|
1130
1204
|
["type", info.deviceType],
|
|
@@ -1194,6 +1268,7 @@ import {
|
|
|
1194
1268
|
detectCmdToolsFlavour,
|
|
1195
1269
|
detectRuntimeOs as detectRuntimeOs2,
|
|
1196
1270
|
getCmdToolsPath,
|
|
1271
|
+
missingProjectRootFiles,
|
|
1197
1272
|
runCodelinter
|
|
1198
1273
|
} from "@oniroproject/core";
|
|
1199
1274
|
function lintTotals(result) {
|
|
@@ -1208,8 +1283,10 @@ function lintTotals(result) {
|
|
|
1208
1283
|
}
|
|
1209
1284
|
function lintExitCode(result) {
|
|
1210
1285
|
if (lintTotals(result).errors > 0) return 1;
|
|
1211
|
-
|
|
1212
|
-
|
|
1286
|
+
return lintFailed(result) ? result.code : 0;
|
|
1287
|
+
}
|
|
1288
|
+
function lintFailed(result) {
|
|
1289
|
+
return result.summary === null && result.findings.length === 0 && result.code !== 0;
|
|
1213
1290
|
}
|
|
1214
1291
|
function registerLintCommand(program) {
|
|
1215
1292
|
program.command("lint [project-dir]").description(
|
|
@@ -1219,10 +1296,11 @@ function registerLintCommand(program) {
|
|
|
1219
1296
|
const dir = path8.resolve(projectDir ?? process.cwd());
|
|
1220
1297
|
const result = await runCodelinter({ config, projectDir: dir, files: opts.files, logger });
|
|
1221
1298
|
const totals = lintTotals(result);
|
|
1299
|
+
const failed = lintFailed(result);
|
|
1222
1300
|
if (opts.json) {
|
|
1223
1301
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1224
1302
|
`);
|
|
1225
|
-
} else {
|
|
1303
|
+
} else if (!failed) {
|
|
1226
1304
|
for (const f of result.findings) {
|
|
1227
1305
|
const at = `${f.file}:${f.line}${f.column ? `:${f.column}` : ""}`;
|
|
1228
1306
|
process.stdout.write(`${at} ${f.severity} ${f.message}${f.code ? ` [${f.code}]` : ""}
|
|
@@ -1233,9 +1311,13 @@ function registerLintCommand(program) {
|
|
|
1233
1311
|
`
|
|
1234
1312
|
);
|
|
1235
1313
|
}
|
|
1236
|
-
if (
|
|
1237
|
-
|
|
1238
|
-
${
|
|
1314
|
+
if (failed) {
|
|
1315
|
+
const missing = missingProjectRootFiles(dir);
|
|
1316
|
+
const why = missing.length ? ` It needs a DevEco project root, and ${dir} has no ${missing.join(", ")}.` : "";
|
|
1317
|
+
logger.error(
|
|
1318
|
+
`codelinter exited ${result.code} without a report, so nothing was linted.${why}
|
|
1319
|
+
` + result.raw.trim().split("\n").slice(-15).join("\n")
|
|
1320
|
+
);
|
|
1239
1321
|
}
|
|
1240
1322
|
if (result.incomplete) {
|
|
1241
1323
|
for (const n of result.notices) logger.warn(`codelinter: ${n}`);
|
|
@@ -1337,14 +1419,531 @@ function registerInputCommand(program) {
|
|
|
1337
1419
|
}
|
|
1338
1420
|
|
|
1339
1421
|
// src/commands/logs.ts
|
|
1340
|
-
import * as
|
|
1422
|
+
import * as os2 from "os";
|
|
1423
|
+
import * as path10 from "path";
|
|
1341
1424
|
import { captureLog } from "@oniroproject/core";
|
|
1425
|
+
|
|
1426
|
+
// src/lib/logBundle.ts
|
|
1427
|
+
import * as fs4 from "fs/promises";
|
|
1428
|
+
import * as path9 from "path";
|
|
1429
|
+
import { deflateRawSync } from "zlib";
|
|
1430
|
+
import {
|
|
1431
|
+
VERSION,
|
|
1432
|
+
hdcExec,
|
|
1433
|
+
parseDeviceList,
|
|
1434
|
+
resolveDeviceSerial,
|
|
1435
|
+
shellChecked
|
|
1436
|
+
} from "@oniroproject/core";
|
|
1437
|
+
var LEVELS = {
|
|
1438
|
+
V: "V",
|
|
1439
|
+
VERBOSE: "V",
|
|
1440
|
+
D: "D",
|
|
1441
|
+
DEBUG: "D",
|
|
1442
|
+
I: "I",
|
|
1443
|
+
INFO: "I",
|
|
1444
|
+
W: "W",
|
|
1445
|
+
WARN: "W",
|
|
1446
|
+
WARNING: "W",
|
|
1447
|
+
E: "E",
|
|
1448
|
+
ERROR: "E",
|
|
1449
|
+
F: "F",
|
|
1450
|
+
FATAL: "F"
|
|
1451
|
+
};
|
|
1452
|
+
var HILOG_RECORD = /^(?<date>\d{2}-\d{2})[ \t]+(?<time>\d{2}:\d{2}:\d{2}\.\d{3})[ \t]+(?<pid>\d+)[ \t]+\d+[ \t]+(?<level>[VDIWEF])[ \t]+(?<tag>\S+?)(?:[ \t]+[NH](?::(?:[ \t]+.*)?|[ \t]+.*)?|:(?:[ \t]+.*)?)$/;
|
|
1453
|
+
var FAILURE_TEXT = /^(?:\s*(?:\[(?:fail|error)\]|(?:error|failed|failure):|permission denied\b|no devices? connected\b|too many arguments\b|unknown option\b|invalid option\b|command not found\b)|\s*(?:hdc|device)\b.*\b(?:error|failed|no device|not connected|offline)\b)/im;
|
|
1454
|
+
function validateDate(date, time) {
|
|
1455
|
+
const [, monthText, dayText] = /^(\d{2})-(\d{2})$/.exec(date) ?? [];
|
|
1456
|
+
const [, hourText, minuteText, secondText, millisecondText] = /^(\d{2}):(\d{2}):(\d{2})\.(\d{3})$/.exec(time) ?? [];
|
|
1457
|
+
if (!monthText || !dayText || !hourText || !minuteText || !secondText || !millisecondText) return false;
|
|
1458
|
+
const value = new Date(Date.UTC(2e3, Number(monthText) - 1, Number(dayText), Number(hourText), Number(minuteText), Number(secondText), Number(millisecondText)));
|
|
1459
|
+
return value.getUTCMonth() === Number(monthText) - 1 && value.getUTCDate() === Number(dayText) && Number(hourText) < 24 && Number(minuteText) < 60 && Number(secondText) < 60;
|
|
1460
|
+
}
|
|
1461
|
+
function filterHilog(input, filters) {
|
|
1462
|
+
const criteria = {};
|
|
1463
|
+
if (filters.pid) criteria.pid = filters.pid.replace(/^0+(?=\d)/, "");
|
|
1464
|
+
if (filters.tag) criteria.tag = filters.tag;
|
|
1465
|
+
if (filters.level) criteria.level = LEVELS[filters.level.toUpperCase()] ?? filters.level.toUpperCase();
|
|
1466
|
+
if (Object.keys(criteria).length === 0) return null;
|
|
1467
|
+
let sawRecord = false;
|
|
1468
|
+
let keep = false;
|
|
1469
|
+
let output = "";
|
|
1470
|
+
for (const line of input.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
|
|
1471
|
+
const content = line.replace(/[\r\n]+$/, "");
|
|
1472
|
+
const match = HILOG_RECORD.exec(content);
|
|
1473
|
+
if (match?.groups) {
|
|
1474
|
+
if (!validateDate(match.groups.date, match.groups.time)) return null;
|
|
1475
|
+
sawRecord = true;
|
|
1476
|
+
keep = Object.entries(criteria).every(([key, value]) => {
|
|
1477
|
+
const field = key === "tag" ? match.groups.tag.slice(match.groups.tag.lastIndexOf("/") + 1) : match.groups[key];
|
|
1478
|
+
return key === "pid" ? field?.replace(/^0+(?=\d)/, "") === value : field === value;
|
|
1479
|
+
});
|
|
1480
|
+
} else if (/^\s*\[?\d{1,4}[-/]/.test(content) || !sawRecord && content.trim()) {
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
if (keep) output += line;
|
|
1484
|
+
}
|
|
1485
|
+
return sawRecord ? { text: output, applied: Object.keys(criteria) } : null;
|
|
1486
|
+
}
|
|
1487
|
+
function redactLogText(value, deviceSerials = []) {
|
|
1488
|
+
let result = value.replace(/(\bBearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[REDACTED]").replace(/(\bBasic\s+)[A-Za-z0-9+/=]+/gi, "$1[REDACTED]").replace(/((?:token|password|passwd|secret|api[_-]?key)\s*[=:]\s*)[^\s,;]+/gi, "$1[REDACTED]").replace(/(cookie\s*[=:]\s*)[^\r\n]+/gi, "$1[REDACTED]").replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[REDACTED_EMAIL]").replace(/((?:phone|mobile|telephone|tel)(?:number)?\s*[=:]\s*)\+?[\d ()-]{8,}\d/gi, "$1[REDACTED_PHONE]").replace(/(?<![\w.])(?:\+\d{1,3}[ -]?)?\(?\d{2,4}\)?[ -]\d{3,4}[ -]\d{3,4}(?![\w.])/g, "[REDACTED_PHONE]").replace(/\b(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}\b/g, "[REDACTED_MAC]").replace(/([A-Z]:\\Users\\)[^\\\r\n "']+/gi, "$1[REDACTED_USER]").replace(/(\/(?:Users|home)\/)[^/\r\n "']+/gi, "$1[REDACTED_USER]").replace(/("(?:target|deviceSerial)"\s*:\s*")[^"]+(")/gi, "$1[REDACTED_DEVICE]$2").replace(/(--device\s+|-t\s+)([A-Za-z0-9._:-]+)/gi, "$1[REDACTED_DEVICE]");
|
|
1489
|
+
for (const serial of deviceSerials) {
|
|
1490
|
+
if (serial) result = result.split(serial).join("[REDACTED_DEVICE]");
|
|
1491
|
+
}
|
|
1492
|
+
const ipv4 = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
|
|
1493
|
+
result = result.replace(ipv4, (address, offset, whole) => {
|
|
1494
|
+
const prefix = whole.slice(Math.max(0, offset - 48), offset);
|
|
1495
|
+
const suffix = whole.slice(offset + address.length);
|
|
1496
|
+
const contextual = /(?:ip|addr(?:ess)?|host|peer|remote|local|endpoint|server|client|target|device(?:id)?|serial)[^\r\n]{0,24}$/i.test(prefix);
|
|
1497
|
+
const hasPort = /^:\d{1,5}(?!\d)/.test(suffix);
|
|
1498
|
+
const octets = address.split(".").map(Number);
|
|
1499
|
+
return octets.every((part) => part <= 255) && (contextual || hasPort) ? "[REDACTED_IP]" : address;
|
|
1500
|
+
});
|
|
1501
|
+
return result;
|
|
1502
|
+
}
|
|
1503
|
+
function decodeText(data, relativePath) {
|
|
1504
|
+
let text;
|
|
1505
|
+
if (data.subarray(0, 2).equals(Buffer.from([255, 254]))) text = data.toString("utf16le").slice(1);
|
|
1506
|
+
else if (data.subarray(0, 2).equals(Buffer.from([254, 255]))) {
|
|
1507
|
+
const swapped = Buffer.from(data.subarray(2));
|
|
1508
|
+
for (let i = 0; i + 1 < swapped.length; i += 2) [swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
|
|
1509
|
+
text = swapped.toString("utf16le");
|
|
1510
|
+
} else {
|
|
1511
|
+
const utf8 = data.subarray(0, 3).equals(Buffer.from([239, 187, 191])) ? data.subarray(3) : data;
|
|
1512
|
+
if (utf8.includes(0)) throw new Error(`Binary data cannot be safely redacted: ${relativePath}`);
|
|
1513
|
+
try {
|
|
1514
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(utf8);
|
|
1515
|
+
} catch {
|
|
1516
|
+
throw new Error(`Non-UTF-8 data cannot be safely redacted: ${relativePath}`);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (text.includes("\0")) throw new Error(`Binary data cannot be safely redacted: ${relativePath}`);
|
|
1520
|
+
return text;
|
|
1521
|
+
}
|
|
1522
|
+
async function redactTree(source, destination, serials, boundedHilog) {
|
|
1523
|
+
await fs4.mkdir(destination, { recursive: false, mode: 448 });
|
|
1524
|
+
try {
|
|
1525
|
+
const visit = async (directory) => {
|
|
1526
|
+
for (const entry of await fs4.readdir(directory, { withFileTypes: true })) {
|
|
1527
|
+
const sourcePath = path9.join(directory, entry.name);
|
|
1528
|
+
const relative2 = path9.relative(source, sourcePath);
|
|
1529
|
+
const portableRelative = relative2.split(path9.sep).join("/");
|
|
1530
|
+
const destinationPath = path9.join(destination, relative2);
|
|
1531
|
+
if (entry.isDirectory()) {
|
|
1532
|
+
await fs4.mkdir(destinationPath, { recursive: true, mode: 448 });
|
|
1533
|
+
await visit(sourcePath);
|
|
1534
|
+
} else if (entry.isFile()) {
|
|
1535
|
+
if (portableRelative === "logs/hilog.txt") {
|
|
1536
|
+
const text2 = boundedHilog ?? "HiLog omitted from this archive because the requested duration could not be reliably applied.\n";
|
|
1537
|
+
await fs4.mkdir(path9.dirname(destinationPath), { recursive: true, mode: 448 });
|
|
1538
|
+
await fs4.writeFile(destinationPath, redactLogText(text2, serials), { encoding: "utf8", mode: 384 });
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
if (portableRelative === "logs/hilog-filtered.txt" && boundedHilog === null) continue;
|
|
1542
|
+
const text = decodeText(await fs4.readFile(sourcePath), relative2);
|
|
1543
|
+
await fs4.mkdir(path9.dirname(destinationPath), { recursive: true, mode: 448 });
|
|
1544
|
+
await fs4.writeFile(destinationPath, redactLogText(text, serials), { encoding: "utf8", mode: 384 });
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
await visit(source);
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
await fs4.rm(destination, { recursive: true, force: true });
|
|
1551
|
+
throw error;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
var CRC_TABLE = new Uint32Array(256);
|
|
1555
|
+
for (let i = 0; i < CRC_TABLE.length; i++) {
|
|
1556
|
+
let value = i;
|
|
1557
|
+
for (let bit = 0; bit < 8; bit++) value = value & 1 ? 3988292384 ^ value >>> 1 : value >>> 1;
|
|
1558
|
+
CRC_TABLE[i] = value >>> 0;
|
|
1559
|
+
}
|
|
1560
|
+
function crc32(data) {
|
|
1561
|
+
let crc = 4294967295;
|
|
1562
|
+
for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
|
|
1563
|
+
return (crc ^ 4294967295) >>> 0;
|
|
1564
|
+
}
|
|
1565
|
+
function dosDateTime(date) {
|
|
1566
|
+
const year = Math.max(1980, date.getFullYear());
|
|
1567
|
+
return {
|
|
1568
|
+
date: year - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate(),
|
|
1569
|
+
time: date.getHours() << 11 | date.getMinutes() << 5 | Math.floor(date.getSeconds() / 2)
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
async function writeZip(source, archivePath) {
|
|
1573
|
+
const files = [];
|
|
1574
|
+
const visit = async (directory) => {
|
|
1575
|
+
for (const entry of await fs4.readdir(directory, { withFileTypes: true })) {
|
|
1576
|
+
const filePath = path9.join(directory, entry.name);
|
|
1577
|
+
if (entry.isDirectory()) await visit(filePath);
|
|
1578
|
+
else if (entry.isFile()) files.push(filePath);
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
await visit(source);
|
|
1582
|
+
const localParts = [];
|
|
1583
|
+
const centralParts = [];
|
|
1584
|
+
let offset = 0;
|
|
1585
|
+
const stamp = dosDateTime(/* @__PURE__ */ new Date());
|
|
1586
|
+
for (const filePath of files) {
|
|
1587
|
+
const name = Buffer.from(path9.relative(source, filePath).split(path9.sep).join("/"), "utf8");
|
|
1588
|
+
const data = await fs4.readFile(filePath);
|
|
1589
|
+
const compressed = deflateRawSync(data);
|
|
1590
|
+
const checksum = crc32(data);
|
|
1591
|
+
const local = Buffer.alloc(30);
|
|
1592
|
+
local.writeUInt32LE(67324752, 0);
|
|
1593
|
+
local.writeUInt16LE(20, 4);
|
|
1594
|
+
local.writeUInt16LE(2048, 6);
|
|
1595
|
+
local.writeUInt16LE(8, 8);
|
|
1596
|
+
local.writeUInt16LE(stamp.time, 10);
|
|
1597
|
+
local.writeUInt16LE(stamp.date, 12);
|
|
1598
|
+
local.writeUInt32LE(checksum, 14);
|
|
1599
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
1600
|
+
local.writeUInt32LE(data.length, 22);
|
|
1601
|
+
local.writeUInt16LE(name.length, 26);
|
|
1602
|
+
local.writeUInt16LE(0, 28);
|
|
1603
|
+
localParts.push(local, name, compressed);
|
|
1604
|
+
const central = Buffer.alloc(46);
|
|
1605
|
+
central.writeUInt32LE(33639248, 0);
|
|
1606
|
+
central.writeUInt16LE(20, 4);
|
|
1607
|
+
central.writeUInt16LE(20, 6);
|
|
1608
|
+
central.writeUInt16LE(2048, 8);
|
|
1609
|
+
central.writeUInt16LE(8, 10);
|
|
1610
|
+
central.writeUInt16LE(stamp.time, 12);
|
|
1611
|
+
central.writeUInt16LE(stamp.date, 14);
|
|
1612
|
+
central.writeUInt32LE(checksum, 16);
|
|
1613
|
+
central.writeUInt32LE(compressed.length, 20);
|
|
1614
|
+
central.writeUInt32LE(data.length, 24);
|
|
1615
|
+
central.writeUInt16LE(name.length, 28);
|
|
1616
|
+
central.writeUInt16LE(0, 30);
|
|
1617
|
+
central.writeUInt16LE(0, 32);
|
|
1618
|
+
central.writeUInt16LE(0, 34);
|
|
1619
|
+
central.writeUInt16LE(0, 36);
|
|
1620
|
+
central.writeUInt32LE(0, 38);
|
|
1621
|
+
central.writeUInt32LE(offset, 42);
|
|
1622
|
+
centralParts.push(central, name);
|
|
1623
|
+
offset += local.length + name.length + compressed.length;
|
|
1624
|
+
}
|
|
1625
|
+
const centralDirectory = Buffer.concat(centralParts);
|
|
1626
|
+
const end = Buffer.alloc(22);
|
|
1627
|
+
end.writeUInt32LE(101010256, 0);
|
|
1628
|
+
end.writeUInt16LE(0, 4);
|
|
1629
|
+
end.writeUInt16LE(0, 6);
|
|
1630
|
+
end.writeUInt16LE(files.length, 8);
|
|
1631
|
+
end.writeUInt16LE(files.length, 10);
|
|
1632
|
+
end.writeUInt32LE(centralDirectory.length, 12);
|
|
1633
|
+
end.writeUInt32LE(offset, 16);
|
|
1634
|
+
end.writeUInt16LE(0, 20);
|
|
1635
|
+
await fs4.writeFile(archivePath, Buffer.concat([...localParts, centralDirectory, end]), { flag: "wx", mode: 384 });
|
|
1636
|
+
}
|
|
1637
|
+
function commandFailure(result) {
|
|
1638
|
+
if (result.code !== 0) return `hdc exited with code ${result.code}`;
|
|
1639
|
+
if (result.stderr.trim()) return "hdc wrote to stderr";
|
|
1640
|
+
const contentError = result.stdout.split(/\r?\n/).find((line) => FAILURE_TEXT.test(line))?.trim() ?? null;
|
|
1641
|
+
return contentError ? `command output indicates failure: ${contentError}` : null;
|
|
1642
|
+
}
|
|
1643
|
+
function errorMessage(error) {
|
|
1644
|
+
return error instanceof Error ? error.message : String(error);
|
|
1645
|
+
}
|
|
1646
|
+
async function uniqueBundleDirectory(outputDirectory, stamp) {
|
|
1647
|
+
await fs4.mkdir(outputDirectory, { recursive: true, mode: 448 });
|
|
1648
|
+
for (let suffix = 0; ; suffix++) {
|
|
1649
|
+
const name = `incident-${stamp}${suffix ? `-${suffix}` : ""}`;
|
|
1650
|
+
const directory = path9.join(outputDirectory, name);
|
|
1651
|
+
try {
|
|
1652
|
+
await fs4.mkdir(directory, { mode: 448 });
|
|
1653
|
+
return directory;
|
|
1654
|
+
} catch (error) {
|
|
1655
|
+
if (error.code !== "EEXIST") throw error;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
async function collectLogBundle(options) {
|
|
1660
|
+
if (options.packageName && !/^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/.test(options.packageName)) {
|
|
1661
|
+
throw new Error("packageName must be a reverse-DNS bundle name.");
|
|
1662
|
+
}
|
|
1663
|
+
if (options.pid && !/^[1-9]\d*$/.test(options.pid)) throw new Error("pid must be a positive integer.");
|
|
1664
|
+
if (options.tag && !/^[^\s/\x00-\x1f\x7f]+$/.test(options.tag)) throw new Error("tag must be a single HiLog tag.");
|
|
1665
|
+
if (options.level && !LEVELS[options.level.toUpperCase()]) throw new Error("level is not a recognized HiLog level.");
|
|
1666
|
+
const durationSeconds = options.durationSeconds ?? 600;
|
|
1667
|
+
if (!Number.isSafeInteger(durationSeconds) || durationSeconds < 1 || durationSeconds > 31536e3) {
|
|
1668
|
+
throw new Error("durationSeconds must be an integer between 1 and 31536000.");
|
|
1669
|
+
}
|
|
1670
|
+
const started = /* @__PURE__ */ new Date();
|
|
1671
|
+
const stamp = started.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
1672
|
+
const bundleDirectory = await uniqueBundleDirectory(path9.resolve(options.outputDirectory), stamp);
|
|
1673
|
+
const records = [];
|
|
1674
|
+
const serials = [];
|
|
1675
|
+
let replacementWarning = false;
|
|
1676
|
+
const execute = options.execute ?? ((args, deviceSerial) => args[0] === "shell" ? shellChecked({ config: options.config, command: args.slice(1).join(" "), deviceSerial }) : hdcExec({ config: options.config, args, deviceSerial }));
|
|
1677
|
+
const manifest = {
|
|
1678
|
+
schemaVersion: "1.0",
|
|
1679
|
+
collectorVersion: VERSION,
|
|
1680
|
+
collectionStartUtc: started.toISOString(),
|
|
1681
|
+
collectionEndUtc: null,
|
|
1682
|
+
host: { os: process.platform },
|
|
1683
|
+
target: null,
|
|
1684
|
+
packageName: options.packageName ?? null,
|
|
1685
|
+
pid: options.pid ?? null,
|
|
1686
|
+
filters: {
|
|
1687
|
+
tag: options.tag ?? null,
|
|
1688
|
+
level: options.level ?? null,
|
|
1689
|
+
pid: options.pid ?? null,
|
|
1690
|
+
packageName: options.packageName ?? null,
|
|
1691
|
+
durationSeconds
|
|
1692
|
+
},
|
|
1693
|
+
filterStatus: {
|
|
1694
|
+
duration: "not-applied",
|
|
1695
|
+
packageName: options.packageName ? "failed" : "not-requested",
|
|
1696
|
+
pid: options.pid ? "not-applied" : "not-requested",
|
|
1697
|
+
tag: options.tag ? "not-applied" : "not-requested",
|
|
1698
|
+
level: options.level ? "not-applied" : "not-requested"
|
|
1699
|
+
},
|
|
1700
|
+
hilogWindowApplied: false,
|
|
1701
|
+
redactionRequested: options.redact,
|
|
1702
|
+
redactionStatus: options.redact ? "pending" : "not-requested",
|
|
1703
|
+
archiveStatus: { status: "pending", path: null },
|
|
1704
|
+
commands: records,
|
|
1705
|
+
files: [],
|
|
1706
|
+
notes: [
|
|
1707
|
+
"Collection is read-only on the target device.",
|
|
1708
|
+
"hilog -x captures the current device buffer; duration is applied locally against device time when log timestamps are recognized.",
|
|
1709
|
+
"Redaction is heuristic and does not guarantee that a bundle is safe to publish."
|
|
1710
|
+
]
|
|
1711
|
+
};
|
|
1712
|
+
const run = async (name, relativeOutput, args, deviceSerial) => {
|
|
1713
|
+
const outputPath = path9.join(bundleDirectory, relativeOutput);
|
|
1714
|
+
await fs4.mkdir(path9.dirname(outputPath), { recursive: true, mode: 448 });
|
|
1715
|
+
let result = null;
|
|
1716
|
+
let failure = null;
|
|
1717
|
+
try {
|
|
1718
|
+
result = await execute(args, deviceSerial);
|
|
1719
|
+
await fs4.writeFile(outputPath, result.stdout, { encoding: "utf8", mode: 384 });
|
|
1720
|
+
if (result.stderr) await fs4.writeFile(`${outputPath}.stderr.txt`, result.stderr, { encoding: "utf8", mode: 384 });
|
|
1721
|
+
if (result.stdout.includes("\uFFFD") || result.stderr.includes("\uFFFD")) {
|
|
1722
|
+
replacementWarning = true;
|
|
1723
|
+
manifest.notes.push(`${name} output contains Unicode replacement characters; review the redacted evidence before sharing.`);
|
|
1724
|
+
}
|
|
1725
|
+
failure = commandFailure(result);
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
failure = errorMessage(error);
|
|
1728
|
+
await fs4.writeFile(outputPath, "", { encoding: "utf8", mode: 384 });
|
|
1729
|
+
}
|
|
1730
|
+
const record = {
|
|
1731
|
+
name,
|
|
1732
|
+
status: failure ? "failed" : "success",
|
|
1733
|
+
output: relativeOutput,
|
|
1734
|
+
stderr: result?.stderr ? `${relativeOutput}.stderr.txt` : null,
|
|
1735
|
+
exitCode: result?.code ?? null,
|
|
1736
|
+
error: failure
|
|
1737
|
+
};
|
|
1738
|
+
records.push(record);
|
|
1739
|
+
return record;
|
|
1740
|
+
};
|
|
1741
|
+
const targetRecord = await run("list-targets", "device/targets.txt", ["list", "targets", "-v"]);
|
|
1742
|
+
const targetOutput = await fs4.readFile(path9.join(bundleDirectory, "device/targets.txt"), "utf8").catch(() => "");
|
|
1743
|
+
const targets = parseDeviceList(targetOutput).filter((target) => target.status === "Connected" || target.status === "Unknown");
|
|
1744
|
+
serials.push(...parseDeviceList(targetOutput).map((target) => target.serial));
|
|
1745
|
+
const requestedDevice = resolveDeviceSerial(options.deviceSerial);
|
|
1746
|
+
let selectedDevice = null;
|
|
1747
|
+
let targetError = null;
|
|
1748
|
+
if (targetRecord.status !== "success") targetError = targetRecord.error ?? "hdc target discovery failed";
|
|
1749
|
+
else if (requestedDevice) {
|
|
1750
|
+
selectedDevice = targets.find((target) => target.serial === requestedDevice)?.serial ?? null;
|
|
1751
|
+
if (!selectedDevice) targetError = "selected device was not discovered as connected";
|
|
1752
|
+
} else if (targets.length === 0) targetError = "hdc did not discover a connected device";
|
|
1753
|
+
else if (targets.length > 1) targetError = "multiple devices are connected; specify --device";
|
|
1754
|
+
else selectedDevice = targets[0].serial;
|
|
1755
|
+
let boundedHilog = null;
|
|
1756
|
+
let durationApplied = false;
|
|
1757
|
+
let durationFailureReason = "device clock or HiLog collection failed";
|
|
1758
|
+
if (targetError) {
|
|
1759
|
+
targetRecord.status = "failed";
|
|
1760
|
+
targetRecord.error = targetError;
|
|
1761
|
+
manifest.notes.push("Device collection stopped because target discovery did not identify exactly one selected connected device.");
|
|
1762
|
+
} else {
|
|
1763
|
+
manifest.target = selectedDevice;
|
|
1764
|
+
const shell = (args) => run(args[0], args[1], args.slice(2), selectedDevice);
|
|
1765
|
+
const captureStartedHostMs = Date.now();
|
|
1766
|
+
const deviceTimeBefore = await shell([
|
|
1767
|
+
"device-time-before",
|
|
1768
|
+
"device/device-time-before.txt",
|
|
1769
|
+
"shell",
|
|
1770
|
+
"date",
|
|
1771
|
+
"+%Y-%m-%dT%H:%M:%S"
|
|
1772
|
+
]);
|
|
1773
|
+
const hilog = await shell(["hilog-snapshot", "logs/hilog.txt", "shell", "hilog", "-x"]);
|
|
1774
|
+
const deviceTimeAfter = await shell([
|
|
1775
|
+
"device-time-after",
|
|
1776
|
+
"device/device-time-after.txt",
|
|
1777
|
+
"shell",
|
|
1778
|
+
"date",
|
|
1779
|
+
"+%Y-%m-%dT%H:%M:%S"
|
|
1780
|
+
]);
|
|
1781
|
+
const hostElapsedMs = Date.now() - captureStartedHostMs;
|
|
1782
|
+
if (deviceTimeBefore.status === "success" && hilog.status === "success" && deviceTimeAfter.status === "success") {
|
|
1783
|
+
const [beforeText, afterText, fullHilog] = await Promise.all([
|
|
1784
|
+
fs4.readFile(path9.join(bundleDirectory, "device/device-time-before.txt"), "utf8"),
|
|
1785
|
+
fs4.readFile(path9.join(bundleDirectory, "device/device-time-after.txt"), "utf8"),
|
|
1786
|
+
fs4.readFile(path9.join(bundleDirectory, "logs/hilog.txt"), "utf8")
|
|
1787
|
+
]);
|
|
1788
|
+
const window = filterHilogWindow(fullHilog, beforeText, afterText, durationSeconds, hostElapsedMs);
|
|
1789
|
+
if (window.text === null) durationFailureReason = window.reason;
|
|
1790
|
+
else boundedHilog = window.text;
|
|
1791
|
+
} else if (deviceTimeBefore.status !== "success" || deviceTimeAfter.status !== "success") {
|
|
1792
|
+
durationFailureReason = "device clock could not be read";
|
|
1793
|
+
} else if (hilog.status !== "success") {
|
|
1794
|
+
durationFailureReason = "HiLog snapshot collection failed";
|
|
1795
|
+
}
|
|
1796
|
+
durationApplied = boundedHilog !== null;
|
|
1797
|
+
manifest.hilogWindowApplied = durationApplied;
|
|
1798
|
+
const faultArgs = options.packageName ? ["faultlog", "logs/faultlog.txt", "shell", "hidumper", "-e", "--print", options.packageName, "-n", "5"] : ["faultlog", "logs/faultlog.txt", "shell", "hidumper", "-e", "--list", "-n", "20"];
|
|
1799
|
+
const faultlog = await shell(faultArgs);
|
|
1800
|
+
manifest.filterStatus = {
|
|
1801
|
+
duration: durationApplied ? "applied" : "not-applied",
|
|
1802
|
+
packageName: options.packageName ? faultlog.status === "success" ? "applied" : "failed" : "not-requested",
|
|
1803
|
+
pid: options.pid ? "not-applied" : "not-requested",
|
|
1804
|
+
tag: options.tag ? "not-applied" : "not-requested",
|
|
1805
|
+
level: options.level ? "not-applied" : "not-requested"
|
|
1806
|
+
};
|
|
1807
|
+
if (!durationApplied) {
|
|
1808
|
+
manifest.notes.push(
|
|
1809
|
+
`HiLog duration was not applied (${durationFailureReason}); the full snapshot is kept only in the local raw bundle and omitted from the redacted archive.`
|
|
1810
|
+
);
|
|
1811
|
+
} else if (boundedHilog !== null && (options.pid || options.tag || options.level)) {
|
|
1812
|
+
const filtered = filterHilog(boundedHilog, { pid: options.pid, tag: options.tag, level: options.level });
|
|
1813
|
+
if (filtered) {
|
|
1814
|
+
await fs4.writeFile(path9.join(bundleDirectory, "logs/hilog-filtered.txt"), filtered.text, { encoding: "utf8", mode: 384 });
|
|
1815
|
+
const status = manifest.filterStatus;
|
|
1816
|
+
for (const key of filtered.applied) status[key] = "applied";
|
|
1817
|
+
} else {
|
|
1818
|
+
manifest.notes.push("Requested HiLog PID/tag/level filters were not applied because the windowed snapshot format could not be parsed reliably.");
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
await shell(["device-name", "device/device-info.txt", "shell", "param", "get", "const.product.name"]);
|
|
1822
|
+
await shell(["process-list", "device/process-list.txt", "shell", "ps", "-ef"]);
|
|
1823
|
+
await shell(["memory", "device/memory.txt", "shell", "hidumper", "--mem"]);
|
|
1824
|
+
await shell(["threads", "device/threads.txt", "shell", "hidumper", "-p"]);
|
|
1825
|
+
}
|
|
1826
|
+
const ended = /* @__PURE__ */ new Date();
|
|
1827
|
+
manifest.collectionEndUtc = ended.toISOString();
|
|
1828
|
+
const files = records.flatMap((record) => [record.output, record.stderr].filter((file) => Boolean(file)));
|
|
1829
|
+
files.push("manifest.json", "notes.txt");
|
|
1830
|
+
if (await fs4.access(path9.join(bundleDirectory, "logs/hilog-filtered.txt")).then(() => true, () => false)) {
|
|
1831
|
+
files.push("logs/hilog-filtered.txt");
|
|
1832
|
+
}
|
|
1833
|
+
manifest.files = files;
|
|
1834
|
+
await fs4.writeFile(path9.join(bundleDirectory, "notes.txt"), "Add reproduction steps and precise event timestamps here.\n", { encoding: "utf8", mode: 384 });
|
|
1835
|
+
const durationFailure = !durationApplied;
|
|
1836
|
+
const criticalFailure = Boolean(targetError) || records.some((record) => record.name === "hilog-snapshot" && record.status !== "success");
|
|
1837
|
+
const anyCommandFailure = records.some((record) => record.status !== "success");
|
|
1838
|
+
let redactedDirectory = null;
|
|
1839
|
+
let archivePath = null;
|
|
1840
|
+
let bundleError;
|
|
1841
|
+
try {
|
|
1842
|
+
if (options.redact) {
|
|
1843
|
+
redactedDirectory = `${bundleDirectory}-redacted`;
|
|
1844
|
+
manifest.redactionStatus = replacementWarning ? "review-required" : "success";
|
|
1845
|
+
manifest.archiveStatus = { status: "success", path: `${path9.basename(redactedDirectory)}.zip` };
|
|
1846
|
+
await fs4.writeFile(path9.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
1847
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1848
|
+
await redactTree(bundleDirectory, redactedDirectory, serials, boundedHilog);
|
|
1849
|
+
archivePath = `${redactedDirectory}.zip`;
|
|
1850
|
+
await writeZip(redactedDirectory, archivePath);
|
|
1851
|
+
} else {
|
|
1852
|
+
archivePath = `${bundleDirectory}.zip`;
|
|
1853
|
+
manifest.archiveStatus = { status: "success", path: path9.basename(archivePath) };
|
|
1854
|
+
await fs4.writeFile(path9.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
1855
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1856
|
+
await writeZip(bundleDirectory, archivePath);
|
|
1857
|
+
}
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
bundleError = options.redact ? `redaction or archive creation failed: ${errorMessage(error)}` : `archive creation failed: ${errorMessage(error)}`;
|
|
1860
|
+
archivePath = null;
|
|
1861
|
+
manifest.redactionStatus = options.redact ? "failed" : "not-requested";
|
|
1862
|
+
manifest.archiveStatus = { status: "failed", path: null };
|
|
1863
|
+
manifest.notes.push(bundleError);
|
|
1864
|
+
await fs4.rm(`${redactedDirectory ?? bundleDirectory}.zip`, { force: true }).catch(() => void 0);
|
|
1865
|
+
if (redactedDirectory) await fs4.rm(redactedDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
1866
|
+
await fs4.writeFile(path9.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
1867
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1868
|
+
redactedDirectory = null;
|
|
1869
|
+
}
|
|
1870
|
+
return {
|
|
1871
|
+
bundleDirectory,
|
|
1872
|
+
archivePath,
|
|
1873
|
+
redactedDirectory,
|
|
1874
|
+
complete: !criticalFailure && !anyCommandFailure && !bundleError && !replacementWarning && !durationFailure,
|
|
1875
|
+
error: bundleError ?? (criticalFailure ? targetError ?? "HiLog collection failed" : replacementWarning ? "Captured output contains Unicode replacement characters; review the manifest before sharing." : durationFailure ? `HiLog duration was not applied (${durationFailureReason}); the redacted archive omits the full snapshot.` : void 0)
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
function parseDeviceClock(value) {
|
|
1879
|
+
const match = /^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\s*$/.exec(value);
|
|
1880
|
+
if (!match) return null;
|
|
1881
|
+
const [, year, month, day, hour, minute, second] = match;
|
|
1882
|
+
const timestamp = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
|
|
1883
|
+
const date = new Date(timestamp);
|
|
1884
|
+
return date.getUTCFullYear() === Number(year) && date.getUTCMonth() === Number(month) - 1 && date.getUTCDate() === Number(day) && Number(hour) < 24 && Number(minute) < 60 && Number(second) < 60 ? timestamp : null;
|
|
1885
|
+
}
|
|
1886
|
+
function parseHilogTimestamp(datePart, timePart, referenceMs) {
|
|
1887
|
+
if (!validateDate(datePart, timePart)) return null;
|
|
1888
|
+
const [month, day] = datePart.split("-").map(Number);
|
|
1889
|
+
const [hour, minute, second, millisecond] = timePart.split(/[:.]/).map(Number);
|
|
1890
|
+
const referenceYear = new Date(referenceMs).getUTCFullYear();
|
|
1891
|
+
const candidates = [referenceYear - 1, referenceYear, referenceYear + 1].map((year) => Date.UTC(year, month - 1, day, hour, minute, second, millisecond)).filter((timestamp) => {
|
|
1892
|
+
const candidate = new Date(timestamp);
|
|
1893
|
+
return candidate.getUTCMonth() === month - 1 && candidate.getUTCDate() === day;
|
|
1894
|
+
}).sort((left, right) => Math.abs(left - referenceMs) - Math.abs(right - referenceMs));
|
|
1895
|
+
const closest = candidates[0];
|
|
1896
|
+
return closest !== void 0 && closest <= referenceMs + 2e3 ? closest : null;
|
|
1897
|
+
}
|
|
1898
|
+
function filterHilogWindow(input, deviceClockBefore, deviceClockAfter, durationSeconds, hostElapsedMs) {
|
|
1899
|
+
if (!Number.isSafeInteger(durationSeconds) || durationSeconds < 1 || durationSeconds > 31536e3) {
|
|
1900
|
+
return { text: null, reason: "requested duration is invalid" };
|
|
1901
|
+
}
|
|
1902
|
+
const beforeMs = parseDeviceClock(deviceClockBefore);
|
|
1903
|
+
const afterMs = parseDeviceClock(deviceClockAfter);
|
|
1904
|
+
if (beforeMs === null || afterMs === null) return { text: null, reason: "device clock output was not recognized" };
|
|
1905
|
+
const deviceElapsedMs = afterMs - beforeMs;
|
|
1906
|
+
if (deviceElapsedMs < -1e3 || deviceElapsedMs > Math.max(0, hostElapsedMs) + 5e3) {
|
|
1907
|
+
return { text: null, reason: "device clock changed inconsistently during capture" };
|
|
1908
|
+
}
|
|
1909
|
+
const windowEnd = afterMs + 999;
|
|
1910
|
+
const windowStart = windowEnd - durationSeconds * 1e3;
|
|
1911
|
+
let sawRecord = false;
|
|
1912
|
+
let keep = false;
|
|
1913
|
+
let output = "";
|
|
1914
|
+
for (const line of input.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
|
|
1915
|
+
const content = line.replace(/[\r\n]+$/, "");
|
|
1916
|
+
const match = HILOG_RECORD.exec(content);
|
|
1917
|
+
if (match?.groups) {
|
|
1918
|
+
if (!validateDate(match.groups.date, match.groups.time)) {
|
|
1919
|
+
return { text: null, reason: "HiLog contains an invalid timestamp" };
|
|
1920
|
+
}
|
|
1921
|
+
const timestamp = parseHilogTimestamp(match.groups.date, match.groups.time, afterMs);
|
|
1922
|
+
if (timestamp === null || timestamp > windowEnd) {
|
|
1923
|
+
return { text: null, reason: "HiLog timestamps do not agree with the device clock" };
|
|
1924
|
+
}
|
|
1925
|
+
sawRecord = true;
|
|
1926
|
+
keep = timestamp >= windowStart;
|
|
1927
|
+
} else if (/^-{3,}\s*beginning of\b/i.test(content.trim())) {
|
|
1928
|
+
continue;
|
|
1929
|
+
} else if (/^\s*\[?\d{1,4}[-/]/.test(content) || /^\s*\d{2}:\d{2}:\d{2}/.test(content)) {
|
|
1930
|
+
return { text: null, reason: "HiLog contains timestamped lines in an unrecognized format" };
|
|
1931
|
+
} else if (!sawRecord && content.trim()) {
|
|
1932
|
+
return { text: null, reason: "HiLog contains unclassified lines before the first record" };
|
|
1933
|
+
}
|
|
1934
|
+
if (keep) output += line;
|
|
1935
|
+
}
|
|
1936
|
+
return sawRecord ? { text: output } : { text: null, reason: "no recognized timestamped HiLog records were found" };
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// src/commands/logs.ts
|
|
1342
1940
|
function registerLogsCommand(program) {
|
|
1343
|
-
program.command("logs").description(
|
|
1941
|
+
const logs = program.command("logs").description(
|
|
1344
1942
|
"Capture hilog to a file for a whole session: the device's small log buffer wraps within seconds under chatty components, so a dump at the end loses lines. Runs until --for elapses or the process is interrupted; start it in the background, e.g. `oniro-app logs --clear -o hilog_full.txt &`. Every line is flushed as it arrives and the stream reconnects after a reboot."
|
|
1345
|
-
).
|
|
1943
|
+
).option("-o, --output <file>", "File to append the lines to (required).").option("--for <ms>", "Stop after this many ms (default: run until interrupted).").option("--grep <regex>", "Keep only lines matching this regex (default: all).").option("--clear", "Clear the device log buffer first (hilog -r), so the file covers only this session.").option("--domain <domain>", "hilog domain filter (e.g. 0xD003900).").option("--device <serial>", "Target device serial.").action(async (opts) => {
|
|
1944
|
+
if (!opts.output) logs.error("error: required option '-o, --output <file>' not specified");
|
|
1346
1945
|
const { config, logger } = getRuntime();
|
|
1347
|
-
const file =
|
|
1946
|
+
const file = path10.resolve(opts.output);
|
|
1348
1947
|
const abort = new AbortController();
|
|
1349
1948
|
const stop = () => abort.abort();
|
|
1350
1949
|
process.once("SIGINT", stop);
|
|
@@ -1365,6 +1964,27 @@ function registerLogsCommand(program) {
|
|
|
1365
1964
|
process.stdout.write(`${result.file}
|
|
1366
1965
|
`);
|
|
1367
1966
|
});
|
|
1967
|
+
logs.command("collect").description("Create a diagnostic bundle from a connected device.").allowExcessArguments(false).option("-o, --output <directory>", "Parent directory for incident bundles.", path10.join(os2.homedir(), ".oniro", "incident-bundles")).option("--device <serial>", "Target device serial.").option("--package <bundle>", "Collect faultlog entries for this application bundle.").option("--pid <pid>", "Create a filtered HiLog view for this process.").option("--tag <tag>", "Create a filtered HiLog view for this tag.").option("--level <level>", "Create a filtered HiLog view for this level (V/D/I/W/E/F).").option("--duration <seconds>", "Keep HiLog records from the last N seconds (default 600).", "600").option("--redact", "Redact sensitive values and archive only the redacted bundle.").option("--json", "Emit bundle paths and status as JSON.").action(async (opts) => {
|
|
1968
|
+
const { config } = getRuntime();
|
|
1969
|
+
const result = await collectLogBundle({
|
|
1970
|
+
config,
|
|
1971
|
+
outputDirectory: opts.output,
|
|
1972
|
+
deviceSerial: opts.device,
|
|
1973
|
+
packageName: opts.package,
|
|
1974
|
+
pid: opts.pid,
|
|
1975
|
+
tag: opts.tag,
|
|
1976
|
+
level: opts.level,
|
|
1977
|
+
durationSeconds: Number(opts.duration),
|
|
1978
|
+
redact: Boolean(opts.redact)
|
|
1979
|
+
});
|
|
1980
|
+
const output = opts.json ? `${JSON.stringify({ ...result, status: result.complete ? "complete" : "partial" }, null, 2)}
|
|
1981
|
+
` : `${result.archivePath ?? result.bundleDirectory}
|
|
1982
|
+
`;
|
|
1983
|
+
process.stdout.write(output);
|
|
1984
|
+
if (!result.complete) {
|
|
1985
|
+
throw new Error(`Bundle is partial; inspect manifest.json. ${result.error ?? "One or more collectors failed."}`);
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1368
1988
|
}
|
|
1369
1989
|
|
|
1370
1990
|
// src/commands/hdc.ts
|
|
@@ -1383,19 +2003,19 @@ function registerHdcCommand(program) {
|
|
|
1383
2003
|
`);
|
|
1384
2004
|
return;
|
|
1385
2005
|
}
|
|
1386
|
-
await new Promise((
|
|
2006
|
+
await new Promise((resolve10, reject) => {
|
|
1387
2007
|
const child = spawnCompat(hdc, hdcPassthroughArgv(args, opts.device), { stdio: "inherit" });
|
|
1388
2008
|
child.on("error", (err) => reject(new Error(`Failed to run ${hdc}: ${err.message}`)));
|
|
1389
2009
|
child.on("close", (code) => {
|
|
1390
2010
|
process.exitCode = code ?? 1;
|
|
1391
|
-
|
|
2011
|
+
resolve10();
|
|
1392
2012
|
});
|
|
1393
2013
|
});
|
|
1394
2014
|
});
|
|
1395
2015
|
}
|
|
1396
2016
|
|
|
1397
2017
|
// src/bin/oniro-app.ts
|
|
1398
|
-
var CLI_VERSION = "0.
|
|
2018
|
+
var CLI_VERSION = "0.16.0";
|
|
1399
2019
|
function buildProgram() {
|
|
1400
2020
|
const program = new Command4().name("oniro-app").description(
|
|
1401
2021
|
"Cross-platform CLI for Oniro/OpenHarmony app development.\n\nDesigned for non-interactive use (CI, scripts, agents): every command\ntakes explicit flags, results go to stdout, progress/logs go to stderr,\nand a non-zero exit code indicates failure."
|
|
@@ -1427,7 +2047,7 @@ async function main() {
|
|
|
1427
2047
|
try {
|
|
1428
2048
|
await program.parseAsync(process.argv);
|
|
1429
2049
|
} catch (err) {
|
|
1430
|
-
const message = err instanceof
|
|
2050
|
+
const message = err instanceof OniroError3 ? err.message : err instanceof Error ? err.message : String(err);
|
|
1431
2051
|
process.stderr.write(`[error] ${message}
|
|
1432
2052
|
`);
|
|
1433
2053
|
if (process.env.ONIRO_DEBUG === "1" && err instanceof Error && err.stack) {
|