Skip to content

Commit 5bbe284

Browse files
committed
fix(windows): logging
1 parent 3d97912 commit 5bbe284

3 files changed

Lines changed: 113 additions & 30 deletions

File tree

lib/common/mobile/windows/windows-application-manager.ts

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
1919
$hooksService: IHooksService,
2020
$deviceLogProvider: Mobile.IDeviceLogProvider,
2121
private $childProcess: IChildProcess,
22+
private _restartLogStream?: () => Promise<void>,
2223
) {
2324
super($logger, $hooksService, $deviceLogProvider);
2425
}
@@ -144,43 +145,67 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
144145

145146
/**
146147
* Returns the path of the runtime's trace log for the most recently started app.
147-
* For UWP-packaged apps the runtime DLL writes inside the app container's TempState,
148-
* not the global system temp directory. Falls back to the system temp path used when
149-
* the app is launched as an unpackaged EXE.
148+
* Inside a UWP process, Win32 GetTempPathW() virtualises to AC\Temp (the app
149+
* container's isolated temp folder) — NOT the WinRT TemporaryFolder (TempState).
150+
* Rust's std::env::temp_dir() calls GetTempPathW(), so the DLL writes to AC\Temp.
151+
* Falls back to the system temp path when no PFN is known (unpackaged EXE).
150152
*/
151153
public getLogFilePath(): string {
152154
const systemTempLog = path.join(os.tmpdir(), "ns_trace.log");
153155
if (this._packageFamilyNames.size > 0) {
154156
const pfn = this._packageFamilyNames.values().next().value as string;
155157
const localAppData = process.env.LOCALAPPDATA;
156158
if (localAppData && pfn) {
157-
return path.join(localAppData, "Packages", pfn, "TempState", "ns_trace.log");
159+
return path.join(localAppData, "Packages", pfn, "AC", "Temp", "ns_trace.log");
158160
}
159161
}
160162
return systemTempLog;
161163
}
162164

165+
/**
166+
* Returns the path of the C# crash/exception log written by CrashDiagnostics.
167+
* Lives in LocalState (persistent app data).
168+
* Returns null when no PFN is known (e.g. unpackaged EXE targets).
169+
*/
170+
public getCrashLogPath(): string | null {
171+
if (this._packageFamilyNames.size > 0) {
172+
const pfn = this._packageFamilyNames.values().next().value as string;
173+
const localAppData = process.env.LOCALAPPDATA;
174+
if (localAppData && pfn) {
175+
return path.join(localAppData, "Packages", pfn, "LocalState", "nativescript-crash.log");
176+
}
177+
}
178+
return null;
179+
}
180+
163181
public async startApplication(
164182
appData: Mobile.IStartApplicationData,
165183
): Promise<void> {
166-
// Truncate the trace log so the streamer starts from a clean state each run.
167-
try {
168-
const logPath = this.getLogFilePath();
169-
fs.writeFileSync(logPath, "", "utf8");
170-
} catch { /* ignore — log dir may not exist yet */ }
171-
172184
const exeCandidate =
173185
(appData.appId && this._installedExePaths[appData.appId]) ||
174186
(appData.projectName && this._installedExePaths[appData.projectName]);
187+
const isExe = !!(exeCandidate && fs.existsSync(exeCandidate));
188+
189+
// For UWP, pre-populate the PFN cache before calling getLogFilePath() so
190+
// that the truncation and the subsequent log stream restart both target the
191+
// correct container TempState path instead of the system temp fallback.
192+
if (!isExe) {
193+
await this._resolvePackageFamilyName(appData.appId);
194+
}
195+
196+
// Truncate the trace log so the streamer starts from a clean state each run.
197+
try {
198+
fs.writeFileSync(this.getLogFilePath(), "", "utf8");
199+
} catch { /* ignore — log dir may not exist yet */ }
175200

176-
if (exeCandidate && fs.existsSync(exeCandidate)) {
201+
if (isExe) {
177202
if (appData.waitForDebugger) {
178203
this.$logger.info(
179204
`[Windows] --debug-brk is not supported for EXE targets.`,
180205
);
181206
}
182207
this.$logger.info(`[Windows] Launching EXE: ${exeCandidate}`);
183-
const proc = spawn(exeCandidate, [], { detached: true, stdio: "ignore" });
208+
const proc = spawn(exeCandidate as string, [], { detached: true, stdio: "ignore" });
184209
proc.unref();
185210
this._runningPid = proc.pid ?? null;
186211
// Clear stale PID when the process exits so stopApplication falls back to
@@ -190,22 +215,30 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
190215
this._runningPid = null;
191216
}
192217
});
193-
return;
218+
} else {
219+
// PFN already cached from the pre-resolve above; no extra round-trip.
220+
const pfn = this._packageFamilyNames.get(appData.appId) ?? appData.appId;
221+
if (appData.waitForDebugger) {
222+
this._writeDebugBreakMarker(pfn);
223+
}
224+
// UWP apps are launched via shell:AppsFolder\<PFN>!<ApplicationId>.
225+
// The ApplicationId comes from the <Application Id="..."> attribute in the manifest.
226+
const appId = "App";
227+
this.$logger.info(`[Windows] Launching UWP: ${pfn}!${appId}`);
228+
const proc = spawn("explorer.exe", [`shell:AppsFolder\\${pfn}!${appId}`], {
229+
detached: true,
230+
stdio: "ignore",
231+
});
232+
proc.unref();
194233
}
195234

