Skip to content

Commit 2fc8ca7

Browse files
committed
chore(windows): polish
1 parent a2a6041 commit 2fc8ca7

3 files changed

Lines changed: 153 additions & 74 deletions

File tree

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

Lines changed: 129 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import * as fs from "fs";
22
import * as path from "path";
3+
import { spawn } from "child_process";
34
import { ApplicationManagerBase } from "../application-manager-base";
45
import { IHooksService, IChildProcess, IDictionary } from "../../declarations";
56
import { IBuildData } from "../../../definitions/build";
67

78
export class WindowsApplicationManager extends ApplicationManagerBase {
8-
private _runningPid: number = null;
9-
private _packageFamilyName: string = null;
9+
private _runningPid: number | null = null;
10+
// Keyed by appId so multiple UWP apps don't stomp each other's cached PFN.
11+
private _packageFamilyNames: Map<string, string> = new Map();
12+
// Populated by installApplication for .exe builds; keyed by both appIdentifier
13+
// and exe-basename so the two lookup paths in startApplication both work.
14+
private _installedExePaths: IDictionary<string> = {};
1015

1116
constructor(
1217
$logger: ILogger,
@@ -39,12 +44,43 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
3944
}
4045
}
4146

47+
// The base class checks getInstalledApplications(), which only knows about UWP
48+
// packages. Override so that EXE-based apps registered via installApplication()
49+
// are also considered "installed" without a PowerShell round-trip.
50+
public async isApplicationInstalled(appIdentifier: string): Promise<boolean> {
51+
if (
52+
appIdentifier &&
53+
Object.prototype.hasOwnProperty.call(
54+
this._installedExePaths,
55+
appIdentifier,
56+
)
57+
) {
58+
return true;
59+
}
60+
return super.isApplicationInstalled(appIdentifier);
61+
}
62+
4263
public async installApplication(
4364
packageFilePath: string,
4465
appIdentifier?: string,
4566
_buildData?: IBuildData,
4667
): Promise<void> {
47-
this.$logger.info(`[Windows] Installing from: ${packageFilePath}`);
68+
if (packageFilePath?.toLowerCase().endsWith(".exe")) {
69+
this.$logger.info(`[Windows] Registering EXE: ${packageFilePath}`);
70+
const exeBase = path.basename(
71+
packageFilePath,
72+
path.extname(packageFilePath),
73+
);
74+
if (appIdentifier) {
75+
this._installedExePaths[appIdentifier] = packageFilePath;
76+
}
77+
// Secondary key so the projectName-based lookup in startApplication works
78+
// even when appIdentifier differs from the exe filename.
79+
this._installedExePaths[exeBase] = packageFilePath;
80+
return;
81+
}
82+
83+
this.$logger.info(`[Windows] Installing MSIX/APPX from: ${packageFilePath}`);
4884
await this.$childProcess.spawnFromEvent(
4985
"powershell.exe",
5086
[
@@ -58,13 +94,17 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
5894
{},
5995
{ throwError: true },
6096
);
61-
// Cache the PFN immediately after install so startApplication can use it.
97+
// Pre-warm the PFN cache so startApplication does not need to resolve it.
6298
if (appIdentifier) {
6399
await this._resolvePackageFamilyName(appIdentifier);
64100
}
65101
}
66102

67103
public async uninstallApplication(appIdentifier: string): Promise<void> {
104+
// Clean up EXE registration so isApplicationInstalled returns false.
105+
delete this._installedExePaths[appIdentifier];
106+
this._packageFamilyNames.delete(appIdentifier);
107+
68108
await this.$childProcess.spawnFromEvent(
69109
"powershell.exe",
70110
[
@@ -76,78 +116,65 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
76116
{},
77117
{ throwError: false },
78118
);
79-
this._packageFamilyName = null;
119+
}
120+
121+
// Explicit override matching the interface contract (IStartApplicationData)
122+
// rather than relying on the base-class forwarding appData typed as the
123+
// narrower IApplicationData, which would silently drop waitForDebugger.
124+
public async restartApplication(
125+
appData: Mobile.IStartApplicationData,
126+
): Promise<void> {
127+
await this.stopApplication(appData);
128+
await this.startApplication(appData);
80129
}
81130

82131
public async startApplication(
83132
appData: Mobile.IStartApplicationData,
84133
): Promise<void> {
85-
const pfn = await this._resolvePackageFamilyName(appData.appId);
134+
const exeCandidate =
135+
(appData.appId && this._installedExePaths[appData.appId]) ||
136+
(appData.projectName && this._installedExePaths[appData.projectName]);
137+
138+
if (exeCandidate && fs.existsSync(exeCandidate)) {
139+
if (appData.waitForDebugger) {
140+
this.$logger.info(
141+
`[Windows] --debug-brk is not supported for EXE targets.`,
142+
);
143+
}
144+
this.$logger.info(`[Windows] Launching EXE: ${exeCandidate}`);
145+
const proc = spawn(exeCandidate, [], { detached: true, stdio: "ignore" });
146+
proc.unref();
147+
this._runningPid = proc.pid ?? null;
148+
// Clear stale PID when the process exits so stopApplication falls back to
149+
// the Stop-Process path on the next restart instead of killing a reused PID.
150+
proc.on("exit", () => {
151+
if (this._runningPid === proc.pid) {
152+
this._runningPid = null;
153+
}
154+
});
155+
return;
156+
}
86157

87-
// Mirror the Android sentinel-file pattern: write ns-debugbreak to the
88-
// app's LocalFolder before launch so the runtime knows to open DevTools.
158+
const pfn = await this._resolvePackageFamilyName(appData.appId);
89159
if (appData.waitForDebugger) {
90160
this._writeDebugBreakMarker(pfn);
91161
}
92-
93-
this.$logger.info(`[Windows] Launching: ${pfn}`);
94-
const proc = require("child_process").spawn(
95-
"explorer.exe",
96-
[`ms-windows-app://${pfn}`],
97-
{ detached: true, stdio: "ignore" },
98-
);
162+
this.$logger.info(`[Windows] Launching UWP: ${pfn}`);
163+
const proc = spawn("explorer.exe", [`ms-windows-app://${pfn}`], {
164+
detached: true,
165+
stdio: "ignore",
166+
});
99167
proc.unref();
100168
}
101169

102-
private async _resolvePackageFamilyName(appId: string): Promise<string> {
103-
if (this._packageFamilyName) return this._packageFamilyName;
104-
try {
105-
const result = await this.$childProcess.spawnFromEvent(
106-
"powershell.exe",
107-
[
108-
"-NoProfile",
109-
"-Command",
110-
`(Get-AppxPackage | Where-Object { $_.Name -eq "${appId}" -or $_.PackageFullName -like "*${appId}*" } | Select-Object -First 1).PackageFamilyName`,
111-
],
112-
"close",
113-
{},
114-
{ throwError: false },
115-
);
116-
const pfn = (result.stdout || "").trim();
117-
if (pfn) this._packageFamilyName = pfn;
118-
} catch {
119-
/* ignore, fall back to appId */
120-
}
121-
return this._packageFamilyName ?? appId;
122-
}
123-
124-
private _writeDebugBreakMarker(pfn: string): void {
125-
const localAppData = process.env.LOCALAPPDATA;
126-
if (!localAppData) return;
127-
const markerPath = path.join(
128-
localAppData,
129-
"Packages",
130-
pfn,
131-
"LocalState",
132-
"ns-debugbreak",
133-
);
134-
try {
135-
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
136-
fs.writeFileSync(markerPath, "", "utf8");
137-
this.$logger.info(`[Windows] Debug break marker: ${markerPath}`);
138-
} catch (e) {
139-
this.$logger.warn(`[Windows] Could not write debug break marker: ${e}`);
140-
}
141-
}
142-
143170
public async stopApplication(
144171
appData: Mobile.IApplicationData,
145172
): Promise<void> {
146173
if (this._runningPid) {
147174
try {
148175
process.kill(this._runningPid);
149176
} catch {
150-
/* already dead */
177+
/* already gone */
151178
}
152179
this._runningPid = null;
153180
} else {
@@ -186,4 +213,49 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
186213
): Promise<IDictionary<Mobile.IDebugWebViewInfo[]>> {
187214
return {} as IDictionary<Mobile.IDebugWebViewInfo[]>;
188215
}
216+
217+
private async _resolvePackageFamilyName(appId: string): Promise<string> {
218+
const cached = this._packageFamilyNames.get(appId);
219+
if (cached) return cached;
220+
221+
try {
222+
const result = await this.$childProcess.spawnFromEvent(
223+
"powershell.exe",
224+
[
225+
"-NoProfile",
226+
"-Command",
227+
`(Get-AppxPackage | Where-Object { $_.Name -eq "${appId}" -or $_.PackageFullName -like "*${appId}*" } | Select-Object -First 1).PackageFamilyName`,
228+
],
229+
"close",
230+
{},
231+
{ throwError: false },
232+
);
233+
const pfn = (result.stdout || "").trim();
234+
if (pfn) {
235+
this._packageFamilyNames.set(appId, pfn);
236+
}
237+
} catch {
238+
/* fall back to appId as the protocol target */
239+
}
240+
return this._packageFamilyNames.get(appId) ?? appId;
241+
}
242+
243+
private _writeDebugBreakMarker(pfn: string): void {
244+
const localAppData = process.env.LOCALAPPDATA;
245+
if (!localAppData) return;
246+
const markerPath = path.join(
247+
localAppData,
248+
"Packages",
249+
pfn,
250+
"LocalState",
251+
"ns-debugbreak",
252+
);
253+
try {
254+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
255+
fs.writeFileSync(markerPath, "", "utf8");
256+
this.$logger.info(`[Windows] Debug break marker: ${markerPath}`);
257+
} catch (e) {
258+
this.$logger.warn(`[Windows] Could not write debug break marker: ${e}`);
259+
}
260+
}
189261
}

lib/services/livesync/windows-device-livesync-service.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as fs from "fs";
12
import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base";
23
import { IPlatformsDataService } from "../../definitions/platform";
34
import { IProjectData } from "../../definitions/project";
@@ -18,10 +19,19 @@ export class WindowsDeviceLiveSyncService
1819
projectData: IProjectData,
1920
_liveSyncInfo: ILiveSyncResultInfo,
2021
): Promise<void> {
21-
// TODO: kill the running Windows app process and relaunch it
2222
this.$logger.info(
23-
`[Windows LiveSync] Restart required for ${projectData.projectName}`,
23+
`[Windows LiveSync] Restarting application ${projectData.projectName}`,
2424
);
25+
26+
const appId =
27+
projectData.projectIdentifiers?.["windows"] ?? projectData.projectId;
28+
29+
await this.device.applicationManager.restartApplication({
30+
appId,
31+
projectName: projectData.projectName,
32+
projectDir: projectData.projectDir,
33+
waitForDebugger: _liveSyncInfo?.waitForDebugger,
34+
} as Mobile.IStartApplicationData);
2535
}
2636

2737
public async shouldRestart(
@@ -45,8 +55,8 @@ export class WindowsDeviceLiveSyncService
4555
): Promise<void> {
4656
for (const localToDevicePathData of localToDevicePaths) {
4757
const devicePath = localToDevicePathData.getDevicePath();
48-
if (require("fs").existsSync(devicePath)) {
49-
require("fs").unlinkSync(devicePath);
58+
if (fs.existsSync(devicePath)) {
59+
fs.unlinkSync(devicePath);
5060
}
5161
}
5262
}

lib/services/windows-project-service.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class WindowsProjectService
4242
super($fs, $projectDataService);
4343
}
4444

45-
private _platformData: IPlatformData = null;
45+
private _platformData: IPlatformData | null = null;
4646

4747
public getPlatformData(projectData: IProjectData): IPlatformData {
4848
if (!projectData && !this._platformData) {
@@ -86,6 +86,7 @@ export class WindowsProjectService
8686
packageNames: [
8787
`${projectData.projectName}.msix`,
8888
`${projectData.projectName}.appx`,
89+
`${projectData.projectName}.exe`,
8990
],
9091
};
9192
},
@@ -97,7 +98,7 @@ export class WindowsProjectService
9798
};
9899
}
99100

