@oniroproject/oniro-app 0.14.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 +39 -0
- package/dist/oniro-app.js +547 -9
- package/dist/oniro-app.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -221,6 +221,45 @@ Local paths may be relative, or absolute with either slash. A device path that a
|
|
|
221
221
|
| `--domain <domain>` | hilog domain filter (e.g. `0xD003900`). |
|
|
222
222
|
| `--device <serial>` | Target device serial. |
|
|
223
223
|
|
|
224
|
+
#### `logs collect` — create a diagnostic bundle
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
oniro-app logs collect --redact --package com.example.app
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The command requires exactly one connected device unless `--device` (or
|
|
231
|
+
`ONIRO_DEVICE_SERIAL`) selects one. It captures the current `hilog -x` buffer,
|
|
232
|
+
faultlog evidence, device information, process list, and memory/thread snapshots.
|
|
233
|
+
Because `hilog -x` has no duration argument, `--duration` filters recognized
|
|
234
|
+
HiLog timestamps locally using device clock readings before and after the snapshot.
|
|
235
|
+
If the clock or log format cannot be parsed reliably, the manifest records the
|
|
236
|
+
window as unapplied, the command exits non-zero, and the full snapshot is omitted
|
|
237
|
+
from the redacted archive (it remains only in the local raw bundle).
|
|
238
|
+
`--pid`, `--tag`, and `--level` create an additional filtered HiLog view from the
|
|
239
|
+
time-bounded records when their format can be parsed reliably. `--package`
|
|
240
|
+
targets `hidumper -e --print <bundle> -n 5` for faultlog evidence.
|
|
241
|
+
|
|
242
|
+
`--redact` creates a sibling `incident-...-redacted` directory and ZIP; the
|
|
243
|
+
archive is built only from that redacted copy. Without `--redact`, the archive
|
|
244
|
+
contains raw sensitive data. Redaction is heuristic, fails closed on binary or
|
|
245
|
+
unknown-encoding files, and does not guarantee that a bundle is safe to publish.
|
|
246
|
+
Collection continues after individual command failures and records each result
|
|
247
|
+
in `manifest.json`; the CLI exits non-zero for a partial bundle. The default
|
|
248
|
+
output is under `~/.oniro/incident-bundles`; bundle files are created with
|
|
249
|
+
owner-only permissions where the host OS supports POSIX modes.
|
|
250
|
+
|
|
251
|
+
| Flag | Description |
|
|
252
|
+
| --- | --- |
|
|
253
|
+
| `-o, --output <directory>` | Parent directory for bundles (default `~/.oniro/incident-bundles`). |
|
|
254
|
+
| `--device <serial>` | Target device serial. |
|
|
255
|
+
| `--package <bundle>` | Bundle name for package-specific faultlog collection. |
|
|
256
|
+
| `--pid <pid>` | Create an exact-PID filtered HiLog view. |
|
|
257
|
+
| `--tag <tag>` | Create an exact-tag filtered HiLog view. |
|
|
258
|
+
| `--level <level>` | Create a filtered view for `V`, `D`, `I`, `W`, `E`, or `F`. |
|
|
259
|
+
| `--duration <seconds>` | Include HiLog records from the last N seconds (default `600`; maximum `31536000`). |
|
|
260
|
+
| `--redact` | Redact text evidence; create the archive only from the redacted copy. |
|
|
261
|
+
| `--json` | Emit bundle paths and completion status as JSON. |
|
|
262
|
+
|
|
224
263
|
#### `hdc` — run the bundled hdc
|
|
225
264
|
|
|
226
265
|
`hdc [args...]` runs the `hdc` binary oniro-app itself uses (the one `cmdtools status` shows), with stdio passed through, so it needs nothing on `PATH`. Device commands are addressed at `--device` or `ONIRO_DEVICE_SERIAL` unless the arguments start with `-t`. `hdc --path` prints the binary's location. The exit code is hdc's own, which is 0 even for a `[Fail]` line.
|
package/dist/oniro-app.js
CHANGED
|
@@ -1122,9 +1122,9 @@ import { getDeviceInfo, listDevices } from "@oniroproject/core";
|
|
|
1122
1122
|
function formatLongDevice(d, info) {
|
|
1123
1123
|
const head = `${d.serial} ${d.connection ?? ""} ${d.status}`;
|
|
1124
1124
|
if (!info) return head;
|
|
1125
|
-
const
|
|
1125
|
+
const os3 = [info.osFamily, info.osFamily === "HarmonyOS" ? info.osDistVersion : void 0].filter(Boolean).join(" ");
|
|
1126
1126
|
const fields = [
|
|
1127
|
-
["os",
|
|
1127
|
+
["os", os3 || void 0],
|
|
1128
1128
|
["api", info.sdkApiVersion],
|
|
1129
1129
|
["abi", info.abi],
|
|
1130
1130
|
["type", info.deviceType],
|
|
@@ -1337,14 +1337,531 @@ function registerInputCommand(program) {
|
|
|
1337
1337
|
}
|
|
1338
1338
|
|
|
1339
1339
|
// src/commands/logs.ts
|
|
1340
|
-
import * as
|
|
1340
|
+
import * as os2 from "os";
|
|
1341
|
+
import * as path10 from "path";
|
|
1341
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
|
|
1342
1858
|
function registerLogsCommand(program) {
|
|
1343
|
-
program.command("logs").description(
|
|
1859
|
+
const logs = program.command("logs").description(
|
|
1344
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."
|
|
1345
|
-
).
|
|
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");
|
|
1346
1863
|
const { config, logger } = getRuntime();
|
|
1347
|
-
const file =
|
|
1864
|
+
const file = path10.resolve(opts.output);
|
|
1348
1865
|
const abort = new AbortController();
|
|
1349
1866
|
const stop = () => abort.abort();
|
|
1350
1867
|
process.once("SIGINT", stop);
|
|
@@ -1365,6 +1882,27 @@ function registerLogsCommand(program) {
|
|
|
1365
1882
|
process.stdout.write(`${result.file}
|
|
1366
1883
|
`);
|
|
1367
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
|
+
});
|
|
1368
1906
|
}
|
|
1369
1907
|
|
|
1370
1908
|
// src/commands/hdc.ts
|
|
@@ -1383,19 +1921,19 @@ function registerHdcCommand(program) {
|
|
|
1383
1921
|
`);
|
|
1384
1922
|
return;
|
|
1385
1923
|
}
|
|
1386
|
-
await new Promise((
|
|
1924
|
+
await new Promise((resolve9, reject) => {
|
|
1387
1925
|
const child = spawnCompat(hdc, hdcPassthroughArgv(args, opts.device), { stdio: "inherit" });
|
|
1388
1926
|
child.on("error", (err) => reject(new Error(`Failed to run ${hdc}: ${err.message}`)));
|
|
1389
1927
|
child.on("close", (code) => {
|
|
1390
1928
|
process.exitCode = code ?? 1;
|
|
1391
|
-
|
|
1929
|
+
resolve9();
|
|
1392
1930
|
});
|
|
1393
1931
|
});
|
|
1394
1932
|
});
|
|
1395
1933
|
}
|
|
1396
1934
|
|
|
1397
1935
|
// src/bin/oniro-app.ts
|
|
1398
|
-
var CLI_VERSION = "0.
|
|
1936
|
+
var CLI_VERSION = "0.15.0";
|
|
1399
1937
|
function buildProgram() {
|
|
1400
1938
|
const program = new Command4().name("oniro-app").description(
|
|
1401
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."
|