196-
const pfn = await this._resolvePackageFamilyName(appData.appId);
197-
if (appData.waitForDebugger) {
198-
this._writeDebugBreakMarker(pfn);
235+
// Restart the log stream so the tailer picks up the correct path (UWP
236+
// container TempState vs. system temp) and resets its offset to 0. Without
237+
// this the tailer keeps the stale offset from device-discovery time and
238+
// skips every log line written after the file was truncated above.
239+
if (this._restartLogStream) {
240+
await this._restartLogStream();
199241
}
200-
// UWP apps are launched via shell:AppsFolder\<PFN>!<ApplicationId>.
201-
// The ApplicationId comes from the <Application Id="..."> attribute in the manifest.
202-
const appId = "App";
203-
this.$logger.info(`[Windows] Launching UWP: ${pfn}!${appId}`);
204-
const proc = spawn("explorer.exe", [`shell:AppsFolder\\${pfn}!${appId}`], {
205-
detached: true,
206-
stdio: "ignore",
207-
});
208-
proc.unref();
209242
}
210243

211244
public async stopApplication(

lib/common/mobile/windows/windows-device.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class WindowsDevice implements Mobile.IDevice {
2727
};
2828

2929
private _logTailInterval: ReturnType<typeof setInterval> | null = null;
30+
private _crashLogTailInterval: ReturnType<typeof setInterval> | null = null;
3031

3132
constructor(
3233
$logger: ILogger,
@@ -39,6 +40,7 @@ export class WindowsDevice implements Mobile.IDevice {
3940
$hooksService,
4041
$deviceLogProvider,
4142
$childProcess,
43+
() => this.openDeviceLogStream(),
4244
);
4345
this.fileSystem = new WindowsDeviceFileSystem();
4446
}
@@ -48,21 +50,40 @@ export class WindowsDevice implements Mobile.IDevice {
4850
clearInterval(this._logTailInterval);
4951
this._logTailInterval = null;
5052
}
53+
if (this._crashLogTailInterval) {
54+
clearInterval(this._crashLogTailInterval);
55+
this._crashLogTailInterval = null;
56+
}
5157

5258
// For packaged UWP apps, GetTempPath() inside the app container resolves to
5359
// %LOCALAPPDATA%\Packages\<PFN>\TempState — not the system temp dir.
5460
// Ask the application manager for the correct path based on the known PFN.
5561
const manager = this.applicationManager as WindowsApplicationManager;
5662
const logPath = manager.getLogFilePath();
63+
const crashLogPath = manager.getCrashLogPath();
5764
const deviceId = this.deviceInfo.identifier;
5865

59-
// Start from the current end of the file so stale output is not replayed.
66+
// startApplication() truncates the trace log before calling this method, so the
67+
// file is either empty (size 0) or absent. Start from 0 to capture all
68+
// output from the new process. The initial call from DeviceEmitter (before
69+
// any app starts) also starts at 0; the truncation in startApplication()
70+
// is what prevents stale output from being replayed.
6071
let offset = 0;
61-
try { offset = fs.statSync(logPath).size; } catch { /* file not yet created */ }
6272

6373
// Rotate the log if it exceeds 10 MB to prevent unbounded disk growth.
6474
const MAX_LOG_BYTES = 10 * 1024 * 1024;
6575

76+
// Internal Rust runtime diagnostics written via debug_output() — useful in
77+
// VS Output / DebugView but noisy in the CLI. Suppress them; errors/exceptions
78+
// are kept because their prefix contains "error", "exception", or "PANIC".
79+
const INTERNAL_PREFIXES = [
80+
"[NativeScript] init_console:",
81+
"[NativeScript] log file:",
82+
"[NativeScript] delegate ctor:",
83+
];
84+
const isInternalTrace = (line: string) =>
85+
INTERNAL_PREFIXES.some((p) => line.startsWith(p));
86+
6687
this._logTailInterval = setInterval(() => {
6788
try {
6889
const stat = fs.statSync(logPath);
@@ -77,7 +98,7 @@ export class WindowsDevice implements Mobile.IDevice {
7798

7899
const lines = buf.toString("utf8").split(/\r?\n/);
79100
for (const line of lines) {
80-
if (line.trim()) {
101+
if (line.trim() && !isInternalTrace(line)) {
81102
this.$deviceLogProvider.logData(line, "Windows", deviceId);
82103
}
83104
}
@@ -86,6 +107,35 @@ export class WindowsDevice implements Mobile.IDevice {
86107
try { fs.writeFileSync(logPath, "", "utf8"); offset = 0; } catch { /* ignore */ }
87108
}
88109
} catch { /* ignore — file may not exist between app restarts */ }
89-
}, 250);
110+
}, 50);
111+
112+
// Also tail nativescript-crash.log from LocalState so C# exception reports
113+
// and JS errors caught by the host surface in the CLI — same as Android/iOS
114+
// crash log streaming. Truncate on each run so only errors from this session appear.
115+
if (crashLogPath) {
116+
try { fs.writeFileSync(crashLogPath, "", "utf8"); } catch { /* may not exist yet */ }
117+
let crashOffset = 0;
118+
119+
this._crashLogTailInterval = setInterval(() => {
120+
try {
121+
const stat = fs.statSync(crashLogPath);
122+
if (stat.size <= crashOffset) return;
123+
124+
const toRead = stat.size - crashOffset;
125+
const buf = Buffer.alloc(toRead);
126+
const fd = fs.openSync(crashLogPath, "r");
127+
fs.readSync(fd, buf, 0, toRead, crashOffset);
128+
fs.closeSync(fd);
129+
crashOffset = stat.size;
130+
131+
const lines = buf.toString("utf8").split(/\r?\n/);
132+
for (const line of lines) {
133+
if (line.trim()) {
134+
this.$deviceLogProvider.logData(`[crash] ${line}`, "Windows", deviceId);
135+
}
136+
}
137+
} catch { /* ignore — file may not exist until first crash */ }
138+
}, 50);
139+
}
90140
}
91141
}

lib/services/project-cleanup-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export class ProjectCleanupService implements IProjectCleanupService {
147147
// Collect exe names from the directory that might be running.
148148
const exeNames: string[] = [];
149149
try {
150-
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true, recursive: true } as any)) {
150+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true, recursive: true } as any) as unknown as fs.Dirent[]) {
151151
if (!entry.isFile()) continue;
152152
const name: string = (entry as any).name ?? "";
153153
if (name.toLowerCase().endsWith(".exe")) {

0 commit comments

Comments
 (0)