Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b5171b8
feat: windows runtime support
triniwiz May 11, 2026
ba29271
fix(windows): syncing
triniwiz May 12, 2026
a2a6041
fix: missing import
triniwiz May 12, 2026
2fc8ca7
chore(windows): polish
triniwiz May 14, 2026
3d97912
fix: windows support
triniwiz May 16, 2026
5bbe284
fix(windows): logging
triniwiz May 17, 2026
1ee87fc
fix(windows): plugins & .net support
triniwiz May 21, 2026
dbff762
chore: improve windows app configs
triniwiz May 24, 2026
77609c0
fix: missing zeroGuid
triniwiz May 28, 2026
6c26545
chore: window.0
triniwiz May 28, 2026
b2a6cb5
fix(windows): test
triniwiz May 28, 2026
32f33ff
Merge branch 'main' into feat/windows
triniwiz May 29, 2026
ae5c765
feat(windows): types generation
triniwiz Jun 1, 2026
c9d9c4f
fix(windows): logs
triniwiz Jun 4, 2026
8f03ee4
fix(windows): buildOutputPath
triniwiz Jun 10, 2026
bc517c0
feat(doctor): add windows support
triniwiz Jun 13, 2026
f73ecf7
fix: fs rename add retry with backoff
triniwiz Jun 14, 2026
c702308
fix(windows): sync lock
triniwiz Jun 14, 2026
1dbb30b
fix(windows): fileSync
triniwiz Jun 14, 2026
7a54c25
fix(windows): devtools auto-start
triniwiz Jun 16, 2026
95cc0ed
feat(windows): release
triniwiz Jun 26, 2026
c7f8432
chore: windows help
triniwiz Jun 26, 2026
e5ef0ac
fix(windows): avoid EPERM rename error during platform project creation
dyazincahya Jun 29, 2026
ea72c74
Merge pull request #6066 from dyazincahya/feat/windows
triniwiz Jul 12, 2026
0f05f01
fix(windows): framework name
triniwiz Jul 30, 2026
2053117
feat(windows): include napi packages
triniwiz Jul 30, 2026
c80a64b
feat(windows): encrypt bundle
triniwiz Aug 5, 2026
6286e82
Merge remote-tracking branch 'origin/main' into feat/windows
triniwiz Sep 23, 2026
eecb511
feat(windows): vite HMR livesync, comment cleanup
triniwiz Sep 24, 2026
a299797
fix(windows): drop dead dotnet-bridge marker check
triniwiz Sep 29, 2026
86dd40c
feat(windows): print the DevTools URL for ns debug windows
triniwiz Sep 30, 2026
e9be2f0
fix(windows): honor --no-auto-guid
triniwiz Sep 30, 2026
69f2c1f
fix(doctor): check .NET SDK 10+ and Developer Mode on Windows
triniwiz Sep 30, 2026
3c839a5
docs(windows): fix inspector port, prerequisites and build output
triniwiz Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(windows): logging
  • Loading branch information