100-
return this._platformData;
101+
return this._platformData as IPlatformData;
101102
}
102103

103104
public async validateOptions(
@@ -434,12 +435,11 @@ export class WindowsProjectService
434435
}
435436

436437
public async preparePluginNativeCode(
437-
_pluginData: IPluginData,
438-
_options?: any,
438+
pluginData: IPluginData,
439+
projectData: IProjectData,
439440
): Promise<void> {
440-
const pluginData = _pluginData;
441-
442-
// stage native files found under plugin's platforms/windows folder into the app's plugins dir
441+
// Stage native files found under the plugin's platforms/windows folder into
442+
// the app's plugins directory so the csproj can import them.
443443
const platformFolder = path.join(
444444
pluginData.fullPath,
445445
"platforms",
@@ -452,13 +452,10 @@ export class WindowsProjectService
452452
? fallbackFolder
453453
: null;
454454
if (!sourcesFolder) {
455-
// Nothing to stage for this plugin
456455
return;
457456
}
458457

459-
const projectData = arguments.length >= 2 ? arguments[1] : null;
460458
if (!projectData) {
461-
// attempt to find a projectData by walking up (best-effort); otherwise skip
462459
return;
463460
}
464461

@@ -471,15 +468,15 @@ export class WindowsProjectService
471468
this.$fs.ensureDirectoryExists(pluginStageDir);
472469

473470
// recursively copy native files (exclude JS/TS/JSON)
474-
const walk = (dir: string, out: string) => {
471+
const walk = (dir: string) => {
475472
const entries = fs.readdirSync(dir, { withFileTypes: true });
476473
for (const e of entries) {
477474
const src = path.join(dir, e.name);
478475
const rel = path.relative(sourcesFolder, src);
479476
const dest = path.join(pluginStageDir, rel);
480477
if (e.isDirectory()) {
481478
this.$fs.ensureDirectoryExists(dest);
482-
walk(src, dest);
479+
walk(src);
483480
} else if (e.isFile()) {
484481
const ext = path.extname(e.name).toLowerCase();
485482
if (
@@ -496,7 +493,7 @@ export class WindowsProjectService
496493
}
497494
};
498495

499-
walk(sourcesFolder, pluginStageDir);
496+
walk(sourcesFolder);
500497

501498
// copy provided plugin.props/targets if present in plugin root
502499
const providedProps = path.join(pluginData.fullPath, "plugin.props");

0 commit comments

Comments
 (0)