Repository navigation
Expand file tree
/
Copy pathtb-launch.ps1
More file actions
632 lines (577 loc) · 29.5 KB
/
Copy pathtb-launch.ps1
File metadata and controls
632 lines (577 loc) · 29.5 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
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
# Launch a process on a private, non-visible Windows desktop, inside a job
# object, and print its pid.
#
# Not run as a file. scripts/lib/tb-ide.mjs reads this text and hands it to
# powershell through -EncodedCommand, so no execution policy is involved and
# nothing has to be marked runnable. Inputs arrive as environment variables
# for the same reason: there is no argument quoting to get wrong.
#
# TBBUILD_EXE the executable
# TBBUILD_ARG its single argument, or empty for none
# TBBUILD_DESKTOP the private desktop's name: a slot's, from tbbroker (WIP.Broker.md),
# which already holds it; CreateDesktop opens a desktop that exists
# TBBUILD_JOB "0" for no job -- a --keep IDE, which must outlive the run
#
# Optional, and without them nothing below changes:
#
# TBBUILD_ARGS more of the command line, already quoted, added after
# TBBUILD_ARG: for a program that takes several arguments.
# TBBUILD_CWD the process's working folder; the executable's own folder
# when unset.
# TBBUILD_STDOUT a file path: the process's standard output goes to that file.
# TBBUILD_STDERR the same for standard error. When either is set the process
# gets standard handles of its own: stdin is the null device,
# and an output left unset is the null device too.
# TBBUILD_DIALOGS "close": while the process runs, every 250 ms, look for
# visible #32770 dialog boxes on the private desktop that
# belong to the process or to anything in its job. Each one
# seen for the first time is reported as a line
# `dialog <base64 of UTF-8 JSON {"title":...,"text":...}>`,
# text being its Static children's texts joined by a space,
# and is then closed: BM_CLICK on its OK button, else on its
# first button, else WM_CLOSE. A compiler that is given a
# damaged project opens a modal message box and waits on it
# for ever; on a private desktop nobody can answer it.
#
# The output is the pid on a line of its own, then any dialog lines, then
# `exit <n>`.
#
# A launch that fails prints no pid, and its cause as one line on stderr.
#
# With TBBUILD_WINDOWS set, nothing is launched: the script works on the windows
# of the private desktop TBBUILD_DESKTOP, which a launch made and still holds
# open, prints one line of JSON and ends (tb-ide.mjs's desktopWindows and
# captureWindow):
#
# TBBUILD_WINDOWS=list every top-level window on the desktop, with
# TBBUILD_CHILDREN=1 the children of each visible one
# too: handle, class, title, process id and
# executable, visibility, styles, owner, DPI and its
# rectangle in physical pixels
# TBBUILD_WINDOWS=capture PrintWindow of the window TBBUILD_HWND, with the
# flags TBBUILD_FLAGS (2, PW_RENDERFULLCONTENT, by
# default: without it what a WebView2 or any other
# DirectComposition content draws is left out), into
# the PNG file TBBUILD_FILE; the line gives its size,
# its DPI, its visible frame and client area, a layered
# window's colour key and alpha, and the texts of the
# window and its child windows, for the caller to
# search for the user name
# TBBUILD_WINDOWS=post PostMessage of TBBUILD_MSG, with TBBUILD_WPARAM and
# TBBUILD_LPARAM, to the window TBBUILD_HWND: a key
# pressed on a desktop that is not on screen reaches
# no window, so WM_SYSCOMMAND SC_KEYMENU with a letter
# opens a window's menu, as Alt and the letter do
#
# tb-ide.mjs passes the script with each line that is only a comment emptied, so
# no line of the C# below may start with `#`.
#
# The calls are made on a thread of their own that has been put on the desktop,
# since a thread sees and draws only the windows of its own desktop, and that is
# aware of each monitor's DPI, so that the sizes are physical pixels.
#
# Why a desktop at all: the twinBASIC IDE calls HostForceFocus() from its own
# window.onload, so it takes the keyboard whatever window style it is started
# with. `start /min` was tried and does not help. A process on another desktop
# has no foreground to take, which is the only thing that stops it -- and the
# IDE compiles there perfectly well, since nothing about the compile needs to
# be on screen.
#
# Why a job: killing the IDE's process tree is a race. The IDE restarts its
# compiler after a crash, and a compiler started while taskkill /T walks the
# tree is not in the tree it walked: it outlives the kill, orphaned, with its
# native debugger attached, holding the install's files open. Observed once in
# about a dozen crash-fixture runs, as the one process left behind. Every
# process the IDE starts inherits its job, and the job is created with
# KILL_ON_JOB_CLOSE, so when this launcher's handle to it closes -- the launcher
# ends when the IDE does, when tb-ide's shutdownIde kills it, and when the Node
# process that started it ends -- Windows ends whatever is still in the job,
# all at once and with nothing to race. A --keep IDE gets no job, because it
# has to outlive the run that started it.
#
# This is the one piece of the harness that cannot be JavaScript: it is Win32
# calls, and Node has no FFI without a native addon.
$ErrorActionPreference = "Stop"
# With its streams redirected, PowerShell writes to stderr in CLIXML: the
# progress record Add-Type makes ("Preparing modules for first use"), and any
# error. A failed launch then read "#< CLIXML" rather than its cause. So
# progress is silenced, and a failure is written here as plain text, in UTF-8,
# which is how tb-ide.mjs reads it.
$ProgressPreference = "SilentlyContinue"
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false
trap {
[Console]::Error.WriteLine($_.Exception.GetBaseException().Message)
exit 1
}
if ($env:TBBUILD_WINDOWS) {
Add-Type -ReferencedAssemblies System.Drawing -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
public static class TbWindows {
public delegate bool EnumProc(IntPtr hwnd, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern IntPtr OpenDesktop(string name, int flags, bool inherit, uint access);
[DllImport("user32.dll", SetLastError = true)] static extern bool SetThreadDesktop(IntPtr desk);
[DllImport("user32.dll", SetLastError = true)] static extern bool EnumDesktopWindows(IntPtr desk, EnumProc cb, IntPtr lp);
[DllImport("user32.dll")] static extern bool EnumChildWindows(IntPtr parent, EnumProc cb, IntPtr lp);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetClassName(IntPtr h, StringBuilder sb, int max);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder sb, int max);
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("user32.dll")] static extern bool IsWindow(IntPtr h);
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr h, out RECT r);
[DllImport("user32.dll")] static extern int GetWindowLong(IntPtr h, int index);
[DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr h, uint cmd);
[DllImport("user32.dll")] static extern uint GetDpiForWindow(IntPtr h);
[DllImport("user32.dll")] static extern IntPtr SetThreadDpiAwarenessContext(IntPtr ctx);
[DllImport("user32.dll", SetLastError = true)] static extern bool PrintWindow(IntPtr h, IntPtr hdc, uint flags);
[DllImport("user32.dll", SetLastError = true)] static extern bool PostMessage(IntPtr h, uint m, IntPtr w, IntPtr l);
[DllImport("user32.dll")] static extern bool GetLayeredWindowAttributes(IntPtr h, out uint key, out byte alpha, out uint flags);
[DllImport("dwmapi.dll")] static extern int DwmGetWindowAttribute(IntPtr h, int a, out RECT r, int n);
[DllImport("user32.dll")] static extern bool GetClientRect(IntPtr h, out RECT r);
[DllImport("user32.dll")] static extern int MapWindowPoints(IntPtr from, IntPtr to, ref RECT r, int n);
[DllImport("kernel32.dll")] static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool QueryFullProcessImageName(IntPtr p, int flags, StringBuilder sb, ref int size);
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h);
static Exception Failed(string what) {
return new Exception(what + " failed: " + new Win32Exception(Marshal.GetLastWin32Error()).Message);
}
static string Cls(IntPtr h) { StringBuilder sb = new StringBuilder(256); GetClassName(h, sb, 256); return sb.ToString(); }
static string Txt(IntPtr h) { StringBuilder sb = new StringBuilder(4096); GetWindowText(h, sb, 4096); return sb.ToString(); }
static string Exe(uint pid) {
// PROCESS_QUERY_LIMITED_INFORMATION
IntPtr p = OpenProcess(0x1000, false, pid);
if (p == IntPtr.Zero) return "";
StringBuilder sb = new StringBuilder(1024); int n = 1024;
string r = QueryFullProcessImageName(p, 0, sb, ref n) ? sb.ToString() : "";
CloseHandle(p);
return System.IO.Path.GetFileName(r);
}
static string Str(string s) {
StringBuilder b = new StringBuilder("\"");
foreach (char c in s) {
if (c == '"' || c == '\\') b.Append('\\').Append(c);
else if (c < 32) b.Append("\\u").Append(((int)c).ToString("x4"));
else b.Append(c);
}
return b.Append('"').ToString();
}
static string Describe(IntPtr h, int depth) {
uint pid; GetWindowThreadProcessId(h, out pid);
RECT r; GetWindowRect(h, out r);
return "{\"hwnd\":" + h.ToInt64() + ",\"depth\":" + depth + ",\"class\":" + Str(Cls(h)) +
",\"title\":" + Str(Txt(h)) + ",\"pid\":" + pid + ",\"exe\":" + Str(Exe(pid)) +
",\"visible\":" + (IsWindowVisible(h) ? "true" : "false") + ",\"style\":" + GetWindowLong(h, -16) +
",\"exStyle\":" + GetWindowLong(h, -20) + ",\"owner\":" + GetWindow(h, 4).ToInt64() +
",\"dpi\":" + GetDpiForWindow(h) + ",\"rect\":[" + r.Left + "," + r.Top + "," + r.Right + "," + r.Bottom + "]}";
}
// Runs `work` on a thread put on the desktop, which it can be as it owns no window yet
// (GENERIC_ALL), aware of each monitor's DPI (PER_MONITOR_AWARE_V2, -4).
static void OnDesktop(string desktop, Action<IntPtr> work) {
Exception failed = null;
Thread t = new Thread(() => {
IntPtr desk = IntPtr.Zero;
try {
desk = OpenDesktop(desktop, 0, false, 0x10000000);
if (desk == IntPtr.Zero) throw Failed("OpenDesktop " + desktop);
if (!SetThreadDesktop(desk)) throw Failed("SetThreadDesktop");
SetThreadDpiAwarenessContext(new IntPtr(-4));
work(desk);
} catch (Exception e) {
failed = e;
}
});
t.Start();
t.Join();
if (failed != null) throw failed;
}
public static string List(string desktop, bool children) {
List<string> rows = new List<string>();
OnDesktop(desktop, (desk) => {
List<IntPtr> top = new List<IntPtr>();
if (!EnumDesktopWindows(desk, delegate(IntPtr h, IntPtr l) { top.Add(h); return true; }, IntPtr.Zero)) {
throw Failed("EnumDesktopWindows");
}
foreach (IntPtr h in top) {
rows.Add(Describe(h, 0));
if (children && IsWindowVisible(h)) {
EnumChildWindows(h, delegate(IntPtr c, IntPtr l) { rows.Add(Describe(c, 1)); return true; }, IntPtr.Zero);
}
}
});
return "[" + string.Join(",", rows.ToArray()) + "]";
}
// [left, top, right, bottom] of `a` relative to `o`'s top left
static string Box(RECT a, RECT o) {
return "[" + (a.Left - o.Left) + "," + (a.Top - o.Top) + "," + (a.Right - o.Left) + "," + (a.Bottom - o.Top) + "]";
}
public static string Capture(string desktop, long hwnd, uint flags, string file) {
string result = null;
OnDesktop(desktop, (desk) => {
IntPtr h = new IntPtr(hwnd);
if (!IsWindow(h)) throw new Exception("there is no window " + hwnd);
RECT r; GetWindowRect(h, out r);
int w = r.Right - r.Left, ht = r.Bottom - r.Top;
if (w <= 0 || ht <= 0) throw new Exception("the window " + hwnd + " has no size");
using (Bitmap bmp = new Bitmap(w, ht, PixelFormat.Format32bppArgb)) {
using (Graphics g = Graphics.FromImage(bmp)) {
IntPtr hdc = g.GetHdc();
bool ok = PrintWindow(h, hdc, flags);
g.ReleaseHdc(hdc);
if (!ok) throw new Exception("PrintWindow drew nothing of the window " + hwnd);
}
bmp.Save(file, ImageFormat.Png);
}
List<string> texts = new List<string>();
texts.Add(Str(Txt(h)));
EnumChildWindows(h, delegate(IntPtr c, IntPtr l) { string t = Txt(c); if (t.Length > 0) texts.Add(Str(t)); return true; }, IntPtr.Zero);
// the visible frame (DWMWA_EXTENDED_FRAME_BOUNDS) and the client area in the picture,
// and a layered window's colour key, alpha and LWA_ flags
RECT f; if (DwmGetWindowAttribute(h, 9, out f, 16) != 0) f = r;
RECT cr; GetClientRect(h, out cr); MapWindowPoints(h, IntPtr.Zero, ref cr, 2);
uint key, lf; byte alpha;
string layered = GetLayeredWindowAttributes(h, out key, out alpha, out lf) ? "[" + key + "," + alpha + "," + lf + "]" : "null";
result = "{\"width\":" + w + ",\"height\":" + ht + ",\"dpi\":" + GetDpiForWindow(h) +
",\"frame\":" + Box(f, r) + ",\"client\":" + Box(cr, r) +
",\"layered\":" + layered + ",\"texts\":[" + string.Join(",", texts.ToArray()) + "]}";
});
return result;
}
public static string Post(string desktop, long hwnd, uint msg, long w, long l) {
OnDesktop(desktop, (desk) => {
IntPtr h = new IntPtr(hwnd);
if (!IsWindow(h)) throw new Exception("there is no window " + hwnd);
if (!PostMessage(h, msg, new IntPtr(w), new IntPtr(l))) throw Failed("PostMessage");
});
return "{\"posted\":true}";
}
}
'@
$desktop = $env:TBBUILD_DESKTOP
if ($env:TBBUILD_WINDOWS -eq "list") {
Write-Output ([TbWindows]::List($desktop, $env:TBBUILD_CHILDREN -eq "1"))
} elseif ($env:TBBUILD_WINDOWS -eq "capture") {
$flags = if ($env:TBBUILD_FLAGS) { [uint32]$env:TBBUILD_FLAGS } else { [uint32]2 }
Write-Output ([TbWindows]::Capture($desktop, [long]$env:TBBUILD_HWND, $flags, $env:TBBUILD_FILE))
} elseif ($env:TBBUILD_WINDOWS -eq "post") {
Write-Output ([TbWindows]::Post($desktop, [long]$env:TBBUILD_HWND, [uint32]$env:TBBUILD_MSG, [long]$env:TBBUILD_WPARAM, [long]$env:TBBUILD_LPARAM))
} else {
throw "TBBUILD_WINDOWS is not list, capture or post: $($env:TBBUILD_WINDOWS)"
}
exit 0
}
$exe = $env:TBBUILD_EXE
$arg = $env:TBBUILD_ARG
$desktop = if ($env:TBBUILD_DESKTOP) { $env:TBBUILD_DESKTOP } else { "tbbuild" }
# Each Win32 call is made, and its error read, in C#. PowerShell makes calls of
# its own before a script's next statement, and they replace the error: read
# from PowerShell, a CreateProcess that had set 3, "The system cannot find the
# path specified", was reported as 203, "The system could not find the
# environment option that was entered".
Add-Type -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
public static class TbLaunch {
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES {
public int nLength; public IntPtr lpSecurityDescriptor; public int bInheritHandle;
}
public delegate bool EnumProc(IntPtr hwnd, IntPtr lParam);
public class Dialog { public string Title, Text; }
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct STARTUPINFO {
public int cb; public string lpReserved; public string lpDesktop; public string lpTitle;
public int dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute;
public int dwFlags; public short wShowWindow; public short cbReserved2;
public IntPtr lpReserved2, hStdInput, hStdOutput, hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION {
public IntPtr hProcess, hThread; public int dwProcessId, dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION {
public long PerProcessUserTimeLimit, PerJobUserTimeLimit;
public uint LimitFlags;
public UIntPtr MinimumWorkingSetSize, MaximumWorkingSetSize;
public uint ActiveProcessLimit;
public UIntPtr Affinity;
public uint PriorityClass, SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
public struct IO_COUNTERS {
public ulong ReadOps, WriteOps, OtherOps, ReadBytes, WriteBytes, OtherBytes;
}
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
public JOBOBJECT_BASIC_LIMIT_INFORMATION Basic;
public IO_COUNTERS Io;
public UIntPtr ProcessMemoryLimit, JobMemoryLimit, PeakProcessMemoryUsed, PeakJobMemoryUsed;
}
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern IntPtr CreateDesktop(string name, IntPtr device, IntPtr devmode,
int flags, uint access, IntPtr sa);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern bool CreateProcess(string app, string cmd, IntPtr pa, IntPtr ta,
bool inherit, uint flags, IntPtr env, string cwd,
ref STARTUPINFO si, out PROCESS_INFORMATION pi);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern IntPtr CreateJobObject(IntPtr sa, string name);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetInformationJobObject(IntPtr job, int infoClass,
ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION info, uint length);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
[DllImport("kernel32.dll", SetLastError = true)]
static extern uint ResumeThread(IntPtr thread);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool TerminateProcess(IntPtr process, uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
static extern uint WaitForSingleObject(IntPtr handle, uint ms);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetExitCodeProcess(IntPtr process, out int exitCode);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern IntPtr CreateFile(string name, uint access, uint share,
ref SECURITY_ATTRIBUTES sa, uint disp, uint flags, IntPtr tmpl);
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool IsProcessInJob(IntPtr process, IntPtr job, out bool result);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr h);
[DllImport("user32.dll", SetLastError = true)]
static extern bool EnumDesktopWindows(IntPtr desk, EnumProc cb, IntPtr lp);
[DllImport("user32.dll", SetLastError = true)]
static extern bool EnumChildWindows(IntPtr parent, EnumProc cb, IntPtr lp);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
static extern int GetClassName(IntPtr h, StringBuilder sb, int max);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
static extern int GetWindowText(IntPtr h, StringBuilder sb, int max);
[DllImport("user32.dll")]
static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("user32.dll")]
static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll")]
static extern bool PostMessage(IntPtr h, uint msg, IntPtr w, IntPtr l);
[DllImport("user32.dll")]
static extern IntPtr GetDlgItem(IntPtr dlg, int id);
// The error of the call just made, with its text. Nothing may come between
// that call and this one.
static Exception Failed(string what) {
int code = Marshal.GetLastWin32Error();
return new Exception(what + " failed: " + new Win32Exception(code).Message);
}
// GENERIC_ALL (0x10000000).
public static IntPtr Desktop(string name) {
IntPtr desk = CreateDesktop(name, IntPtr.Zero, IntPtr.Zero, 0, 0x10000000, IntPtr.Zero);
if (desk == IntPtr.Zero) throw Failed("CreateDesktop");
return desk;
}
// KILL_ON_JOB_CLOSE (0x2000), set through JobObjectExtendedLimitInformation,
// which is information class 9.
public static IntPtr KillOnCloseJob() {
IntPtr job = CreateJobObject(IntPtr.Zero, null);
if (job == IntPtr.Zero) throw Failed("CreateJobObject");
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
info.Basic.LimitFlags = 0x2000;
if (!SetInformationJobObject(job, 9, ref info, (uint)Marshal.SizeOf(info))) {
throw Failed("SetInformationJobObject");
}
return job;
}
public static PROCESS_INFORMATION Start(string exe, string cmd, string cwd, string desktop,
uint flags) {
STARTUPINFO si = new STARTUPINFO();
si.cb = Marshal.SizeOf(si);
si.lpDesktop = desktop;
PROCESS_INFORMATION pi;
if (!CreateProcess(exe, cmd, IntPtr.Zero, IntPtr.Zero, false, flags, IntPtr.Zero, cwd,
ref si, out pi)) {
throw Failed("CreateProcess");
}
return pi;
}
// Ends the process, still suspended, when it cannot go into the job.
public static void Assign(IntPtr job, PROCESS_INFORMATION pi) {
if (AssignProcessToJobObject(job, pi.hProcess)) return;
Exception failed = Failed("AssignProcessToJobObject");
TerminateProcess(pi.hProcess, 1);
throw failed;
}
public static void Resume(PROCESS_INFORMATION pi) {
ResumeThread(pi.hThread);
}
// Waits for the process to end, through the handle CreateProcess returned,
// which stays valid however soon it ends; returns its exit code.
public static int Wait(PROCESS_INFORMATION pi) {
WaitForSingleObject(pi.hProcess, 0xFFFFFFFF);
int code;
if (!GetExitCodeProcess(pi.hProcess, out code)) throw Failed("GetExitCodeProcess");
return code;
}
// An inheritable handle: for writing, a file created or truncated, or the null
// device when the path is "NUL"; for reading, the null device.
public static IntPtr OpenInheritable(string path, bool write) {
SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
sa.nLength = Marshal.SizeOf(sa);
sa.bInheritHandle = 1;
bool device = !write || path == "NUL";
// a device is opened with OPEN_EXISTING (3), a file with CREATE_ALWAYS (2)
IntPtr h = CreateFile(device ? "NUL" : path, write ? 0x40000000u : 0x80000000u, 3, ref sa,
device ? 3u : 2u, 0x80, IntPtr.Zero);
if (h == new IntPtr(-1)) throw Failed("CreateFile " + (device ? "NUL" : path));
return h;
}
// Start with the standard handles given (STARTF_USESTDHANDLES). The handles
// must be inheritable, which makes CreateProcess inherit them.
public static PROCESS_INFORMATION StartWithHandles(string exe, string cmd, string cwd,
string desktop, uint flags,
IntPtr hIn, IntPtr hOut, IntPtr hErr) {
STARTUPINFO si = new STARTUPINFO();
si.cb = Marshal.SizeOf(si);
si.lpDesktop = desktop;
si.dwFlags = 0x100;
si.hStdInput = hIn;
si.hStdOutput = hOut;
si.hStdError = hErr;
PROCESS_INFORMATION pi;
if (!CreateProcess(exe, cmd, IntPtr.Zero, IntPtr.Zero, true, flags, IntPtr.Zero, cwd,
ref si, out pi)) {
throw Failed("CreateProcess");
}
return pi;
}
// Waits up to ms; true when the process has ended, with its exit code.
public static bool WaitMs(PROCESS_INFORMATION pi, uint ms, out int code) {
code = 0;
if (WaitForSingleObject(pi.hProcess, ms) != 0) return false;
if (!GetExitCodeProcess(pi.hProcess, out code)) throw Failed("GetExitCodeProcess");
return true;
}
static string Cls(IntPtr h) {
StringBuilder sb = new StringBuilder(256); GetClassName(h, sb, 256); return sb.ToString();
}
static string Txt(IntPtr h) {
StringBuilder sb = new StringBuilder(4096); GetWindowText(h, sb, 4096); return sb.ToString();
}
// Whether a window's process is the launched one or is in its job (job is
// IntPtr.Zero when there is none).
static bool Belongs(IntPtr hwnd, uint pid, IntPtr job) {
uint owner;
GetWindowThreadProcessId(hwnd, out owner);
if (owner == pid) return true;
if (job == IntPtr.Zero) return false;
// PROCESS_QUERY_LIMITED_INFORMATION
IntPtr p = OpenProcess(0x1000, false, owner);
if (p == IntPtr.Zero) return false;
bool inJob;
bool ok = IsProcessInJob(p, job, out inJob);
CloseHandle(p);
return ok && inJob;
}
// The visible dialog boxes (#32770) on the desktop that belong to the launched
// process or to its job and are not in `seen`: each is read, then closed. A
// window is forgotten once it is gone, since window handles are reused.
public static List<Dialog> NewDialogs(IntPtr desk, IntPtr job, int pid, HashSet<long> seen) {
List<IntPtr> top = new List<IntPtr>();
EnumDesktopWindows(desk, delegate(IntPtr h, IntPtr l) { top.Add(h); return true; }, IntPtr.Zero);
HashSet<long> now = new HashSet<long>();
List<Dialog> found = new List<Dialog>();
foreach (IntPtr h in top) {
now.Add(h.ToInt64());
if (seen.Contains(h.ToInt64())) continue;
if (!IsWindowVisible(h) || Cls(h) != "#32770") continue;
if (!Belongs(h, (uint)pid, job)) continue;
List<string> texts = new List<string>();
IntPtr first = IntPtr.Zero;
EnumChildWindows(h, delegate(IntPtr c, IntPtr l) {
string cls = Cls(c);
if (cls == "Static") {
string t = Txt(c);
if (t.Length > 0) texts.Add(t);
} else if (cls == "Button" && first == IntPtr.Zero) {
first = c;
}
return true;
}, IntPtr.Zero);
Dialog d = new Dialog();
d.Title = Txt(h);
d.Text = string.Join(" ", texts.ToArray());
found.Add(d);
seen.Add(h.ToInt64());
// IDOK is control id 1.
IntPtr ok = GetDlgItem(h, 1);
IntPtr btn = (ok != IntPtr.Zero && Cls(ok) == "Button") ? ok : first;
if (btn != IntPtr.Zero) PostMessage(btn, 0xF5, IntPtr.Zero, IntPtr.Zero); // BM_CLICK
else PostMessage(h, 0x10, IntPtr.Zero, IntPtr.Zero); // WM_CLOSE
}
seen.IntersectWith(now);
return found;
}
}
'@
# The slot's desktop, which tbbroker holds for as long as it runs; this handle
# is for the dialog closer, and goes with this process. Under TB_BROKER=off it
# is the only handle, so the desktop lives as long as this process -- hence the
# wait at the end.
$hDesk = [TbLaunch]::Desktop($desktop)
# The job. KILL_ON_JOB_CLOSE is the whole point; breakaway is not allowed, so
# nothing the IDE starts can leave it. Its one handle is this process's, which
# closes when this process ends, however it ends.
$useJob = $env:TBBUILD_JOB -ne "0"
if ($useJob) { $job = [TbLaunch]::KillOnCloseJob() }
# The command line is assembled here rather than by Start-Process, which
# appends a trailing space -- parseCommandLine() in ide/main2.js reads that as an
# empty second file argument and refuses the launch with "Bad command line
# syntax."
$cmd = '"' + $exe + '"'
if ($arg) { $cmd += ' "' + $arg + '"' }
if ($env:TBBUILD_ARGS) { $cmd += ' ' + $env:TBBUILD_ARGS }
# CREATE_SUSPENDED (0x4): the IDE goes into the job before it runs a single
# instruction, so there is no moment in which it could start a child outside.
$flags = if ($useJob) { 0x4 } else { 0 }
$cwd = if ($env:TBBUILD_CWD) { $env:TBBUILD_CWD } else { [IO.Path]::GetDirectoryName($exe) }
if ($env:TBBUILD_STDOUT -or $env:TBBUILD_STDERR) {
$hIn = [TbLaunch]::OpenInheritable("NUL", $false)
$hOut = [TbLaunch]::OpenInheritable($(if ($env:TBBUILD_STDOUT) { $env:TBBUILD_STDOUT } else { "NUL" }), $true)
$hErr = [TbLaunch]::OpenInheritable($(if ($env:TBBUILD_STDERR) { $env:TBBUILD_STDERR } else { "NUL" }), $true)
$pi = [TbLaunch]::StartWithHandles($exe, $cmd, $cwd, $desktop, $flags, $hIn, $hOut, $hErr)
} else {
$pi = [TbLaunch]::Start($exe, $cmd, $cwd, $desktop, $flags)
}
if ($useJob) {
# An IDE that cannot go into the job is ended, still suspended, rather than
# resumed: the caller gets no pid from a failed launch, so an IDE resumed
# here would be one nothing ever kills.
[TbLaunch]::Assign($job, $pi)
[TbLaunch]::Resume($pi)
}
Write-Output $pi.dwProcessId
# The process's exit code, on a line of its own once it ends: tbrun's --exe
# reads it for the probe's exe.
if ($env:TBBUILD_DIALOGS -eq "close") {
$jobHandle = if ($useJob) { $job } else { [IntPtr]::Zero }
$seen = New-Object 'System.Collections.Generic.HashSet[long]'
$code = 0
while ($true) {
foreach ($d in [TbLaunch]::NewDialogs($hDesk, $jobHandle, $pi.dwProcessId, $seen)) {
$json = ([ordered]@{ title = $d.Title; text = $d.Text } | ConvertTo-Json -Compress)
Write-Output ("dialog " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($json)))
}
if ([TbLaunch]::WaitMs($pi, 250, [ref]$code)) { break }
}
Write-Output ("exit " + $code)
} else {
Write-Output ("exit " + [TbLaunch]::Wait($pi))
}