triniwiz committed May 17, 2026
commit 5bbe284ceda16b5094922971c0cf7eab4bebc438
83 changes: 58 additions & 25 deletions lib/common/mobile/windows/windows-application-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
$hooksService: IHooksService,
$deviceLogProvider: Mobile.IDeviceLogProvider,
private $childProcess: IChildProcess,
private _restartLogStream?: () => Promise<void>,
) {
super($logger, $hooksService, $deviceLogProvider);
}
Expand Down Expand Up @@ -144,43 +145,67 @@ export class WindowsApplicationManager extends ApplicationManagerBase {

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

/**
* Returns the path of the C# crash/exception log written by CrashDiagnostics.
* Lives in LocalState (persistent app data).
* Returns null when no PFN is known (e.g. unpackaged EXE targets).
*/
public getCrashLogPath(): string | null {
if (this._packageFamilyNames.size > 0) {
const pfn = this._packageFamilyNames.values().next().value as string;
const localAppData = process.env.LOCALAPPDATA;
if (localAppData && pfn) {
return path.join(localAppData, "Packages", pfn, "LocalState", "nativescript-crash.log");
}
}
return null;
}

public async startApplication(
appData: Mobile.IStartApplicationData,
): Promise<void> {
// Truncate the trace log so the streamer starts from a clean state each run.
try {
const logPath = this.getLogFilePath();
fs.writeFileSync(logPath, "", "utf8");
} catch { /* ignore — log dir may not exist yet */ }

const exeCandidate =
(appData.appId && this._installedExePaths[appData.appId]) ||
(appData.projectName && this._installedExePaths[appData.projectName]);
const isExe = !!(exeCandidate && fs.existsSync(exeCandidate));

// For UWP, pre-populate the PFN cache before calling getLogFilePath() so
// that the truncation and the subsequent log stream restart both target the
// correct container TempState path instead of the system temp fallback.
if (!isExe) {
await this._resolvePackageFamilyName(appData.appId);
}

// Truncate the trace log so the streamer starts from a clean state each run.
try {
fs.writeFileSync(this.getLogFilePath(), "", "utf8");
} catch { /* ignore — log dir may not exist yet */ }

if (exeCandidate && fs.existsSync(exeCandidate)) {
if (isExe) {
if (appData.waitForDebugger) {
this.$logger.info(
`[Windows] --debug-brk is not supported for EXE targets.`,
);
}
this.$logger.info(`[Windows] Launching EXE: ${exeCandidate}`);
const proc = spawn(exeCandidate, [], { detached: true, stdio: "ignore" });
const proc = spawn(exeCandidate as string, [], { detached: true, stdio: "ignore" });
proc.unref();
this._runningPid = proc.pid ?? null;
// Clear stale PID when the process exits so stopApplication falls back to
Expand All @@ -190,22 +215,30 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
this._runningPid = null;
}
});
return;
} else {
// PFN already cached from the pre-resolve above; no extra round-trip.
const pfn = this._packageFamilyNames.get(appData.appId) ?? appData.appId;
if (appData.waitForDebugger) {
this._writeDebugBreakMarker(pfn);
}
// UWP apps are launched via shell:AppsFolder\<PFN>!<ApplicationId>.
// The ApplicationId comes from the <Application Id="..."> attribute in the manifest.
const appId = "App";
this.$logger.info(`[Windows] Launching UWP: ${pfn}!${appId}`);
const proc = spawn("explorer.exe", [`shell:AppsFolder\\${pfn}!${appId}`], {
detached: true,
stdio: "ignore",
});
proc.unref();
}

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

