diff --git a/docs/man_pages/project/testing/run-ios.md b/docs/man_pages/project/testing/run-ios.md index 43259fe40f..c808c8f0e0 100644 --- a/docs/man_pages/project/testing/run-ios.md +++ b/docs/man_pages/project/testing/run-ios.md @@ -33,7 +33,7 @@ Start an emulator with specified device identifier and sdk | `$ ns run ios [--de * `--emulator` - If set, runs the app in all available and configured ios simulators. It will start a simulator if none are already running. * `--sdk` - Specifies the target simulator's sdk. * `--justlaunch` - If set, does not print the application output in the console. -* `--log ` - Also controls how much of the application output is printed. By default only its JavaScript output is shown. `debug` adds everything else the app logs - its native code and the system frameworks it uses, like Xcode's console - and `trace` adds debug level entries as well. +* `--log ` - Also controls how much of the application output is printed. By default only its JavaScript output is shown. `debug` adds everything else the app logs - its native code, `print()` output and the system frameworks it uses, like Xcode's console - and `trace` adds debug level entries as well. * `--clean` - If set, forces the complete rebuild of the native application. * `--no-watch` - If set, changes in your code will not be reflected during the execution of this command. * `--release` - If set, produces a release build by running webpack in production mode and native build in release mode. Otherwise, produces a debug build. diff --git a/lib/common/definitions/mobile.d.ts b/lib/common/definitions/mobile.d.ts index 32a4bb7f4c..12e8de36a6 100644 --- a/lib/common/definitions/mobile.d.ts +++ b/lib/common/definitions/mobile.d.ts @@ -173,6 +173,8 @@ declare global { image: string; level: string; message: string; + subsystem?: string; + category?: string; } interface IiOSLogStreamOptions { diff --git a/lib/common/mobile/ios/ios-log-entry.ts b/lib/common/mobile/ios/ios-log-entry.ts index 0d1121fe52..7c1ff130e8 100644 --- a/lib/common/mobile/ios/ios-log-entry.ts +++ b/lib/common/mobile/ios/ios-log-entry.ts @@ -38,6 +38,10 @@ export function isIOSRuntimeLogEntry(entry: Mobile.IiOSLogEntry): boolean { return path.basename(entry.image || "") === "NativeScript"; } +// The runtime forwards the app's stdout and stderr under this subsystem, when +// the CLI asks for it through the forwardStdio key of the app's package.json. +const STDIO_SUBSYSTEM = "org.nativescript.stdio"; + // Logged by the runtime itself on every launch, not by the app. const RUNTIME_NOISE = ["NativeScript loaded bundle"]; @@ -57,6 +61,10 @@ export function getIOSLogEntryText( return null; } + if (entry.subsystem === STDIO_SUBSYSTEM) { + return options.includeNative ? `${entry.category}: ${entry.message}` : null; + } + const imageName = path.basename(entry.image || ""); if (isIOSRuntimeLogEntry(entry)) { return RUNTIME_NOISE.some((noise) => entry.message.includes(noise)) @@ -78,7 +86,7 @@ export function getIOSLogEntryText( // asks for it, through the logChunkMarkers key of the app's package.json. const CHUNK_JOIN_MARKER = "\u2060"; const CHUNK_LINE_MARKER = "\u2061"; -const CONSOLE_PREFIX = /^CONSOLE [A-Z]+: /; +const REPEATED_PREFIX = /^(?:CONSOLE [A-Z]+|stdout|stderr): /; // Long enough to ride out a backlog of chunks, while the rest of a message that // never arrives, because the app crashed, still gets printed. @@ -109,7 +117,7 @@ export class IOSLogChunkStitcher { const message = this.pending.get(key) ?? { text: "", - prefix: chunk.match(CONSOLE_PREFIX)?.[0] ?? "", + prefix: chunk.match(REPEATED_PREFIX)?.[0] ?? "", separator: "", timer: null, }; diff --git a/lib/common/mobile/ios/simulator/ios-simulator-log-provider.ts b/lib/common/mobile/ios/simulator/ios-simulator-log-provider.ts index e560adf789..cb1e1e48f0 100644 --- a/lib/common/mobile/ios/simulator/ios-simulator-log-provider.ts +++ b/lib/common/mobile/ios/simulator/ios-simulator-log-provider.ts @@ -143,6 +143,8 @@ export class IOSSimulatorLogProvider image: event.senderImagePath, level: (event.messageType || "").toLowerCase(), message: event.eventMessage, + subsystem: event.subsystem, + category: event.category, }, this.$devicePlatformsConstants.iOS, deviceId, diff --git a/lib/common/test/unit-tests/mobile/ios-log-entry.ts b/lib/common/test/unit-tests/mobile/ios-log-entry.ts index e8c8cc4a2e..febf7271e0 100644 --- a/lib/common/test/unit-tests/mobile/ios-log-entry.ts +++ b/lib/common/test/unit-tests/mobile/ios-log-entry.ts @@ -98,6 +98,31 @@ describe("getIOSLogEntryText", () => { }); }); +describe("getIOSLogEntryText for forwarded stdio", () => { + const entry = { + process: "/var/containers/Bundle/Application/X/app.app/app", + image: "/private/Frameworks/NativeScript.framework/NativeScript", + level: "default", + message: "printed", + subsystem: "org.nativescript.stdio", + category: "stdout", + }; + + it("is hidden by default, unlike the runtime's console output", () => { + assert.isNull(getIOSLogEntryText(entry, { processName: "app" })); + }); + + it("is labeled with its stream along with native output", () => { + assert.equal( + getIOSLogEntryText( + { ...entry, category: "stderr" }, + { processName: "app", includeNative: true }, + ), + "stderr: printed", + ); + }); +}); + describe("getIOSLogScope", () => { it("maps the CLI log level to the app logs that are shown", () => { assert.deepEqual(getIOSLogScope("INFO", false), { @@ -159,6 +184,11 @@ describe("IOSLogChunkStitcher", () => { }); it("restores the line break a chunk was cut at", () => { + add(`stdout: one${JOIN}`); + add("stdout: two"); + assert.deepEqual(messages, ["stdout: onetwo"]); + messages.length = 0; + add(`CONSOLE ERROR: first${LINE}`); add("CONSOLE ERROR: second"); assert.deepEqual(messages, ["CONSOLE ERROR: first\nsecond"]); diff --git a/lib/controllers/prepare-controller.ts b/lib/controllers/prepare-controller.ts index f70795f49b..dcee53d1f4 100644 --- a/lib/controllers/prepare-controller.ts +++ b/lib/controllers/prepare-controller.ts @@ -526,6 +526,7 @@ export class PrepareController } if (prepareData && !prepareData.release) { packageData.logChunkMarkers = true; + packageData.forwardStdio = true; } if (packageData.ios && packageData.ios.discardUncaughtJsExceptions) { packageData.discardUncaughtJsExceptions = diff --git a/test/controllers/prepare-controller.ts b/test/controllers/prepare-controller.ts index 336db5e766..19ed116e4b 100644 --- a/test/controllers/prepare-controller.ts +++ b/test/controllers/prepare-controller.ts @@ -181,6 +181,8 @@ describe("prepareController", () => { it("asks for log chunk markers in iOS debug builds only", async () => { assert.isTrue((await writePackageJson("ios", false)).logChunkMarkers); assert.isUndefined((await writePackageJson("ios", true)).logChunkMarkers); + assert.isTrue((await writePackageJson("ios", false)).forwardStdio); + assert.isUndefined((await writePackageJson("ios", true)).forwardStdio); assert.isUndefined( (await writePackageJson("android", false)).logChunkMarkers, );