@oniroproject/oniro-app 0.13.0 → 0.15.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 +89 -11
- package/dist/oniro-app.js +746 -26
- package/dist/oniro-app.js.map +1 -1
- package/package.json +2 -2
package/dist/oniro-app.js
CHANGED
|
@@ -175,7 +175,9 @@ function registerCmdToolsCommand(program) {
|
|
|
175
175
|
await installCmdTools({ config, progress, logger, localZipPath: opts.fromZip, tmpDir: opts.tmpDir });
|
|
176
176
|
logger.info("Command-line tools installed.");
|
|
177
177
|
});
|
|
178
|
-
cmdtools.command("status").description(
|
|
178
|
+
cmdtools.command("status").description(
|
|
179
|
+
"Report whether the command-line tools are installed, their version and flavour (HarmonyOS or OpenHarmony), and the hdc binary every device command uses."
|
|
180
|
+
).option("--json", "Emit machine-readable JSON.").action((opts) => {
|
|
179
181
|
const { config } = getRuntime();
|
|
180
182
|
const status = getCmdToolsStatus(config);
|
|
181
183
|
if (opts.json) {
|
|
@@ -184,6 +186,12 @@ function registerCmdToolsCommand(program) {
|
|
|
184
186
|
return;
|
|
185
187
|
}
|
|
186
188
|
process.stdout.write(`${status.status}
|
|
189
|
+
`);
|
|
190
|
+
process.stdout.write(`path: ${status.path}
|
|
191
|
+
`);
|
|
192
|
+
if (status.flavour) process.stdout.write(`flavour: ${status.flavour}
|
|
193
|
+
`);
|
|
194
|
+
process.stdout.write(`hdc: ${status.hdc.path}${status.hdc.exists ? "" : " (missing)"}
|
|
187
195
|
`);
|
|
188
196
|
if (!status.installed) process.exit(1);
|
|
189
197
|
});
|
|
@@ -197,6 +205,7 @@ function registerCmdToolsCommand(program) {
|
|
|
197
205
|
// src/commands/emulator.ts
|
|
198
206
|
import {
|
|
199
207
|
attemptHdcConnection,
|
|
208
|
+
getEmulatorStatus,
|
|
200
209
|
installEmulator,
|
|
201
210
|
isEmulatorInstalled,
|
|
202
211
|
removeEmulator,
|
|
@@ -230,6 +239,24 @@ function registerEmulatorCommand(program) {
|
|
|
230
239
|
});
|
|
231
240
|
logger.info("Emulator start command complete.");
|
|
232
241
|
});
|
|
242
|
+
emu.command("status").description(
|
|
243
|
+
"Report whether the Oniro emulator is installed, running, and connected to hdc, without starting it. Exits 1 when it is not installed."
|
|
244
|
+
).option("--address <addr>", "hdc address the emulator listens on", "127.0.0.1:55555").option("--json", "Emit machine-readable JSON.").action(async (opts) => {
|
|
245
|
+
const { config, logger } = getRuntime();
|
|
246
|
+
const status = await getEmulatorStatus(config, { address: opts.address, logger });
|
|
247
|
+
if (opts.json) {
|
|
248
|
+
process.stdout.write(`${JSON.stringify(status, null, 2)}
|
|
249
|
+
`);
|
|
250
|
+
} else {
|
|
251
|
+
process.stdout.write(
|
|
252
|
+
`installed: ${status.installed ? "yes" : "no"} (${status.dir})
|
|
253
|
+
running: ${status.running ? "yes" : "no"}
|
|
254
|
+
connected: ${status.connected ? "yes" : "no"} (${status.address})
|
|
255
|
+
`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
if (!status.installed) process.exitCode = 1;
|
|
259
|
+
});
|
|
233
260
|
emu.command("stop").description("Kill running emulator processes.").action(async () => {
|
|
234
261
|
const { logger } = getRuntime();
|
|
235
262
|
await stopEmulator(logger);
|
|
@@ -253,15 +280,15 @@ function registerEmulatorCommand(program) {
|
|
|
253
280
|
|
|
254
281
|
// src/commands/build.ts
|
|
255
282
|
import * as path2 from "path";
|
|
256
|
-
import { buildHap } from "@oniroproject/core";
|
|
283
|
+
import { SdkNotInstalledError, buildHap, downloadAndInstallSdk as downloadAndInstallSdk2 } from "@oniroproject/core";
|
|
257
284
|
function registerBuildCommand(program) {
|
|
258
285
|
program.command("build [project-dir]").description(
|
|
259
|
-
'Build an OpenHarmony or HarmonyOS app via hvigorw. Defaults to the current directory. Projects declaring runtimeOS "HarmonyOS" build against the HarmonyOS SDK (ONIRO_HARMONYOS_SDK_PATH or an installed DevEco Studio).'
|
|
260
|
-
).option("--product <product>", "hvigor product name", "default").option("--module <module>", "restrict the build to a specific module").option("--mode <mode>", "hvigor build mode (e.g. release, debug)").option("--task <task>", "hvigor task to run", "assembleHap").option("--no-deps", "skip the automatic `ohpm install --all` when oh_modules/ is missing").option("--no-parallel", "build modules serially (default: parallel)").option("--json", "emit the build result (discovered HAPs, warnings) as JSON").action(async (projectDir, opts) => {
|
|
261
|
-
const { config, logger } = getRuntime();
|
|
286
|
+
'Build an OpenHarmony or HarmonyOS app via hvigorw. Defaults to the current directory. Projects declaring runtimeOS "HarmonyOS" build against the HarmonyOS SDK (ONIRO_HARMONYOS_SDK_PATH or an installed DevEco Studio). An OpenHarmony project fails early, naming the release to install, when the SDK for its compileSdkVersion is missing.'
|
|
287
|
+
).option("--product <product>", "hvigor product name", "default").option("--module <module>", "restrict the build to a specific module").option("--mode <mode>", "hvigor build mode (e.g. release, debug)").option("--task <task>", "hvigor task to run", "assembleHap").option("--no-deps", "skip the automatic `ohpm install --all` when oh_modules/ is missing").option("--no-parallel", "build modules serially (default: parallel)").option("--install-sdk", "install the OpenHarmony SDK the project compiles against when it is missing, then build").option("--json", "emit the build result (discovered HAPs, warnings) as JSON").action(async (projectDir, opts) => {
|
|
288
|
+
const { config, logger, progress } = getRuntime();
|
|
262
289
|
const dir = path2.resolve(projectDir ?? process.cwd());
|
|
263
290
|
logger.info(`Building project at ${dir}...`);
|
|
264
|
-
const
|
|
291
|
+
const build = () => buildHap({
|
|
265
292
|
config,
|
|
266
293
|
projectDir: dir,
|
|
267
294
|
product: opts.product,
|
|
@@ -275,6 +302,15 @@ function registerBuildCommand(program) {
|
|
|
275
302
|
onOutput: () => {
|
|
276
303
|
}
|
|
277
304
|
});
|
|
305
|
+
let result;
|
|
306
|
+
try {
|
|
307
|
+
result = await build();
|
|
308
|
+
} catch (err) {
|
|
309
|
+
if (!(err instanceof SdkNotInstalledError && err.version && opts.installSdk)) throw err;
|
|
310
|
+
logger.info(`Installing OpenHarmony SDK ${err.version} (api ${err.api}), which the project compiles against...`);
|
|
311
|
+
await downloadAndInstallSdk2({ config, version: err.version, api: err.api, progress, logger });
|
|
312
|
+
result = await build();
|
|
313
|
+
}
|
|
278
314
|
for (const w of result.warnings) logger.warn(w);
|
|
279
315
|
if (opts.json) {
|
|
280
316
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
@@ -326,6 +362,11 @@ function registerSignCommand(program) {
|
|
|
326
362
|
"--acls <list>",
|
|
327
363
|
"Comma-separated permission names to write into the profile's acls.allowed-acls. Required for apps that request permissions above their apl (e.g. a system_basic app requesting ohos.permission.CAPTURE_SCREEN). Example: --acls ohos.permission.REBOOT,ohos.permission.INJECT_INPUT_EVENT"
|
|
328
364
|
).argParser(parseAclsList)
|
|
365
|
+
).addOption(
|
|
366
|
+
new Option(
|
|
367
|
+
"--app-privilege-capabilities <list>",
|
|
368
|
+
`Comma-separated privileges to write into the profile's app-privilege-capabilities. An app that declares a privileged extension ability (e.g. an AccessibilityExtensionAbility) needs AllowAppUsePrivilegeExtension, else install fails with 9568344 "not allow use privilege extension". Others include AllowAppMultiProcess and AllowAppDesktopIconHide.`
|
|
369
|
+
).argParser(parseAclsList)
|
|
329
370
|
).option(
|
|
330
371
|
"--harmonyos",
|
|
331
372
|
"Sign a HarmonyOS project through AppGallery Connect. Needs `oniro-app auth login`, and a connected device unless the team already has devices registered."
|
|
@@ -359,6 +400,7 @@ function registerSignCommand(program) {
|
|
|
359
400
|
apl: opts.apl,
|
|
360
401
|
appFeature: opts.appFeature,
|
|
361
402
|
acls: opts.acls,
|
|
403
|
+
appPrivilegeCapabilities: opts.appPrivilegeCapabilities,
|
|
362
404
|
passwords,
|
|
363
405
|
logger
|
|
364
406
|
});
|
|
@@ -373,6 +415,7 @@ function registerSignCommand(program) {
|
|
|
373
415
|
apl: opts.apl,
|
|
374
416
|
appFeature: opts.appFeature,
|
|
375
417
|
acls: opts.acls,
|
|
418
|
+
appPrivilegeCapabilities: opts.appPrivilegeCapabilities,
|
|
376
419
|
passwords,
|
|
377
420
|
logger
|
|
378
421
|
});
|
|
@@ -448,12 +491,16 @@ import * as path4 from "path";
|
|
|
448
491
|
import { installApp, launchApp, uninstallApp, forceStop, applyChanges, getBundleName } from "@oniroproject/core";
|
|
449
492
|
function registerAppCommand(program) {
|
|
450
493
|
const app = program.command("app").description("Install, launch, and manage built apps on a connected device/emulator.");
|
|
451
|
-
app.command("install [project-dir]").description(
|
|
494
|
+
app.command("install [project-dir-or-hap]").description(
|
|
495
|
+
"Install a built .hap on the connected device/emulator via hdc. Takes a project dir (default: the current one), whose newest signed HAP is installed, else its unsigned one, or a .hap file. Fails when the bundle manager rejects the HAP, and says how to sign when a target refuses an unsigned one."
|
|
496
|
+
).option("--hap <path>", "Override the .hap path (relative to project-dir or absolute).").option("--device <serial>", "Target device serial.").action(async (target, opts) => {
|
|
452
497
|
const { config, logger } = getRuntime();
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
logger.info(
|
|
498
|
+
const isHap = target !== void 0 && /\.hap$/i.test(target);
|
|
499
|
+
const dir = isHap ? process.cwd() : path4.resolve(target ?? process.cwd());
|
|
500
|
+
const hapPath = isHap ? path4.resolve(target) : opts.hap;
|
|
501
|
+
logger.info(`Installing ${hapPath ?? `app from ${dir}`}...`);
|
|
502
|
+
const result = await installApp({ config, projectDir: dir, hapPath, deviceSerial: opts.device, logger });
|
|
503
|
+
logger.info(`Installed ${result.hapPath}.`);
|
|
457
504
|
});
|
|
458
505
|
app.command("launch [project-dir]").description(
|
|
459
506
|
"Launch an app on the connected device/emulator. Project mode (default): resolves bundle + main ability from the project. Bundle mode (--bundle): launch an already-installed app by bundle name, no project needed."
|
|
@@ -1071,13 +1118,42 @@ function registerDumpCommand(program) {
|
|
|
1071
1118
|
}
|
|
1072
1119
|
|
|
1073
1120
|
// src/commands/devices.ts
|
|
1074
|
-
import { listDevices } from "@oniroproject/core";
|
|
1121
|
+
import { getDeviceInfo, listDevices } from "@oniroproject/core";
|
|
1122
|
+
function formatLongDevice(d, info) {
|
|
1123
|
+
const head = `${d.serial} ${d.connection ?? ""} ${d.status}`;
|
|
1124
|
+
if (!info) return head;
|
|
1125
|
+
const os3 = [info.osFamily, info.osFamily === "HarmonyOS" ? info.osDistVersion : void 0].filter(Boolean).join(" ");
|
|
1126
|
+
const fields = [
|
|
1127
|
+
["os", os3 || void 0],
|
|
1128
|
+
["api", info.sdkApiVersion],
|
|
1129
|
+
["abi", info.abi],
|
|
1130
|
+
["type", info.deviceType],
|
|
1131
|
+
["model", info.model],
|
|
1132
|
+
["fullname", info.osFullName]
|
|
1133
|
+
];
|
|
1134
|
+
const rest = fields.filter(([, v]) => v).map(([k, v]) => `${k}=${/\s/.test(v) ? JSON.stringify(v) : v}`);
|
|
1135
|
+
return [head, ...rest].join(" ");
|
|
1136
|
+
}
|
|
1075
1137
|
function registerDevicesCommand(program) {
|
|
1076
|
-
program.command("devices").description("List connected hdc devices.").option(
|
|
1077
|
-
|
|
1138
|
+
program.command("devices").description("List connected hdc devices.").option(
|
|
1139
|
+
"--long",
|
|
1140
|
+
"Also ask each connected device for its OS family (HarmonyOS / OpenHarmony), version, API level, CPU ABI, device type and model."
|
|
1141
|
+
).option("--json", "Emit machine-readable JSON.").action(async (opts) => {
|
|
1142
|
+
const { config, logger } = getRuntime();
|
|
1078
1143
|
const devices = await listDevices(config);
|
|
1144
|
+
const infos = /* @__PURE__ */ new Map();
|
|
1145
|
+
if (opts.long) {
|
|
1146
|
+
for (const d of devices.filter((x) => x.status === "Connected")) {
|
|
1147
|
+
try {
|
|
1148
|
+
infos.set(d.serial, await getDeviceInfo({ config, deviceSerial: d.serial, logger }));
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
logger.warn(`Could not query ${d.serial}: ${err.message}`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1079
1154
|
if (opts.json) {
|
|
1080
|
-
|
|
1155
|
+
const out = opts.long ? devices.map((d) => ({ ...d, info: infos.get(d.serial) ?? null })) : devices;
|
|
1156
|
+
process.stdout.write(`${JSON.stringify(out, null, 2)}
|
|
1081
1157
|
`);
|
|
1082
1158
|
return;
|
|
1083
1159
|
}
|
|
@@ -1086,7 +1162,7 @@ function registerDevicesCommand(program) {
|
|
|
1086
1162
|
return;
|
|
1087
1163
|
}
|
|
1088
1164
|
for (const d of devices) {
|
|
1089
|
-
process.stdout.write(`${d.serial} ${d.connection ?? ""} ${d.status}
|
|
1165
|
+
process.stdout.write(`${opts.long ? formatLongDevice(d, infos.get(d.serial)) : `${d.serial} ${d.connection ?? ""} ${d.status}`}
|
|
1090
1166
|
`);
|
|
1091
1167
|
}
|
|
1092
1168
|
});
|
|
@@ -1096,10 +1172,14 @@ function registerDevicesCommand(program) {
|
|
|
1096
1172
|
import { sendFile, recvFile } from "@oniroproject/core";
|
|
1097
1173
|
function registerFileCommand(program) {
|
|
1098
1174
|
const file = program.command("file").description("Transfer files to/from the device (hdc file send/recv).");
|
|
1099
|
-
file.command("send <local> <remote>").description(
|
|
1175
|
+
file.command("send <local> <remote>").description(
|
|
1176
|
+
"Push a local file to the device. With --bundle, <remote> may be relative to that app's sandbox (files/x is its filesDir, haps/entry/files/x the entry module's), and the file is chowned to the app so it can read it. Prints the device path."
|
|
1177
|
+
).option("--bundle <bundle>", "Push into this app's sandbox and give the file to the app's user.").option("--device <serial>", "Target device serial.").action(async (local, remote, opts) => {
|
|
1100
1178
|
const { config, logger } = getRuntime();
|
|
1101
|
-
await sendFile({ config, local, remote, deviceSerial: opts.device, logger });
|
|
1102
|
-
logger.info(`Sent ${local} -> ${
|
|
1179
|
+
const written = await sendFile({ config, local, remote, bundle: opts.bundle, deviceSerial: opts.device, logger });
|
|
1180
|
+
logger.info(`Sent ${local} -> ${written}${opts.bundle ? ` (owned by ${opts.bundle})` : ""}.`);
|
|
1181
|
+
process.stdout.write(`${written}
|
|
1182
|
+
`);
|
|
1103
1183
|
});
|
|
1104
1184
|
file.command("recv <remote> <local>").description("Pull a file from the device.").option("--device <serial>", "Target device serial.").action(async (remote, local, opts) => {
|
|
1105
1185
|
const { config, logger } = getRuntime();
|
|
@@ -1110,23 +1190,65 @@ function registerFileCommand(program) {
|
|
|
1110
1190
|
|
|
1111
1191
|
// src/commands/lint.ts
|
|
1112
1192
|
import * as path8 from "path";
|
|
1113
|
-
import {
|
|
1193
|
+
import {
|
|
1194
|
+
detectCmdToolsFlavour,
|
|
1195
|
+
detectRuntimeOs as detectRuntimeOs2,
|
|
1196
|
+
getCmdToolsPath,
|
|
1197
|
+
runCodelinter
|
|
1198
|
+
} from "@oniroproject/core";
|
|
1199
|
+
function lintTotals(result) {
|
|
1200
|
+
if (result.summary) return result.summary;
|
|
1201
|
+
const count = (s) => result.findings.filter((f) => f.severity === s).length;
|
|
1202
|
+
return {
|
|
1203
|
+
defects: result.findings.length,
|
|
1204
|
+
errors: count("error"),
|
|
1205
|
+
warnings: count("warning"),
|
|
1206
|
+
suggestions: count("suggestion")
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
function lintExitCode(result) {
|
|
1210
|
+
if (lintTotals(result).errors > 0) return 1;
|
|
1211
|
+
const noReport = result.summary === null && result.findings.length === 0;
|
|
1212
|
+
return noReport && result.code !== 0 ? result.code : 0;
|
|
1213
|
+
}
|
|
1114
1214
|
function registerLintCommand(program) {
|
|
1115
|
-
program.command("lint [project-dir]").description(
|
|
1215
|
+
program.command("lint [project-dir]").description(
|
|
1216
|
+
"Run the OpenHarmony codelinter on a project. Defaults to the current directory. Exits 1 on error-severity findings or when codelinter fails to run; warnings alone exit 0."
|
|
1217
|
+
).option("--files <globs...>", "Specific files/globs to lint (default: the whole project).").option("--json", "Emit the report (findings, summary, notices, incomplete) as JSON.").action(async (projectDir, opts) => {
|
|
1116
1218
|
const { config, logger } = getRuntime();
|
|
1117
1219
|
const dir = path8.resolve(projectDir ?? process.cwd());
|
|
1118
1220
|
const result = await runCodelinter({ config, projectDir: dir, files: opts.files, logger });
|
|
1221
|
+
const totals = lintTotals(result);
|
|
1119
1222
|
if (opts.json) {
|
|
1120
1223
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1121
1224
|
`);
|
|
1122
1225
|
} else {
|
|
1123
1226
|
for (const f of result.findings) {
|
|
1124
|
-
|
|
1227
|
+
const at = `${f.file}:${f.line}${f.column ? `:${f.column}` : ""}`;
|
|
1228
|
+
process.stdout.write(`${at} ${f.severity} ${f.message}${f.code ? ` [${f.code}]` : ""}
|
|
1125
1229
|
`);
|
|
1126
1230
|
}
|
|
1127
|
-
|
|
1231
|
+
process.stdout.write(
|
|
1232
|
+
totals.defects === 0 ? "No defects found.\n" : `${totals.defects} defects: ${totals.errors} errors, ${totals.warnings} warnings, ${totals.suggestions} suggestions.
|
|
1233
|
+
`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
if (result.summary === null && result.findings.length === 0 && result.code !== 0) {
|
|
1237
|
+
logger.error(`codelinter exited ${result.code} without a report:
|
|
1238
|
+
${result.raw.trim().split("\n").slice(-15).join("\n")}`);
|
|
1239
|
+
}
|
|
1240
|
+
if (result.incomplete) {
|
|
1241
|
+
for (const n of result.notices) logger.warn(`codelinter: ${n}`);
|
|
1242
|
+
const cmdToolsPath = getCmdToolsPath(config);
|
|
1243
|
+
const flavour = detectCmdToolsFlavour(cmdToolsPath);
|
|
1244
|
+
const projectOs = detectRuntimeOs2(dir);
|
|
1245
|
+
if (flavour !== projectOs) {
|
|
1246
|
+
logger.warn(
|
|
1247
|
+
`The command-line tools at ${cmdToolsPath} are the ${flavour} flavour, but the project targets ${projectOs} (runtimeOS in build-profile.json5), so codelinter skipped part of its checks: treat the counts as a lower bound. For a complete report, point ONIRO_CMD_TOOLS_PATH at ${projectOs} command-line tools. The build's ArkTS compiler checks are not affected.`
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1128
1250
|
}
|
|
1129
|
-
|
|
1251
|
+
process.exitCode = lintExitCode(result);
|
|
1130
1252
|
});
|
|
1131
1253
|
}
|
|
1132
1254
|
|
|
@@ -1214,12 +1336,608 @@ function registerInputCommand(program) {
|
|
|
1214
1336
|
});
|
|
1215
1337
|
}
|
|
1216
1338
|
|
|
1339
|
+
// src/commands/logs.ts
|
|
1340
|
+
import * as os2 from "os";
|
|
1341
|
+
import * as path10 from "path";
|
|
1342
|
+
import { captureLog } from "@oniroproject/core";
|
|
1343
|
+
|
|
1344
|
+
// src/lib/logBundle.ts
|
|
1345
|
+
import * as fs4 from "fs/promises";
|
|
1346
|
+
import * as path9 from "path";
|
|
1347
|
+
import { deflateRawSync } from "zlib";
|
|
1348
|
+
import {
|
|
1349
|
+
VERSION,
|
|
1350
|
+
hdcExec,
|
|
1351
|
+
parseDeviceList,
|
|
1352
|
+
resolveDeviceSerial,
|
|
1353
|
+
shellChecked
|
|
1354
|
+
} from "@oniroproject/core";
|
|
1355
|
+
var LEVELS = {
|
|
1356
|
+
V: "V",
|
|
1357
|
+
VERBOSE: "V",
|
|
1358
|
+
D: "D",
|
|
1359
|
+
DEBUG: "D",
|
|
1360
|
+
I: "I",
|
|
1361
|
+
INFO: "I",
|
|
1362
|
+
W: "W",
|
|
1363
|
+
WARN: "W",
|
|
1364
|
+
WARNING: "W",
|
|
1365
|
+
E: "E",
|
|
1366
|
+
ERROR: "E",
|
|
1367
|
+
F: "F",
|
|
1368
|
+
FATAL: "F"
|
|
1369
|
+
};
|
|
1370
|
+
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]+.*)?)$/;
|
|
1371
|
+
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;
|
|
1372
|
+
function validateDate(date, time) {
|
|
1373
|
+
const [, monthText, dayText] = /^(\d{2})-(\d{2})$/.exec(date) ?? [];
|
|
1374
|
+
const [, hourText, minuteText, secondText, millisecondText] = /^(\d{2}):(\d{2}):(\d{2})\.(\d{3})$/.exec(time) ?? [];
|
|
1375
|
+
if (!monthText || !dayText || !hourText || !minuteText || !secondText || !millisecondText) return false;
|
|
1376
|
+
const value = new Date(Date.UTC(2e3, Number(monthText) - 1, Number(dayText), Number(hourText), Number(minuteText), Number(secondText), Number(millisecondText)));
|
|
1377
|
+
return value.getUTCMonth() === Number(monthText) - 1 && value.getUTCDate() === Number(dayText) && Number(hourText) < 24 && Number(minuteText) < 60 && Number(secondText) < 60;
|
|
1378
|
+
}
|
|
1379
|
+
function filterHilog(input, filters) {
|
|
1380
|
+
const criteria = {};
|
|
1381
|
+
if (filters.pid) criteria.pid = filters.pid.replace(/^0+(?=\d)/, "");
|
|
1382
|
+
if (filters.tag) criteria.tag = filters.tag;
|
|
1383
|
+
if (filters.level) criteria.level = LEVELS[filters.level.toUpperCase()] ?? filters.level.toUpperCase();
|
|
1384
|
+
if (Object.keys(criteria).length === 0) return null;
|
|
1385
|
+
let sawRecord = false;
|
|
1386
|
+
let keep = false;
|
|
1387
|
+
let output = "";
|
|
1388
|
+
for (const line of input.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
|
|
1389
|
+
const content = line.replace(/[\r\n]+$/, "");
|
|
1390
|
+
const match = HILOG_RECORD.exec(content);
|
|
1391
|
+
if (match?.groups) {
|
|
1392
|
+
if (!validateDate(match.groups.date, match.groups.time)) return null;
|
|
1393
|
+
sawRecord = true;
|
|
1394
|
+
keep = Object.entries(criteria).every(([key, value]) => {
|
|
1395
|
+
const field = key === "tag" ? match.groups.tag.slice(match.groups.tag.lastIndexOf("/") + 1) : match.groups[key];
|
|
1396
|
+
return key === "pid" ? field?.replace(/^0+(?=\d)/, "") === value : field === value;
|
|
1397
|
+
});
|
|
1398
|
+
} else if (/^\s*\[?\d{1,4}[-/]/.test(content) || !sawRecord && content.trim()) {
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
if (keep) output += line;
|
|
1402
|
+
}
|
|
1403
|
+
return sawRecord ? { text: output, applied: Object.keys(criteria) } : null;
|
|
1404
|
+
}
|
|
1405
|
+
function redactLogText(value, deviceSerials = []) {
|
|
1406
|
+
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]");
|
|
1407
|
+
for (const serial of deviceSerials) {
|
|
1408
|
+
if (serial) result = result.split(serial).join("[REDACTED_DEVICE]");
|
|
1409
|
+
}
|
|
1410
|
+
const ipv4 = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
|
|
1411
|
+
result = result.replace(ipv4, (address, offset, whole) => {
|
|
1412
|
+
const prefix = whole.slice(Math.max(0, offset - 48), offset);
|
|
1413
|
+
const suffix = whole.slice(offset + address.length);
|
|
1414
|
+
const contextual = /(?:ip|addr(?:ess)?|host|peer|remote|local|endpoint|server|client|target|device(?:id)?|serial)[^\r\n]{0,24}$/i.test(prefix);
|
|
1415
|
+
const hasPort = /^:\d{1,5}(?!\d)/.test(suffix);
|
|
1416
|
+
const octets = address.split(".").map(Number);
|
|
1417
|
+
return octets.every((part) => part <= 255) && (contextual || hasPort) ? "[REDACTED_IP]" : address;
|
|
1418
|
+
});
|
|
1419
|
+
return result;
|
|
1420
|
+
}
|
|
1421
|
+
function decodeText(data, relativePath) {
|
|
1422
|
+
let text;
|
|
1423
|
+
if (data.subarray(0, 2).equals(Buffer.from([255, 254]))) text = data.toString("utf16le").slice(1);
|
|
1424
|
+
else if (data.subarray(0, 2).equals(Buffer.from([254, 255]))) {
|
|
1425
|
+
const swapped = Buffer.from(data.subarray(2));
|
|
1426
|
+
for (let i = 0; i + 1 < swapped.length; i += 2) [swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
|
|
1427
|
+
text = swapped.toString("utf16le");
|
|
1428
|
+
} else {
|
|
1429
|
+
const utf8 = data.subarray(0, 3).equals(Buffer.from([239, 187, 191])) ? data.subarray(3) : data;
|
|
1430
|
+
if (utf8.includes(0)) throw new Error(`Binary data cannot be safely redacted: ${relativePath}`);
|
|
1431
|
+
try {
|
|
1432
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(utf8);
|
|
1433
|
+
} catch {
|
|
1434
|
+
throw new Error(`Non-UTF-8 data cannot be safely redacted: ${relativePath}`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if (text.includes("\0")) throw new Error(`Binary data cannot be safely redacted: ${relativePath}`);
|
|
1438
|
+
return text;
|
|
1439
|
+
}
|
|
1440
|
+
async function redactTree(source, destination, serials, boundedHilog) {
|
|
1441
|
+
await fs4.mkdir(destination, { recursive: false, mode: 448 });
|
|
1442
|
+
try {
|
|
1443
|
+
const visit = async (directory) => {
|
|
1444
|
+
for (const entry of await fs4.readdir(directory, { withFileTypes: true })) {
|
|
1445
|
+
const sourcePath = path9.join(directory, entry.name);
|
|
1446
|
+
const relative2 = path9.relative(source, sourcePath);
|
|
1447
|
+
const portableRelative = relative2.split(path9.sep).join("/");
|
|
1448
|
+
const destinationPath = path9.join(destination, relative2);
|
|
1449
|
+
if (entry.isDirectory()) {
|
|
1450
|
+
await fs4.mkdir(destinationPath, { recursive: true, mode: 448 });
|
|
1451
|
+
await visit(sourcePath);
|
|
1452
|
+
} else if (entry.isFile()) {
|
|
1453
|
+
if (portableRelative === "logs/hilog.txt") {
|
|
1454
|
+
const text2 = boundedHilog ?? "HiLog omitted from this archive because the requested duration could not be reliably applied.\n";
|
|
1455
|
+
await fs4.mkdir(path9.dirname(destinationPath), { recursive: true, mode: 448 });
|
|
1456
|
+
await fs4.writeFile(destinationPath, redactLogText(text2, serials), { encoding: "utf8", mode: 384 });
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
if (portableRelative === "logs/hilog-filtered.txt" && boundedHilog === null) continue;
|
|
1460
|
+
const text = decodeText(await fs4.readFile(sourcePath), relative2);
|
|
1461
|
+
await fs4.mkdir(path9.dirname(destinationPath), { recursive: true, mode: 448 });
|
|
1462
|
+
await fs4.writeFile(destinationPath, redactLogText(text, serials), { encoding: "utf8", mode: 384 });
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
await visit(source);
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
await fs4.rm(destination, { recursive: true, force: true });
|
|
1469
|
+
throw error;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
var CRC_TABLE = new Uint32Array(256);
|
|
1473
|
+
for (let i = 0; i < CRC_TABLE.length; i++) {
|
|
1474
|
+
let value = i;
|
|
1475
|
+
for (let bit = 0; bit < 8; bit++) value = value & 1 ? 3988292384 ^ value >>> 1 : value >>> 1;
|
|
1476
|
+
CRC_TABLE[i] = value >>> 0;
|
|
1477
|
+
}
|
|
1478
|
+
function crc32(data) {
|
|
1479
|
+
let crc = 4294967295;
|
|
1480
|
+
for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
|
|
1481
|
+
return (crc ^ 4294967295) >>> 0;
|
|
1482
|
+
}
|
|
1483
|
+
function dosDateTime(date) {
|
|
1484
|
+
const year = Math.max(1980, date.getFullYear());
|
|
1485
|
+
return {
|
|
1486
|
+
date: year - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate(),
|
|
1487
|
+
time: date.getHours() << 11 | date.getMinutes() << 5 | Math.floor(date.getSeconds() / 2)
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
async function writeZip(source, archivePath) {
|
|
1491
|
+
const files = [];
|
|
1492
|
+
const visit = async (directory) => {
|
|
1493
|
+
for (const entry of await fs4.readdir(directory, { withFileTypes: true })) {
|
|
1494
|
+
const filePath = path9.join(directory, entry.name);
|
|
1495
|
+
if (entry.isDirectory()) await visit(filePath);
|
|
1496
|
+
else if (entry.isFile()) files.push(filePath);
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
await visit(source);
|
|
1500
|
+
const localParts = [];
|
|
1501
|
+
const centralParts = [];
|
|
1502
|
+
let offset = 0;
|
|
1503
|
+
const stamp = dosDateTime(/* @__PURE__ */ new Date());
|
|
1504
|
+
for (const filePath of files) {
|
|
1505
|
+
const name = Buffer.from(path9.relative(source, filePath).split(path9.sep).join("/"), "utf8");
|
|
1506
|
+
const data = await fs4.readFile(filePath);
|
|
1507
|
+
const compressed = deflateRawSync(data);
|
|
1508
|
+
const checksum = crc32(data);
|
|
1509
|
+
const local = Buffer.alloc(30);
|
|
1510
|
+
local.writeUInt32LE(67324752, 0);
|
|
1511
|
+
local.writeUInt16LE(20, 4);
|
|
1512
|
+
local.writeUInt16LE(2048, 6);
|
|
1513
|
+
local.writeUInt16LE(8, 8);
|
|
1514
|
+
local.writeUInt16LE(stamp.time, 10);
|
|
1515
|
+
local.writeUInt16LE(stamp.date, 12);
|
|
1516
|
+
local.writeUInt32LE(checksum, 14);
|
|
1517
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
1518
|
+
local.writeUInt32LE(data.length, 22);
|
|
1519
|
+
local.writeUInt16LE(name.length, 26);
|
|
1520
|
+
local.writeUInt16LE(0, 28);
|
|
1521
|
+
localParts.push(local, name, compressed);
|
|
1522
|
+
const central = Buffer.alloc(46);
|
|
1523
|
+
central.writeUInt32LE(33639248, 0);
|
|
1524
|
+
central.writeUInt16LE(20, 4);
|
|
1525
|
+
central.writeUInt16LE(20, 6);
|
|
1526
|
+
central.writeUInt16LE(2048, 8);
|
|
1527
|
+
central.writeUInt16LE(8, 10);
|
|
1528
|
+
central.writeUInt16LE(stamp.time, 12);
|
|
1529
|
+
central.writeUInt16LE(stamp.date, 14);
|
|
1530
|
+
central.writeUInt32LE(checksum, 16);
|
|
1531
|
+
central.writeUInt32LE(compressed.length, 20);
|
|
1532
|
+
central.writeUInt32LE(data.length, 24);
|
|
1533
|
+
central.writeUInt16LE(name.length, 28);
|
|
1534
|
+
central.writeUInt16LE(0, 30);
|
|
1535
|
+
central.writeUInt16LE(0, 32);
|
|
1536
|
+
central.writeUInt16LE(0, 34);
|
|
1537
|
+
central.writeUInt16LE(0, 36);
|
|
1538
|
+
central.writeUInt32LE(0, 38);
|
|
1539
|
+
central.writeUInt32LE(offset, 42);
|
|
1540
|
+
centralParts.push(central, name);
|
|
1541
|
+
offset += local.length + name.length + compressed.length;
|
|
1542
|
+
}
|
|
1543
|
+
const centralDirectory = Buffer.concat(centralParts);
|
|
1544
|
+
const end = Buffer.alloc(22);
|
|
1545
|
+
end.writeUInt32LE(101010256, 0);
|
|
1546
|
+
end.writeUInt16LE(0, 4);
|
|
1547
|
+
end.writeUInt16LE(0, 6);
|
|
1548
|
+
end.writeUInt16LE(files.length, 8);
|
|
1549
|
+
end.writeUInt16LE(files.length, 10);
|
|
1550
|
+
end.writeUInt32LE(centralDirectory.length, 12);
|
|
1551
|
+
end.writeUInt32LE(offset, 16);
|
|
1552
|
+
end.writeUInt16LE(0, 20);
|
|
1553
|
+
await fs4.writeFile(archivePath, Buffer.concat([...localParts, centralDirectory, end]), { flag: "wx", mode: 384 });
|
|
1554
|
+
}
|
|
1555
|
+
function commandFailure(result) {
|
|
1556
|
+
if (result.code !== 0) return `hdc exited with code ${result.code}`;
|
|
1557
|
+
if (result.stderr.trim()) return "hdc wrote to stderr";
|
|
1558
|
+
const contentError = result.stdout.split(/\r?\n/).find((line) => FAILURE_TEXT.test(line))?.trim() ?? null;
|
|
1559
|
+
return contentError ? `command output indicates failure: ${contentError}` : null;
|
|
1560
|
+
}
|
|
1561
|
+
function errorMessage(error) {
|
|
1562
|
+
return error instanceof Error ? error.message : String(error);
|
|
1563
|
+
}
|
|
1564
|
+
async function uniqueBundleDirectory(outputDirectory, stamp) {
|
|
1565
|
+
await fs4.mkdir(outputDirectory, { recursive: true, mode: 448 });
|
|
1566
|
+
for (let suffix = 0; ; suffix++) {
|
|
1567
|
+
const name = `incident-${stamp}${suffix ? `-${suffix}` : ""}`;
|
|
1568
|
+
const directory = path9.join(outputDirectory, name);
|
|
1569
|
+
try {
|
|
1570
|
+
await fs4.mkdir(directory, { mode: 448 });
|
|
1571
|
+
return directory;
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
if (error.code !== "EEXIST") throw error;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
async function collectLogBundle(options) {
|
|
1578
|
+
if (options.packageName && !/^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/.test(options.packageName)) {
|
|
1579
|
+
throw new Error("packageName must be a reverse-DNS bundle name.");
|
|
1580
|
+
}
|
|
1581
|
+
if (options.pid && !/^[1-9]\d*$/.test(options.pid)) throw new Error("pid must be a positive integer.");
|
|
1582
|
+
if (options.tag && !/^[^\s/\x00-\x1f\x7f]+$/.test(options.tag)) throw new Error("tag must be a single HiLog tag.");
|
|
1583
|
+
if (options.level && !LEVELS[options.level.toUpperCase()]) throw new Error("level is not a recognized HiLog level.");
|
|
1584
|
+
const durationSeconds = options.durationSeconds ?? 600;
|
|
1585
|
+
if (!Number.isSafeInteger(durationSeconds) || durationSeconds < 1 || durationSeconds > 31536e3) {
|
|
1586
|
+
throw new Error("durationSeconds must be an integer between 1 and 31536000.");
|
|
1587
|
+
}
|
|
1588
|
+
const started = /* @__PURE__ */ new Date();
|
|
1589
|
+
const stamp = started.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
1590
|
+
const bundleDirectory = await uniqueBundleDirectory(path9.resolve(options.outputDirectory), stamp);
|
|
1591
|
+
const records = [];
|
|
1592
|
+
const serials = [];
|
|
1593
|
+
let replacementWarning = false;
|
|
1594
|
+
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 }));
|
|
1595
|
+
const manifest = {
|
|
1596
|
+
schemaVersion: "1.0",
|
|
1597
|
+
collectorVersion: VERSION,
|
|
1598
|
+
collectionStartUtc: started.toISOString(),
|
|
1599
|
+
collectionEndUtc: null,
|
|
1600
|
+
host: { os: process.platform },
|
|
1601
|
+
target: null,
|
|
1602
|
+
packageName: options.packageName ?? null,
|
|
1603
|
+
pid: options.pid ?? null,
|
|
1604
|
+
filters: {
|
|
1605
|
+
tag: options.tag ?? null,
|
|
1606
|
+
level: options.level ?? null,
|
|
1607
|
+
pid: options.pid ?? null,
|
|
1608
|
+
packageName: options.packageName ?? null,
|
|
1609
|
+
durationSeconds
|
|
1610
|
+
},
|
|
1611
|
+
filterStatus: {
|
|
1612
|
+
duration: "not-applied",
|
|
1613
|
+
packageName: options.packageName ? "failed" : "not-requested",
|
|
1614
|
+
pid: options.pid ? "not-applied" : "not-requested",
|
|
1615
|
+
tag: options.tag ? "not-applied" : "not-requested",
|
|
1616
|
+
level: options.level ? "not-applied" : "not-requested"
|
|
1617
|
+
},
|
|
1618
|
+
hilogWindowApplied: false,
|
|
1619
|
+
redactionRequested: options.redact,
|
|
1620
|
+
redactionStatus: options.redact ? "pending" : "not-requested",
|
|
1621
|
+
archiveStatus: { status: "pending", path: null },
|
|
1622
|
+
commands: records,
|
|
1623
|
+
files: [],
|
|
1624
|
+
notes: [
|
|
1625
|
+
"Collection is read-only on the target device.",
|
|
1626
|
+
"hilog -x captures the current device buffer; duration is applied locally against device time when log timestamps are recognized.",
|
|
1627
|
+
"Redaction is heuristic and does not guarantee that a bundle is safe to publish."
|
|
1628
|
+
]
|
|
1629
|
+
};
|
|
1630
|
+
const run = async (name, relativeOutput, args, deviceSerial) => {
|
|
1631
|
+
const outputPath = path9.join(bundleDirectory, relativeOutput);
|
|
1632
|
+
await fs4.mkdir(path9.dirname(outputPath), { recursive: true, mode: 448 });
|
|
1633
|
+
let result = null;
|
|
1634
|
+
let failure = null;
|
|
1635
|
+
try {
|
|
1636
|
+
result = await execute(args, deviceSerial);
|
|
1637
|
+
await fs4.writeFile(outputPath, result.stdout, { encoding: "utf8", mode: 384 });
|
|
1638
|
+
if (result.stderr) await fs4.writeFile(`${outputPath}.stderr.txt`, result.stderr, { encoding: "utf8", mode: 384 });
|
|
1639
|
+
if (result.stdout.includes("\uFFFD") || result.stderr.includes("\uFFFD")) {
|
|
1640
|
+
replacementWarning = true;
|
|
1641
|
+
manifest.notes.push(`${name} output contains Unicode replacement characters; review the redacted evidence before sharing.`);
|
|
1642
|
+
}
|
|
1643
|
+
failure = commandFailure(result);
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
failure = errorMessage(error);
|
|
1646
|
+
await fs4.writeFile(outputPath, "", { encoding: "utf8", mode: 384 });
|
|
1647
|
+
}
|
|
1648
|
+
const record = {
|
|
1649
|
+
name,
|
|
1650
|
+
status: failure ? "failed" : "success",
|
|
1651
|
+
output: relativeOutput,
|
|
1652
|
+
stderr: result?.stderr ? `${relativeOutput}.stderr.txt` : null,
|
|
1653
|
+
exitCode: result?.code ?? null,
|
|
1654
|
+
error: failure
|
|
1655
|
+
};
|
|
1656
|
+
records.push(record);
|
|
1657
|
+
return record;
|
|
1658
|
+
};
|
|
1659
|
+
const targetRecord = await run("list-targets", "device/targets.txt", ["list", "targets", "-v"]);
|
|
1660
|
+
const targetOutput = await fs4.readFile(path9.join(bundleDirectory, "device/targets.txt"), "utf8").catch(() => "");
|
|
1661
|
+
const targets = parseDeviceList(targetOutput).filter((target) => target.status === "Connected" || target.status === "Unknown");
|
|
1662
|
+
serials.push(...parseDeviceList(targetOutput).map((target) => target.serial));
|
|
1663
|
+
const requestedDevice = resolveDeviceSerial(options.deviceSerial);
|
|
1664
|
+
let selectedDevice = null;
|
|
1665
|
+
let targetError = null;
|
|
1666
|
+
if (targetRecord.status !== "success") targetError = targetRecord.error ?? "hdc target discovery failed";
|
|
1667
|
+
else if (requestedDevice) {
|
|
1668
|
+
selectedDevice = targets.find((target) => target.serial === requestedDevice)?.serial ?? null;
|
|
1669
|
+
if (!selectedDevice) targetError = "selected device was not discovered as connected";
|
|
1670
|
+
} else if (targets.length === 0) targetError = "hdc did not discover a connected device";
|
|
1671
|
+
else if (targets.length > 1) targetError = "multiple devices are connected; specify --device";
|
|
1672
|
+
else selectedDevice = targets[0].serial;
|
|
1673
|
+
let boundedHilog = null;
|
|
1674
|
+
let durationApplied = false;
|
|
1675
|
+
let durationFailureReason = "device clock or HiLog collection failed";
|
|
1676
|
+
if (targetError) {
|
|
1677
|
+
targetRecord.status = "failed";
|
|
1678
|
+
targetRecord.error = targetError;
|
|
1679
|
+
manifest.notes.push("Device collection stopped because target discovery did not identify exactly one selected connected device.");
|
|
1680
|
+
} else {
|
|
1681
|
+
manifest.target = selectedDevice;
|
|
1682
|
+
const shell = (args) => run(args[0], args[1], args.slice(2), selectedDevice);
|
|
1683
|
+
const captureStartedHostMs = Date.now();
|
|
1684
|
+
const deviceTimeBefore = await shell([
|
|
1685
|
+
"device-time-before",
|
|
1686
|
+
"device/device-time-before.txt",
|
|
1687
|
+
"shell",
|
|
1688
|
+
"date",
|
|
1689
|
+
"+%Y-%m-%dT%H:%M:%S"
|
|
1690
|
+
]);
|
|
1691
|
+
const hilog = await shell(["hilog-snapshot", "logs/hilog.txt", "shell", "hilog", "-x"]);
|
|
1692
|
+
const deviceTimeAfter = await shell([
|
|
1693
|
+
"device-time-after",
|
|
1694
|
+
"device/device-time-after.txt",
|
|
1695
|
+
"shell",
|
|
1696
|
+
"date",
|
|
1697
|
+
"+%Y-%m-%dT%H:%M:%S"
|
|
1698
|
+
]);
|
|
1699
|
+
const hostElapsedMs = Date.now() - captureStartedHostMs;
|
|
1700
|
+
if (deviceTimeBefore.status === "success" && hilog.status === "success" && deviceTimeAfter.status === "success") {
|
|
1701
|
+
const [beforeText, afterText, fullHilog] = await Promise.all([
|
|
1702
|
+
fs4.readFile(path9.join(bundleDirectory, "device/device-time-before.txt"), "utf8"),
|
|
1703
|
+
fs4.readFile(path9.join(bundleDirectory, "device/device-time-after.txt"), "utf8"),
|
|
1704
|
+
fs4.readFile(path9.join(bundleDirectory, "logs/hilog.txt"), "utf8")
|
|
1705
|
+
]);
|
|
1706
|
+
const window = filterHilogWindow(fullHilog, beforeText, afterText, durationSeconds, hostElapsedMs);
|
|
1707
|
+
if (window.text === null) durationFailureReason = window.reason;
|
|
1708
|
+
else boundedHilog = window.text;
|
|
1709
|
+
} else if (deviceTimeBefore.status !== "success" || deviceTimeAfter.status !== "success") {
|
|
1710
|
+
durationFailureReason = "device clock could not be read";
|
|
1711
|
+
} else if (hilog.status !== "success") {
|
|
1712
|
+
durationFailureReason = "HiLog snapshot collection failed";
|
|
1713
|
+
}
|
|
1714
|
+
durationApplied = boundedHilog !== null;
|
|
1715
|
+
manifest.hilogWindowApplied = durationApplied;
|
|
1716
|
+
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"];
|
|
1717
|
+
const faultlog = await shell(faultArgs);
|
|
1718
|
+
manifest.filterStatus = {
|
|
1719
|
+
duration: durationApplied ? "applied" : "not-applied",
|
|
1720
|
+
packageName: options.packageName ? faultlog.status === "success" ? "applied" : "failed" : "not-requested",
|
|
1721
|
+
pid: options.pid ? "not-applied" : "not-requested",
|
|
1722
|
+
tag: options.tag ? "not-applied" : "not-requested",
|
|
1723
|
+
level: options.level ? "not-applied" : "not-requested"
|
|
1724
|
+
};
|
|
1725
|
+
if (!durationApplied) {
|
|
1726
|
+
manifest.notes.push(
|
|
1727
|
+
`HiLog duration was not applied (${durationFailureReason}); the full snapshot is kept only in the local raw bundle and omitted from the redacted archive.`
|
|
1728
|
+
);
|
|
1729
|
+
} else if (boundedHilog !== null && (options.pid || options.tag || options.level)) {
|
|
1730
|
+
const filtered = filterHilog(boundedHilog, { pid: options.pid, tag: options.tag, level: options.level });
|
|
1731
|
+
if (filtered) {
|
|
1732
|
+
await fs4.writeFile(path9.join(bundleDirectory, "logs/hilog-filtered.txt"), filtered.text, { encoding: "utf8", mode: 384 });
|
|
1733
|
+
const status = manifest.filterStatus;
|
|
1734
|
+
for (const key of filtered.applied) status[key] = "applied";
|
|
1735
|
+
} else {
|
|
1736
|
+
manifest.notes.push("Requested HiLog PID/tag/level filters were not applied because the windowed snapshot format could not be parsed reliably.");
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
await shell(["device-name", "device/device-info.txt", "shell", "param", "get", "const.product.name"]);
|
|
1740
|
+
await shell(["process-list", "device/process-list.txt", "shell", "ps", "-ef"]);
|
|
1741
|
+
await shell(["memory", "device/memory.txt", "shell", "hidumper", "--mem"]);
|
|
1742
|
+
await shell(["threads", "device/threads.txt", "shell", "hidumper", "-p"]);
|
|
1743
|
+
}
|
|
1744
|
+
const ended = /* @__PURE__ */ new Date();
|
|
1745
|
+
manifest.collectionEndUtc = ended.toISOString();
|
|
1746
|
+
const files = records.flatMap((record) => [record.output, record.stderr].filter((file) => Boolean(file)));
|
|
1747
|
+
files.push("manifest.json", "notes.txt");
|
|
1748
|
+
if (await fs4.access(path9.join(bundleDirectory, "logs/hilog-filtered.txt")).then(() => true, () => false)) {
|
|
1749
|
+
files.push("logs/hilog-filtered.txt");
|
|
1750
|
+
}
|
|
1751
|
+
manifest.files = files;
|
|
1752
|
+
await fs4.writeFile(path9.join(bundleDirectory, "notes.txt"), "Add reproduction steps and precise event timestamps here.\n", { encoding: "utf8", mode: 384 });
|
|
1753
|
+
const durationFailure = !durationApplied;
|
|
1754
|
+
const criticalFailure = Boolean(targetError) || records.some((record) => record.name === "hilog-snapshot" && record.status !== "success");
|
|
1755
|
+
const anyCommandFailure = records.some((record) => record.status !== "success");
|
|
1756
|
+
let redactedDirectory = null;
|
|
1757
|
+
let archivePath = null;
|
|
1758
|
+
let bundleError;
|
|
1759
|
+
try {
|
|
1760
|
+
if (options.redact) {
|
|
1761
|
+
redactedDirectory = `${bundleDirectory}-redacted`;
|
|
1762
|
+
manifest.redactionStatus = replacementWarning ? "review-required" : "success";
|
|
1763
|
+
manifest.archiveStatus = { status: "success", path: `${path9.basename(redactedDirectory)}.zip` };
|
|
1764
|
+
await fs4.writeFile(path9.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
1765
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1766
|
+
await redactTree(bundleDirectory, redactedDirectory, serials, boundedHilog);
|
|
1767
|
+
archivePath = `${redactedDirectory}.zip`;
|
|
1768
|
+
await writeZip(redactedDirectory, archivePath);
|
|
1769
|
+
} else {
|
|
1770
|
+
archivePath = `${bundleDirectory}.zip`;
|
|
1771
|
+
manifest.archiveStatus = { status: "success", path: path9.basename(archivePath) };
|
|
1772
|
+
await fs4.writeFile(path9.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
1773
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1774
|
+
await writeZip(bundleDirectory, archivePath);
|
|
1775
|
+
}
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
bundleError = options.redact ? `redaction or archive creation failed: ${errorMessage(error)}` : `archive creation failed: ${errorMessage(error)}`;
|
|
1778
|
+
archivePath = null;
|
|
1779
|
+
manifest.redactionStatus = options.redact ? "failed" : "not-requested";
|
|
1780
|
+
manifest.archiveStatus = { status: "failed", path: null };
|
|
1781
|
+
manifest.notes.push(bundleError);
|
|
1782
|
+
await fs4.rm(`${redactedDirectory ?? bundleDirectory}.zip`, { force: true }).catch(() => void 0);
|
|
1783
|
+
if (redactedDirectory) await fs4.rm(redactedDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
1784
|
+
await fs4.writeFile(path9.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
1785
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1786
|
+
redactedDirectory = null;
|
|
1787
|
+
}
|
|
1788
|
+
return {
|
|
1789
|
+
bundleDirectory,
|
|
1790
|
+
archivePath,
|
|
1791
|
+
redactedDirectory,
|
|
1792
|
+
complete: !criticalFailure && !anyCommandFailure && !bundleError && !replacementWarning && !durationFailure,
|
|
1793
|
+
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)
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
function parseDeviceClock(value) {
|
|
1797
|
+
const match = /^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\s*$/.exec(value);
|
|
1798
|
+
if (!match) return null;
|
|
1799
|
+
const [, year, month, day, hour, minute, second] = match;
|
|
1800
|
+
const timestamp = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
|
|
1801
|
+
const date = new Date(timestamp);
|
|
1802
|
+
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;
|
|
1803
|
+
}
|
|
1804
|
+
function parseHilogTimestamp(datePart, timePart, referenceMs) {
|
|
1805
|
+
if (!validateDate(datePart, timePart)) return null;
|
|
1806
|
+
const [month, day] = datePart.split("-").map(Number);
|
|
1807
|
+
const [hour, minute, second, millisecond] = timePart.split(/[:.]/).map(Number);
|
|
1808
|
+
const referenceYear = new Date(referenceMs).getUTCFullYear();
|
|
1809
|
+
const candidates = [referenceYear - 1, referenceYear, referenceYear + 1].map((year) => Date.UTC(year, month - 1, day, hour, minute, second, millisecond)).filter((timestamp) => {
|
|
1810
|
+
const candidate = new Date(timestamp);
|
|
1811
|
+
return candidate.getUTCMonth() === month - 1 && candidate.getUTCDate() === day;
|
|
1812
|
+
}).sort((left, right) => Math.abs(left - referenceMs) - Math.abs(right - referenceMs));
|
|
1813
|
+
const closest = candidates[0];
|
|
1814
|
+
return closest !== void 0 && closest <= referenceMs + 2e3 ? closest : null;
|
|
1815
|
+
}
|
|
1816
|
+
function filterHilogWindow(input, deviceClockBefore, deviceClockAfter, durationSeconds, hostElapsedMs) {
|
|
1817
|
+
if (!Number.isSafeInteger(durationSeconds) || durationSeconds < 1 || durationSeconds > 31536e3) {
|
|
1818
|
+
return { text: null, reason: "requested duration is invalid" };
|
|
1819
|
+
}
|
|
1820
|
+
const beforeMs = parseDeviceClock(deviceClockBefore);
|
|
1821
|
+
const afterMs = parseDeviceClock(deviceClockAfter);
|
|
1822
|
+
if (beforeMs === null || afterMs === null) return { text: null, reason: "device clock output was not recognized" };
|
|
1823
|
+
const deviceElapsedMs = afterMs - beforeMs;
|
|
1824
|
+
if (deviceElapsedMs < -1e3 || deviceElapsedMs > Math.max(0, hostElapsedMs) + 5e3) {
|
|
1825
|
+
return { text: null, reason: "device clock changed inconsistently during capture" };
|
|
1826
|
+
}
|
|
1827
|
+
const windowEnd = afterMs + 999;
|
|
1828
|
+
const windowStart = windowEnd - durationSeconds * 1e3;
|
|
1829
|
+
let sawRecord = false;
|
|
1830
|
+
let keep = false;
|
|
1831
|
+
let output = "";
|
|
1832
|
+
for (const line of input.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
|
|
1833
|
+
const content = line.replace(/[\r\n]+$/, "");
|
|
1834
|
+
const match = HILOG_RECORD.exec(content);
|
|
1835
|
+
if (match?.groups) {
|
|
1836
|
+
if (!validateDate(match.groups.date, match.groups.time)) {
|
|
1837
|
+
return { text: null, reason: "HiLog contains an invalid timestamp" };
|
|
1838
|
+
}
|
|
1839
|
+
const timestamp = parseHilogTimestamp(match.groups.date, match.groups.time, afterMs);
|
|
1840
|
+
if (timestamp === null || timestamp > windowEnd) {
|
|
1841
|
+
return { text: null, reason: "HiLog timestamps do not agree with the device clock" };
|
|
1842
|
+
}
|
|
1843
|
+
sawRecord = true;
|
|
1844
|
+
keep = timestamp >= windowStart;
|
|
1845
|
+
} else if (/^-{3,}\s*beginning of\b/i.test(content.trim())) {
|
|
1846
|
+
continue;
|
|
1847
|
+
} else if (/^\s*\[?\d{1,4}[-/]/.test(content) || /^\s*\d{2}:\d{2}:\d{2}/.test(content)) {
|
|
1848
|
+
return { text: null, reason: "HiLog contains timestamped lines in an unrecognized format" };
|
|
1849
|
+
} else if (!sawRecord && content.trim()) {
|
|
1850
|
+
return { text: null, reason: "HiLog contains unclassified lines before the first record" };
|
|
1851
|
+
}
|
|
1852
|
+
if (keep) output += line;
|
|
1853
|
+
}
|
|
1854
|
+
return sawRecord ? { text: output } : { text: null, reason: "no recognized timestamped HiLog records were found" };
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// src/commands/logs.ts
|
|
1858
|
+
function registerLogsCommand(program) {
|
|
1859
|
+
const logs = program.command("logs").description(
|
|
1860
|
+
"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."
|
|
1861
|
+
).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) => {
|
|
1862
|
+
if (!opts.output) logs.error("error: required option '-o, --output <file>' not specified");
|
|
1863
|
+
const { config, logger } = getRuntime();
|
|
1864
|
+
const file = path10.resolve(opts.output);
|
|
1865
|
+
const abort = new AbortController();
|
|
1866
|
+
const stop = () => abort.abort();
|
|
1867
|
+
process.once("SIGINT", stop);
|
|
1868
|
+
process.once("SIGTERM", stop);
|
|
1869
|
+
logger.info(`Capturing hilog to ${file}${opts.for ? ` for ${opts.for} ms` : " until interrupted"}...`);
|
|
1870
|
+
const result = await captureLog({
|
|
1871
|
+
config,
|
|
1872
|
+
file,
|
|
1873
|
+
durationMs: opts.for !== void 0 ? Number(opts.for) : void 0,
|
|
1874
|
+
pattern: opts.grep ? new RegExp(opts.grep) : void 0,
|
|
1875
|
+
clear: opts.clear,
|
|
1876
|
+
domain: opts.domain,
|
|
1877
|
+
deviceSerial: opts.device,
|
|
1878
|
+
abortSignal: abort.signal,
|
|
1879
|
+
logger
|
|
1880
|
+
});
|
|
1881
|
+
logger.info(`Wrote ${result.lines} lines to ${result.file} (${result.reconnects} reconnects).`);
|
|
1882
|
+
process.stdout.write(`${result.file}
|
|
1883
|
+
`);
|
|
1884
|
+
});
|
|
1885
|
+
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) => {
|
|
1886
|
+
const { config } = getRuntime();
|
|
1887
|
+
const result = await collectLogBundle({
|
|
1888
|
+
config,
|
|
1889
|
+
outputDirectory: opts.output,
|
|
1890
|
+
deviceSerial: opts.device,
|
|
1891
|
+
packageName: opts.package,
|
|
1892
|
+
pid: opts.pid,
|
|
1893
|
+
tag: opts.tag,
|
|
1894
|
+
level: opts.level,
|
|
1895
|
+
durationSeconds: Number(opts.duration),
|
|
1896
|
+
redact: Boolean(opts.redact)
|
|
1897
|
+
});
|
|
1898
|
+
const output = opts.json ? `${JSON.stringify({ ...result, status: result.complete ? "complete" : "partial" }, null, 2)}
|
|
1899
|
+
` : `${result.archivePath ?? result.bundleDirectory}
|
|
1900
|
+
`;
|
|
1901
|
+
process.stdout.write(output);
|
|
1902
|
+
if (!result.complete) {
|
|
1903
|
+
throw new Error(`Bundle is partial; inspect manifest.json. ${result.error ?? "One or more collectors failed."}`);
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// src/commands/hdc.ts
|
|
1909
|
+
import { getHdcPath, hdcTargetArgsFor, spawnCompat } from "@oniroproject/core";
|
|
1910
|
+
function hdcPassthroughArgv(args, device) {
|
|
1911
|
+
return args[0] === "-t" ? [...args] : [...hdcTargetArgsFor(args, device), ...args];
|
|
1912
|
+
}
|
|
1913
|
+
function registerHdcCommand(program) {
|
|
1914
|
+
program.command("hdc").description(
|
|
1915
|
+
"Run the hdc binary oniro-app itself uses (see `cmdtools status`), addressed at --device or ONIRO_DEVICE_SERIAL, with stdio passed through. Exits with hdc's own code, which is 0 even for a `[Fail]` line. Example: oniro-app hdc shell param get const.ohos.apiversion"
|
|
1916
|
+
).argument("[args...]", "hdc arguments, passed through verbatim").option("--device <serial>", "Target device serial.").option("--path", "Print the hdc binary path and exit.").passThroughOptions().allowUnknownOption().action(async (args, opts) => {
|
|
1917
|
+
const { config } = getRuntime();
|
|
1918
|
+
const hdc = getHdcPath(config);
|
|
1919
|
+
if (opts.path) {
|
|
1920
|
+
process.stdout.write(`${hdc}
|
|
1921
|
+
`);
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
await new Promise((resolve9, reject) => {
|
|
1925
|
+
const child = spawnCompat(hdc, hdcPassthroughArgv(args, opts.device), { stdio: "inherit" });
|
|
1926
|
+
child.on("error", (err) => reject(new Error(`Failed to run ${hdc}: ${err.message}`)));
|
|
1927
|
+
child.on("close", (code) => {
|
|
1928
|
+
process.exitCode = code ?? 1;
|
|
1929
|
+
resolve9();
|
|
1930
|
+
});
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1217
1935
|
// src/bin/oniro-app.ts
|
|
1218
|
-
var CLI_VERSION = "0.
|
|
1936
|
+
var CLI_VERSION = "0.15.0";
|
|
1219
1937
|
function buildProgram() {
|
|
1220
1938
|
const program = new Command4().name("oniro-app").description(
|
|
1221
1939
|
"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."
|
|
1222
|
-
).version(CLI_VERSION).showHelpAfterError();
|
|
1940
|
+
).version(CLI_VERSION).showHelpAfterError().enablePositionalOptions();
|
|
1223
1941
|
registerSdkCommand(program);
|
|
1224
1942
|
registerCmdToolsCommand(program);
|
|
1225
1943
|
registerEmulatorCommand(program);
|
|
@@ -1238,6 +1956,8 @@ function buildProgram() {
|
|
|
1238
1956
|
registerFileCommand(program);
|
|
1239
1957
|
registerLintCommand(program);
|
|
1240
1958
|
registerInputCommand(program);
|
|
1959
|
+
registerLogsCommand(program);
|
|
1960
|
+
registerHdcCommand(program);
|
|
1241
1961
|
return program;
|
|
1242
1962
|
}
|
|
1243
1963
|
async function main() {
|