public async stopApplication(
Expand Down
58 changes: 54 additions & 4 deletions lib/common/mobile/windows/windows-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class WindowsDevice implements Mobile.IDevice {
};

private _logTailInterval: ReturnType<typeof setInterval> | null = null;
private _crashLogTailInterval: ReturnType<typeof setInterval> | null = null;

constructor(
$logger: ILogger,
Expand All @@ -39,6 +40,7 @@ export class WindowsDevice implements Mobile.IDevice {
$hooksService,
$deviceLogProvider,
$childProcess,
() => this.openDeviceLogStream(),
);
this.fileSystem = new WindowsDeviceFileSystem();
}
Expand All @@ -48,21 +50,40 @@ export class WindowsDevice implements Mobile.IDevice {
clearInterval(this._logTailInterval);
this._logTailInterval = null;
}
if (this._crashLogTailInterval) {
clearInterval(this._crashLogTailInterval);
this._crashLogTailInterval = null;
}

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

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

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

// Internal Rust runtime diagnostics written via debug_output() — useful in
// VS Output / DebugView but noisy in the CLI. Suppress them; errors/exceptions
// are kept because their prefix contains "error", "exception", or "PANIC".
const INTERNAL_PREFIXES = [
"[NativeScript] init_console:",
"[NativeScript] log file:",
"[NativeScript] delegate ctor:",
];
const isInternalTrace = (line: string) =>
INTERNAL_PREFIXES.some((p) => line.startsWith(p));

this._logTailInterval = setInterval(() => {
try {
const stat = fs.statSync(logPath);
Expand All @@ -77,7 +98,7 @@ export class WindowsDevice implements Mobile.IDevice {

const lines = buf.toString("utf8").split(/\r?\n/);
for (const line of lines) {
if (line.trim()) {
if (line.trim() && !isInternalTrace(line)) {
this.$deviceLogProvider.logData(line, "Windows", deviceId);
}
}
Expand All @@ -86,6 +107,35 @@ export class WindowsDevice implements Mobile.IDevice {
try { fs.writeFileSync(logPath, "", "utf8"); offset = 0; } catch { /* ignore */ }
}
} catch { /* ignore — file may not exist between app restarts */ }
}, 250);
}, 50);

// Also tail nativescript-crash.log from LocalState so C# exception reports
// and JS errors caught by the host surface in the CLI — same as Android/iOS
// crash log streaming. Truncate on each run so only errors from this session appear.
if (crashLogPath) {
try { fs.writeFileSync(crashLogPath, "", "utf8"); } catch { /* may not exist yet */ }
let crashOffset = 0;

this._crashLogTailInterval = setInterval(() => {
try {
const stat = fs.statSync(crashLogPath);
if (stat.size <= crashOffset) return;

const toRead = stat.size - crashOffset;
const buf = Buffer.alloc(toRead);
const fd = fs.openSync(crashLogPath, "r");
fs.readSync(fd, buf, 0, toRead, crashOffset);
fs.closeSync(fd);
crashOffset = stat.size;

const lines = buf.toString("utf8").split(/\r?\n/);
for (const line of lines) {
if (line.trim()) {
this.$deviceLogProvider.logData(`[crash] ${line}`, "Windows", deviceId);
}
}
} catch { /* ignore — file may not exist until first crash */ }
}, 50);
Comment on lines +87 to +138

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

File descriptor leak: closeSync is not in a finally.

In both the trace tailer (Lines 94-96) and the crash tailer (Lines 126-128), if fs.readSync throws, fs.closeSync(fd) is skipped and the outer catch swallows the error. Because these run on a 50 ms interval, repeated read failures leak descriptors and can eventually exhaust the process fd table.

🔒 Proposed fix (apply to both tailers)
-				const fd = fs.openSync(logPath, "r");
-				fs.readSync(fd, buf, 0, toRead, offset);
-				fs.closeSync(fd);
-				offset = stat.size;
+				const fd = fs.openSync(logPath, "r");
+				try {
+					fs.readSync(fd, buf, 0, toRead, offset);
+				} finally {
+					fs.closeSync(fd);
+				}
+				offset = stat.size;
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 106-106: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(logPath, "", "utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)


[warning] 115-115: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(crashLogPath, "", "utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/common/mobile/windows/windows-device.ts` around lines 87 - 138, The
interval-based tailers in windows-device.ts can leak file descriptors because
`fs.closeSync(fd)` is only reached after `fs.readSync` succeeds. Update both the
main log tailing block and the crash log tailing block so the open/read/close
sequence is wrapped with a `finally` (or equivalent guaranteed cleanup) inside
the `setInterval` callbacks, ensuring `closeSync` always runs even when
`statSync` or `readSync` throws.

}
}
}
2 changes: 1 addition & 1 deletion lib/services/project-cleanup-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export class ProjectCleanupService implements IProjectCleanupService {
// Collect exe names from the directory that might be running.
const exeNames: string[] = [];
try {
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true, recursive: true } as any)) {
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true, recursive: true } as any) as unknown as fs.Dirent[]) {
if (!entry.isFile()) continue;
const name: string = (entry as any).name ?? "";
if (name.toLowerCase().endsWith(".exe")) {
Expand Down