-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdLabAppControl.ahk
More file actions
404 lines (359 loc) · 18.2 KB
/
dLabAppControl.ahk
File metadata and controls
404 lines (359 loc) · 18.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
; ============================================================================
; dLabAppControl - Lab Application Controller for RDP Disconnect Handling
; ============================================================================
; Manages single or dual applications with automatic closure on RDP disconnect
; Supports custom close methods and embedded app containers
; ============================================================================
#SingleInstance Force
#Requires AutoHotkey v2.0
ProcessSetPriority "High"
; ============================================================================
; LOAD MODULES
; ============================================================================
#Include lib\Config.ahk
#Include lib\Utils.ahk
#Include lib\WindowClosing.ahk
#Include lib\RdpMonitoring.ahk
#Include lib\SingleAppMode.ahk
#Include lib\DualAppMode.ahk
global APP_VERSION := "2.4.0"
Log("dLabAppControl v" . APP_VERSION . " starting")
; ============================================================================
; HELP & USAGE
; ============================================================================
; Single mode examples (CMD/Batch syntax):
; dLabAppControl.exe "MozillaWindowClass" "\"C:\Program Files\Mozilla Firefox\firefox.exe\""
; dLabAppControl.exe "Notepad++" "\"C:\Program Files (x86)\Notepad++\notepad++.exe\""
; dLabAppControl.exe "Chrome_WidgetWin_1" "\"C:\Program Files\Google\Chrome\Application\chrome.exe\" --app=http://www.google.com"
; (Note: Browser auto-kiosk will add --kiosk --incognito automatically)
; dLabAppControl.exe "Notepad++" "\"C:\Program Files (x86)\Notepad++\notepad++.exe\" test.txt" @test
;
; Dual mode example (CMD/Batch syntax):
; dLabAppControl.exe @dual "MozillaWindowClass" "\"C:\Program Files\Mozilla Firefox\firefox.exe\"" "Notepad++" "\"C:\Program Files (x86)\Notepad++\notepad++.exe\"" @tab1="Firefox" @tab2="Notepad++"
; (Note: Browser auto-kiosk is disabled in dual mode)
if (A_Args.Length < 2) {
infoHeader := Format("dLabAppControl v{1}`n", APP_VERSION)
MsgBox infoHeader . "Use: dLabAppControl.exe [window_ahk_class] [C:\path\to\app.exe] [@options]"
. "`n`nSingle Application Mode:"
. "`n- dLabAppControl.exe `"MozillaWindowClass`" `\`"C:\Program Files\Mozilla Firefox\firefox.exe\`""
. "`n- dLabAppControl.exe `"Chrome_WidgetWin_1`" `\`"C:\Program Files\Google\Chrome\Application\chrome.exe\`" http://127.0.0.1:8000`""
. "`n- dLabAppControl.exe `"Notepad++`" `\`"notepad++.exe\`" test.txt --myParam`" @test"
. "`n- dLabAppControl.exe `"MyAppClass`" `"myapp.exe`" @close-button=`"Button2`""
. "`n- dLabAppControl.exe `"LVDChild`" `"myVI.exe`" @close-coords=`"330,484`" @test"
. "`n`nDual Application Mode (Tabbed Container):"
. "`n- dLabAppControl.exe @dual `"Class1`" `"App1.exe`" `"Class2`" `"App2.exe`""
. "`n- dLabAppControl.exe @dual `"Class1`" `\`"App1.exe\`" --param1 value1`" `"Class2`" `\`"App2.exe\`" --param2 value2`" @tab1=`"Camera`" @tab2=`"Viewer`""
. "`n- Both apps shown in tabs within a single container window"
. "`n`nOptions (use @ prefix to avoid conflicts with app parameters):"
. "`n @dual Enable dual app mode (tabbed container)"
. "`n @tab1=`"Title`" Custom title for first tab (dual mode only)"
. "`n @tab2=`"Title`" Custom title for second tab (dual mode only)"
. "`n @close-button=`"ClassNN`" Custom close button control (e.g., Button2)"
. "`n @close-coords=`"X,Y`" Custom close coordinates in CLIENT space"
. "`n @test Test custom close method after 5 seconds"
. "`n`nApplication Commands:"
. "`n- Simple paths: C:\path\to\app.exe"
. "`n- With spaces and parameters: `\`"C:\my path\to\app.exe\`" --param1 value1`""
. "`n- App params can use -- freely (only @ is for dLabAppControl options)"
. "`n- CMD: Use \`" to escape quotes"
. "`n- Guacamole: Chrome_WidgetWin_1 `"C:\...\chrome.exe`" --app=http://url @test"
. "`n`nCoordinate Guidelines (use CLIENT coordinates from WindowSpy):"
. "`n- Example: @close-coords=`"330,484`" means 330 pixels right, 484 down from client area"
ExitApp
}
; ============================================================================
; MAIN ENTRY POINT - Argument Parsing & Mode Detection
; ============================================================================
; Helper function to determine if an argument is a full command (with parameters) or just a path
IsFullCommand(arg) {
; If it contains spaces and an executable extension, likely a command with parameters
; Examples:
; - "C:\path\to\app.exe --param value"
; - "\"C:\path\to\app.exe\" --param value"
; - "C:\Program Files\app.exe" https://url.com
if (InStr(arg, " ") && (InStr(arg, ".exe") || InStr(arg, ".bat") || InStr(arg, ".cmd"))) {
return true
}
return false
}
StripOuterQuotes(value) {
quote := Chr(34)
if (StrLen(value) >= 2 && SubStr(value, 1, 1) = quote && SubStr(value, -1) = quote) {
return SubStr(value, 2, StrLen(value) - 2)
}
return value
}
HasOuterQuotes(value) {
quote := Chr(34)
return StrLen(value) >= 2 && SubStr(value, 1, 1) = quote && SubStr(value, -1) = quote
}
; Parse optional parameters
DUAL_APP_MODE := false
tab1Title := "Application 1" ; Default title
tab2Title := "Application 2" ; Default title
positionalArgs := [] ; Non-option arguments
; Global variables for custom close (accessed by Utils.ahk and WindowClosing.ahk)
global customCloseControl := ""
global customCloseX := 0
global customCloseY := 0
global TEST_MODE := false
global CUSTOM_CLOSE_METHOD := "none"
global ARGS_DUMP_PATH := ""
; Log all received arguments for debugging
Log("==== RECEIVED ARGUMENTS ====")
Log("Total arguments: " . A_Args.Length)
for index, arg in A_Args {
Log("Arg[" . index . "]: '" . arg . "'")
}
Log("============================")
; First pass: extract options (prefixed with @) and collect positional arguments
for index, arg in A_Args {
if (SubStr(arg, 1, 1) = "@") {
; This is a dLabAppControl option (prefixed with @)
argLower := StrLower(arg)
if (argLower = "@dual") {
DUAL_APP_MODE := true
Log("@dual flag detected - Dual app mode enabled")
} else if (argLower = "@test") {
TEST_MODE := true
Log("@test flag detected - Test mode enabled")
} else if (SubStr(argLower, 1, 6) = "@tab1=") {
tab1Title := SubStr(arg, 7)
quote := Chr(34)
if (SubStr(tab1Title, 1, 1) = quote && SubStr(tab1Title, -1) = quote) {
tab1Title := SubStr(tab1Title, 2, StrLen(tab1Title) - 2)
}
Log("Custom tab 1 title: " . tab1Title)
} else if (SubStr(argLower, 1, 6) = "@tab2=") {
tab2Title := SubStr(arg, 7)
quote := Chr(34)
if (SubStr(tab2Title, 1, 1) = quote && SubStr(tab2Title, -1) = quote) {
tab2Title := SubStr(tab2Title, 2, StrLen(tab2Title) - 2)
}
Log("Custom tab 2 title: " . tab2Title)
} else if (SubStr(argLower, 1, 14) = "@close-button=") {
customCloseControl := SubStr(arg, 15)
quote := Chr(34)
if (SubStr(customCloseControl, 1, 1) = quote && SubStr(customCloseControl, -1) = quote) {
customCloseControl := SubStr(customCloseControl, 2, StrLen(customCloseControl) - 2)
}
CUSTOM_CLOSE_METHOD := "control"
Log("Custom close button: " . customCloseControl)
} else if (SubStr(argLower, 1, 14) = "@close-coords=") {
coordsStr := SubStr(arg, 15)
quote := Chr(34)
if (SubStr(coordsStr, 1, 1) = quote && SubStr(coordsStr, -1) = quote) {
coordsStr := SubStr(coordsStr, 2, StrLen(coordsStr) - 2)
}
; Parse X,Y coordinates
coords := StrSplit(coordsStr, ",")
if (coords.Length = 2) {
customCloseX := Integer(coords[1])
customCloseY := Integer(coords[2])
CUSTOM_CLOSE_METHOD := "coordinates"
Log("Custom close coordinates: " . customCloseX . "," . customCloseY)
} else {
if !SILENT_ERRORS {
MsgBox("Error: @close-coords must be in format X,Y (e.g., @close-coords=`"330,484`")", "Invalid Coordinates", 16)
}
ExitApp(1)
}
} else if (SubStr(argLower, 1, 11) = "@dump-args=") {
ARGS_DUMP_PATH := StripOuterQuotes(SubStr(arg, 12))
if (ARGS_DUMP_PATH = "") {
MsgBox("Error: @dump-args requires a valid file path", "Invalid @dump-args", 16)
ExitApp(1)
}
Log("Argument dump will be written to: " . ARGS_DUMP_PATH)
} else {
Log("WARNING: Unknown option: " . arg . " - ignoring")
}
} else {
; This is a positional argument (window class, app command, or app parameters)
positionalArgs.Push(arg)
}
}
; Validate custom close parameters
if (customCloseControl != "" && (customCloseX > 0 || customCloseY > 0)) {
if !SILENT_ERRORS {
MsgBox("Error: Cannot use both @close-button and @close-coords at the same time", "Invalid Parameters", 16)
}
ExitApp(1)
}
; Parse arguments based on mode
if (DUAL_APP_MODE) {
; Dual app mode: class1 command1 class2 command2
if (positionalArgs.Length < 4) {
MsgBox "Error: Dual mode requires 4 arguments: class1 command1 class2 command2"
ExitApp
}
windowClass := positionalArgs[1]
appInput := positionalArgs[2]
appWasQuoted := HasOuterQuotes(appInput)
appCommand := StripOuterQuotes(appInput)
windowClass2 := positionalArgs[3]
appInput2 := positionalArgs[4]
appWasQuoted2 := HasOuterQuotes(appInput2)
appCommand2 := StripOuterQuotes(appInput2)
quote := Chr(34)
; Reconstruct commands if there are additional arguments beyond the basic 4
; This handles cases where Guacamole might split application parameters
if (positionalArgs.Length > 4) {
Log("Additional arguments in dual mode - may need reconstruction")
; Wrap executables before appending any extra parameters so spaces stay intact
if (SubStr(appCommand, 1, 1) != quote) {
appCommand := quote . appCommand . quote
Log("Wrapped app1 executable path for reconstruction")
}
if (SubStr(appCommand2, 1, 1) != quote) {
appCommand2 := quote . appCommand2 . quote
Log("Wrapped app2 executable path for reconstruction")
}
; Collect extra arguments - they could belong to either app
; Strategy: Assume extra args belong to app2 if we can't determine otherwise
Loop positionalArgs.Length - 4 {
argIndex := 4 + A_Index
appCommand2 .= " " . positionalArgs[argIndex]
Log("Added argument to App2 [" . argIndex . "]: " . positionalArgs[argIndex])
}
Log("Reconstructed App2 command: " . appCommand2)
}
; Ensure simple paths with spaces stay quoted when no extra parameters were provided
if (InStr(appCommand, " ") && SubStr(appCommand, 1, 1) != quote) {
appCommand := quote . appCommand . quote
Log("Auto-quoted App1 executable path (dual mode): " . appCommand)
} else if (!InStr(appCommand, " ") && appWasQuoted && SubStr(appCommand, 1, 1) != quote) {
appCommand := quote . appCommand . quote
Log("Preserved App1 quotes (dual mode)")
}
if (InStr(appCommand2, " ") && SubStr(appCommand2, 1, 1) != quote) {
appCommand2 := quote . appCommand2 . quote
Log("Auto-quoted App2 executable path (dual mode): " . appCommand2)
} else if (!InStr(appCommand2, " ") && appWasQuoted2 && SubStr(appCommand2, 1, 1) != quote) {
appCommand2 := quote . appCommand2 . quote
Log("Preserved App2 quotes (dual mode)")
}
; NOTE: Browser kiosk mode is NOT applied in dual mode
; Kiosk mode would prevent apps from being embedded in the tab container
; Extract executable paths for validation and logging
appPath := IsFullCommand(appCommand) ? ExtractExecutablePath(appCommand) : appCommand
appPath2 := IsFullCommand(appCommand2) ? ExtractExecutablePath(appCommand2) : appCommand2
Log("App 1: Class=" . windowClass . ", Command=" . appCommand . ", Tab Title=" . tab1Title)
Log("App 2: Class=" . windowClass2 . ", Command=" . appCommand2 . ", Tab Title=" . tab2Title)
Log("DUAL MODE: Browser kiosk auto-enhancement is disabled (apps must be embeddable)", "INFO")
if (ARGS_DUMP_PATH != "") {
dump := Map()
dump["windowClass"] := windowClass
dump["windowClass2"] := windowClass2
dump["appCommand"] := appCommand
dump["appCommand2"] := appCommand2
dump["tab1Title"] := tab1Title
dump["tab2Title"] := tab2Title
DumpParsedArgs("dual", dump)
ExitApp
}
; Launch dual app container with custom tab titles
CreateDualAppContainer(windowClass, appCommand, windowClass2, appCommand2, tab1Title, tab2Title)
return ; Container handles everything from here
} else {
; Single app mode
if (positionalArgs.Length < 2) {
MsgBox "Error: Single mode requires at least 2 arguments: class command"
ExitApp
}
windowClass := positionalArgs[1]
appInput := positionalArgs[2]
appWasQuoted := HasOuterQuotes(appInput)
appCommand := StripOuterQuotes(appInput)
quote := Chr(34)
; Reconstruct command if there are additional arguments (e.g., from Guacamole)
; Guacamole splits: Chrome_WidgetWin_1 "C:\Program Files\app.exe" https://url.com
; Into: Arg[1]=Chrome_WidgetWin_1, Arg[2]=C:\Program Files\app.exe, Arg[3]=https://url.com
if (positionalArgs.Length > 2) {
Log("Additional arguments detected - reconstructing command from " . positionalArgs.Length . " parts")
; Quote the executable path if it contains spaces
if (InStr(appCommand, " ")) {
appCommand := quote . appCommand . quote
Log("Quoted executable path: " . appCommand)
}
; Append all remaining arguments
Loop positionalArgs.Length - 2 {
argIndex := 2 + A_Index
appCommand .= " " . positionalArgs[argIndex]
Log("Added argument [" . argIndex . "]: " . positionalArgs[argIndex])
}
Log("Reconstructed full command: " . appCommand)
}
; For simple two-argument invocations, Windows strips the grouping quotes.
; Re-wrap any spaced path so later enhancements (kiosk flags) don't break it.
if (InStr(appCommand, " ") && SubStr(appCommand, 1, 1) != quote) {
appCommand := quote . appCommand . quote
Log("Auto-quoted executable path (single mode): " . appCommand)
} else if (!InStr(appCommand, " ") && appWasQuoted && SubStr(appCommand, 1, 1) != quote) {
appCommand := quote . appCommand . quote
Log("Preserved executable quotes (single mode)")
}
; Some options (@close-*, @test) are parsed separately, so the executable may lose quotes
; even when the original CLI used them. If any custom close/test option is active and the
; command is a bare executable path, re-wrap it to keep dumps predictable.
if (!IsFullCommand(appCommand) && SubStr(appCommand, 1, 1) != quote && (CUSTOM_CLOSE_METHOD != "none" || TEST_MODE)) {
appCommand := quote . appCommand . quote
Log("Wrapped executable path due to custom close/test options", "DEBUG")
}
; Auto-enhance browser command with kiosk/incognito flags
appCommand := EnhanceBrowserCommand(appCommand)
; Extract executable path for validation
appPath := IsFullCommand(appCommand) ? ExtractExecutablePath(appCommand) : appCommand
Log("SINGLE APP MODE - Class: " . windowClass . ", Command: " . appCommand)
if (CUSTOM_CLOSE_METHOD = "control") {
Log("Custom close method: Button control '" . customCloseControl . "'")
} else if (CUSTOM_CLOSE_METHOD = "coordinates") {
Log("Custom close method: Coordinates (" . customCloseX . "," . customCloseY . ")")
} else {
Log("Custom close method: Standard cascade")
}
if (ARGS_DUMP_PATH != "") {
dump := Map()
dump["windowClass"] := windowClass
dump["appCommand"] := appCommand
dump["customCloseMethod"] := CUSTOM_CLOSE_METHOD
dump["customCloseControl"] := customCloseControl
dump["customCloseCoords"] := customCloseX . "," . customCloseY
dump["testMode"] := TEST_MODE ? "true" : "false"
DumpParsedArgs("single", dump)
ExitApp
}
; Launch single app mode
CreateSingleApp(windowClass, appCommand)
return ; Single mode handles everything from here
}
; ============================================================================
; HOTKEY DIRECTIVES (Must be at file level, not inside functions)
; ============================================================================
; Block Alt+F4 on the lab window (single app mode)
#HotIf WinActive(target)
!F4::return
#HotIf
; Block Alt+F4 on both embedded applications (dual app mode)
; Check if app1Hwnd is set (non-zero) to ensure we're in dual mode
#HotIf (app1Hwnd != 0) && (WinActive("ahk_id " . app1Hwnd) || WinActive("ahk_id " . app2Hwnd))
!F4::return
#HotIf
DumpParsedArgs(mode, data) {
global ARGS_DUMP_PATH
try {
output := "mode=" . mode . "`n"
for key, value in data {
output .= key . "=" . value . "`n"
}
if (FileExist(ARGS_DUMP_PATH)) {
FileDelete(ARGS_DUMP_PATH)
}
FileAppend(output, ARGS_DUMP_PATH, "UTF-8")
Log("Argument dump saved to " . ARGS_DUMP_PATH, "DEBUG")
} catch as e {
Log("Failed to write argument dump: " . e.Message, "ERROR")
if !SILENT_ERRORS
MsgBox "Cannot write argument dump to: " . ARGS_DUMP_PATH
}
}