11import * as fs from "fs" ;
22import * as path from "path" ;
3+ import { spawn } from "child_process" ;
34import { ApplicationManagerBase } from "../application-manager-base" ;
45import { IHooksService , IChildProcess , IDictionary } from "../../declarations" ;
56import { IBuildData } from "../../../definitions/build" ;
67
78export 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}
0 commit comments