SynapseCTRL 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. synapsectrl-0.2.1/.gitignore +16 -0
  2. synapsectrl-0.2.1/Install-SynapseInspectHook.ps1 +698 -0
  3. synapsectrl-0.2.1/LICENSE +201 -0
  4. synapsectrl-0.2.1/NOTICE +5 -0
  5. synapsectrl-0.2.1/PKG-INFO +327 -0
  6. synapsectrl-0.2.1/README.md +100 -0
  7. synapsectrl-0.2.1/codex-stuff/ASTRA_SPEC.md +360 -0
  8. synapsectrl-0.2.1/codex-stuff/KNOWN_FINDINGS.json +72 -0
  9. synapsectrl-0.2.1/codex-stuff/PRODUCT_FINDINGS.md +59 -0
  10. synapsectrl-0.2.1/codex-stuff/PROJECT_CLEANUP.md +44 -0
  11. synapsectrl-0.2.1/codex-stuff/README.md +32 -0
  12. synapsectrl-0.2.1/codex-stuff/README_FOR_ASTRA.md +18 -0
  13. synapsectrl-0.2.1/codex-stuff/tools/README.md +62 -0
  14. synapsectrl-0.2.1/codex-stuff/tools/_synapse.py +656 -0
  15. synapsectrl-0.2.1/codex-stuff/tools/capture_stdio.ps1 +43 -0
  16. synapsectrl-0.2.1/codex-stuff/tools/devices.py +38 -0
  17. synapsectrl-0.2.1/codex-stuff/tools/diagnostic_bundle.py +73 -0
  18. synapsectrl-0.2.1/codex-stuff/tools/eval.py +41 -0
  19. synapsectrl-0.2.1/codex-stuff/tools/hook_status.ps1 +36 -0
  20. synapsectrl-0.2.1/codex-stuff/tools/probe.py +23 -0
  21. synapsectrl-0.2.1/codex-stuff/tools/renderers.py +32 -0
  22. synapsectrl-0.2.1/codex-stuff/tools/source_search.py +54 -0
  23. synapsectrl-0.2.1/codex-stuff/tools/storage.py +58 -0
  24. synapsectrl-0.2.1/codex-stuff/tools/switch_profile.py +73 -0
  25. synapsectrl-0.2.1/docs/API.md +193 -0
  26. synapsectrl-0.2.1/docs/Architecture.md +81 -0
  27. synapsectrl-0.2.1/docs/Attribution.md +49 -0
  28. synapsectrl-0.2.1/docs/CLI.md +176 -0
  29. synapsectrl-0.2.1/docs/Development.md +83 -0
  30. synapsectrl-0.2.1/docs/Getting-Started.md +165 -0
  31. synapsectrl-0.2.1/docs/Publishing.md +104 -0
  32. synapsectrl-0.2.1/docs/Python-SDK.md +88 -0
  33. synapsectrl-0.2.1/docs/README.md +38 -0
  34. synapsectrl-0.2.1/docs/Setup.md +128 -0
  35. synapsectrl-0.2.1/docs/Troubleshooting.md +104 -0
  36. synapsectrl-0.2.1/examples/switch_profile.py +33 -0
  37. synapsectrl-0.2.1/pyproject.toml +63 -0
  38. synapsectrl-0.2.1/src/synapsectrl/__init__.py +14 -0
  39. synapsectrl-0.2.1/src/synapsectrl/__main__.py +7 -0
  40. synapsectrl-0.2.1/src/synapsectrl/_inspector.py +267 -0
  41. synapsectrl-0.2.1/src/synapsectrl/bootstrap.py +194 -0
  42. synapsectrl-0.2.1/src/synapsectrl/cli.py +346 -0
  43. synapsectrl-0.2.1/src/synapsectrl/client.py +254 -0
  44. synapsectrl-0.2.1/src/synapsectrl/discovery.py +418 -0
  45. synapsectrl-0.2.1/src/synapsectrl/errors.py +15 -0
  46. synapsectrl-0.2.1/src/synapsectrl/hook.py +90 -0
  47. synapsectrl-0.2.1/src/synapsectrl/models.py +85 -0
  48. synapsectrl-0.2.1/src/synapsectrl/py.typed +0 -0
  49. synapsectrl-0.2.1/synapse_ctrl.py +827 -0
  50. synapsectrl-0.2.1/tests/live_synapse.py +102 -0
  51. synapsectrl-0.2.1/tests/test_bootstrap.py +157 -0
  52. synapsectrl-0.2.1/tests/test_cli.py +226 -0
  53. synapsectrl-0.2.1/tests/test_client.py +215 -0
  54. synapsectrl-0.2.1/tests/test_discovery.py +282 -0
  55. synapsectrl-0.2.1/tests/test_hook_cli.py +57 -0
  56. synapsectrl-0.2.1/tests/test_inspector.py +135 -0
  57. synapsectrl-0.2.1/tests/test_installer.ps1 +216 -0
  58. synapsectrl-0.2.1/tests/test_integration_review.py +147 -0
@@ -0,0 +1,16 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .venv-*/
12
+ .uv-cache/
13
+ .ruff_cache/
14
+ .pytest_cache/
15
+ .coverage
16
+ htmlcov/
@@ -0,0 +1,698 @@
1
+ param(
2
+ [switch]$Uninstall,
3
+ [switch]$NoPause
4
+ )
5
+
6
+ $ErrorActionPreference = "Stop"
7
+
8
+ function Test-IsAdministrator {
9
+ $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
10
+ $principal = [Security.Principal.WindowsPrincipal]::new($identity)
11
+
12
+ return $principal.IsInRole(
13
+ [Security.Principal.WindowsBuiltInRole]::Administrator
14
+ )
15
+ }
16
+
17
+ if (-not (Test-IsAdministrator)) {
18
+ $elevatedArgs = @(
19
+ "-NoProfile",
20
+ "-ExecutionPolicy", "Bypass",
21
+ "-File", "`"$PSCommandPath`"",
22
+ # The parent handles the optional pause; the hidden child must never wait.
23
+ "-NoPause"
24
+ )
25
+
26
+ if ($Uninstall) {
27
+ $elevatedArgs += "-Uninstall"
28
+ }
29
+
30
+ try {
31
+ $powerShellExe = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
32
+ if ([Environment]::Is64BitOperatingSystem -and -not [Environment]::Is64BitProcess) {
33
+ $powerShellExe = Join-Path $env:SystemRoot "Sysnative\WindowsPowerShell\v1.0\powershell.exe"
34
+ }
35
+ $child = Start-Process -FilePath $powerShellExe -Verb RunAs `
36
+ -WindowStyle Hidden -ArgumentList $elevatedArgs -Wait -PassThru
37
+ $resultCode = $child.ExitCode
38
+ if ($resultCode -ne 0) {
39
+ Write-Error "Elevated SynapseCTRL installer failed (exit $resultCode). Run this script from an Administrator PowerShell window to see the detailed error." -ErrorAction Continue
40
+ } else {
41
+ Write-Host "SynapseCTRL hook operation completed. Fully exit and reopen Synapse to apply changes."
42
+ }
43
+ } catch {
44
+ Write-Error "Could not complete elevated installer: $_" -ErrorAction Continue
45
+ $resultCode = 1
46
+ }
47
+ if (-not $NoPause) { Read-Host "Press Enter to continue..." | Out-Null }
48
+ exit $resultCode
49
+ }
50
+
51
+ function Invoke-SynapseHook {
52
+
53
+ $StableExe = Join-Path `
54
+ $env:ProgramFiles `
55
+ "Razer\RazerAppEngine\RazerAppEngine.exe"
56
+
57
+ $InstallDir = Join-Path `
58
+ $env:ProgramFiles `
59
+ "SynapseCTRL"
60
+
61
+ $ShimExe = Join-Path `
62
+ $InstallDir `
63
+ "RazerInspectShim.exe"
64
+
65
+ $OldShimPs1 = Join-Path `
66
+ $InstallDir `
67
+ "RazerInspectShim.ps1"
68
+
69
+ $IFEOBase = (
70
+ "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\" +
71
+ "Image File Execution Options\RazerAppEngine.exe"
72
+ )
73
+
74
+ $FilterKey = Join-Path `
75
+ $IFEOBase `
76
+ "SynapseCTRL"
77
+
78
+ $debuggerCommand = "`"$ShimExe`""
79
+
80
+ # Check ownership and conflicts before compiling, replacing files, or changing IFEO.
81
+ $baseProperties = if (Test-Path -LiteralPath $IFEOBase) {
82
+ Get-ItemProperty -LiteralPath $IFEOBase
83
+ } else { $null }
84
+ $filterProperties = if (Test-Path -LiteralPath $FilterKey) {
85
+ Get-ItemProperty -LiteralPath $FilterKey
86
+ } else { $null }
87
+ if ($filterProperties -and (
88
+ $filterProperties.FilterFullPath -ine $StableExe -or
89
+ $filterProperties.Debugger -ine $debuggerCommand
90
+ )) {
91
+ throw "The SynapseCTRL IFEO filter has unexpected values. No changes were made; inspect $FilterKey before repairing it."
92
+ }
93
+ if (-not $Uninstall) {
94
+ if (-not (Test-Path -LiteralPath $StableExe -PathType Leaf)) {
95
+ throw "Razer launcher not found: $StableExe"
96
+ }
97
+ if ($baseProperties.Debugger) {
98
+ throw "A non-filtered IFEO Debugger already exists for RazerAppEngine.exe. No changes were made: $($baseProperties.Debugger)"
99
+ }
100
+ if (Test-Path -LiteralPath $IFEOBase) {
101
+ foreach ($otherFilter in Get-ChildItem -LiteralPath $IFEOBase) {
102
+ if ($otherFilter.PSChildName -ieq 'SynapseCTRL') { continue }
103
+ $otherProperties = Get-ItemProperty -LiteralPath $otherFilter.PSPath
104
+ if ($otherProperties.FilterFullPath -ieq $StableExe) {
105
+ throw "Another IFEO filter already targets the stable Razer launcher: $($otherFilter.PSChildName). No changes were made."
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ # Every removable file is a fixed immediate child of this exact install directory.
112
+ # Refuse redirected paths even when this script is invoked with elevation.
113
+ $expectedInstallDir = [IO.Path]::GetFullPath((Join-Path $env:ProgramFiles 'SynapseCTRL'))
114
+ if ([IO.Path]::GetFullPath($InstallDir) -ine $expectedInstallDir) {
115
+ throw "Unexpected installation directory: $InstallDir"
116
+ }
117
+ foreach ($ownedPath in @($InstallDir, $ShimExe, $OldShimPs1)) {
118
+ if (Test-Path -LiteralPath $ownedPath) {
119
+ if ((Get-Item -LiteralPath $ownedPath -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) {
120
+ throw "Refusing redirected installation path: $ownedPath"
121
+ }
122
+ }
123
+ }
124
+
125
+ if ($Uninstall) {
126
+ if ($filterProperties) {
127
+ # Preserve any unrelated values or nested filters in the same key.
128
+ foreach ($valueName in @('Debugger', 'FilterFullPath', 'HookVersion', 'PreviousUseFilter', 'UseFilterWasPresent')) {
129
+ Remove-ItemProperty -LiteralPath $FilterKey -Name $valueName -ErrorAction SilentlyContinue
130
+ }
131
+ $remainingKey = Get-Item -LiteralPath $FilterKey
132
+ if ($remainingKey.ValueCount -eq 0 -and $remainingKey.SubKeyCount -eq 0) {
133
+ Remove-Item -LiteralPath $FilterKey -Force
134
+ }
135
+ }
136
+
137
+ if (Test-Path $ShimExe) {
138
+ Remove-Item `
139
+ -LiteralPath $ShimExe `
140
+ -Force
141
+ }
142
+
143
+ if (Test-Path $OldShimPs1) {
144
+ Remove-Item `
145
+ -LiteralPath $OldShimPs1 `
146
+ -Force
147
+ }
148
+
149
+ if (Test-Path $IFEOBase) {
150
+ $remainingFilters = @(
151
+ Get-ChildItem `
152
+ -LiteralPath $IFEOBase
153
+ )
154
+
155
+ if ($remainingFilters.Count -eq 0 -and $filterProperties -and
156
+ $baseProperties.UseFilter -eq 1 -and -not $baseProperties.Debugger) {
157
+ if ($filterProperties.UseFilterWasPresent -eq 1) {
158
+ Set-ItemProperty -LiteralPath $IFEOBase -Name UseFilter -Value $filterProperties.PreviousUseFilter
159
+ } elseif ($baseProperties.UseFilter -eq 1) {
160
+ Remove-ItemProperty -LiteralPath $IFEOBase -Name UseFilter -ErrorAction SilentlyContinue
161
+ }
162
+ }
163
+ }
164
+
165
+ if (
166
+ (Test-Path $InstallDir) -and
167
+ @(
168
+ Get-ChildItem `
169
+ -LiteralPath $InstallDir `
170
+ -Force `
171
+ -ErrorAction SilentlyContinue
172
+ ).Count -eq 0
173
+ ) {
174
+ Remove-Item `
175
+ -LiteralPath $InstallDir `
176
+ -Force
177
+ }
178
+
179
+ Write-Host ""
180
+ Write-Host "SynapseCTRL inspector hook removed."
181
+ return
182
+ }
183
+
184
+ New-Item `
185
+ -ItemType Directory `
186
+ -Path $InstallDir `
187
+ -Force | Out-Null
188
+
189
+ # Build a GUI-subsystem executable.
190
+ # Unlike powershell.exe, this shim has NO console of its own.
191
+ $source = @'
192
+ using System;
193
+ using System.Collections.Generic;
194
+ using System.ComponentModel;
195
+ using System.IO;
196
+ using System.Runtime.InteropServices;
197
+ using System.Text;
198
+
199
+ internal static class RazerInspectShim
200
+ {
201
+ private const uint CREATE_NO_WINDOW = 0x08000000;
202
+
203
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
204
+ private struct STARTUPINFO
205
+ {
206
+ public int cb;
207
+ public string lpReserved;
208
+ public string lpDesktop;
209
+ public string lpTitle;
210
+ public int dwX;
211
+ public int dwY;
212
+ public int dwXSize;
213
+ public int dwYSize;
214
+ public int dwXCountChars;
215
+ public int dwYCountChars;
216
+ public int dwFillAttribute;
217
+ public int dwFlags;
218
+ public short wShowWindow;
219
+ public short cbReserved2;
220
+ public IntPtr lpReserved2;
221
+ public IntPtr hStdInput;
222
+ public IntPtr hStdOutput;
223
+ public IntPtr hStdError;
224
+ }
225
+
226
+ [StructLayout(LayoutKind.Sequential)]
227
+ private struct PROCESS_INFORMATION
228
+ {
229
+ public IntPtr hProcess;
230
+ public IntPtr hThread;
231
+ public int dwProcessId;
232
+ public int dwThreadId;
233
+ }
234
+
235
+ [DllImport(
236
+ "kernel32.dll",
237
+ CharSet = CharSet.Unicode,
238
+ SetLastError = true
239
+ )]
240
+ [return: MarshalAs(UnmanagedType.Bool)]
241
+ private static extern bool CreateProcessW(
242
+ string lpApplicationName,
243
+ StringBuilder lpCommandLine,
244
+ IntPtr lpProcessAttributes,
245
+ IntPtr lpThreadAttributes,
246
+ bool bInheritHandles,
247
+ uint dwCreationFlags,
248
+ IntPtr lpEnvironment,
249
+ string lpCurrentDirectory,
250
+ ref STARTUPINFO lpStartupInfo,
251
+ out PROCESS_INFORMATION lpProcessInformation
252
+ );
253
+
254
+ [DllImport("kernel32.dll", SetLastError = true)]
255
+ [return: MarshalAs(UnmanagedType.Bool)]
256
+ private static extern bool CloseHandle(IntPtr hObject);
257
+
258
+ [STAThread]
259
+ private static int Main(string[] args)
260
+ {
261
+ try
262
+ {
263
+ string programFiles =
264
+ Environment.GetFolderPath(
265
+ Environment.SpecialFolder.ProgramFiles
266
+ );
267
+
268
+ string root = Path.Combine(
269
+ programFiles,
270
+ "Razer",
271
+ "RazerAppEngine"
272
+ );
273
+
274
+ string versionedExe = FindNewestVersionedExe(root);
275
+
276
+ if (String.IsNullOrEmpty(versionedExe))
277
+ {
278
+ LogError(
279
+ "Could not find a versioned RazerAppEngine.exe."
280
+ );
281
+
282
+ return 193;
283
+ }
284
+
285
+ List<string> forwarded = BuildArguments(args);
286
+
287
+ StringBuilder commandLine = new StringBuilder();
288
+
289
+ commandLine.Append(
290
+ QuoteWindowsArgument(versionedExe)
291
+ );
292
+
293
+ for (int i = 0; i < forwarded.Count; i++)
294
+ {
295
+ commandLine.Append(' ');
296
+ commandLine.Append(
297
+ QuoteWindowsArgument(forwarded[i])
298
+ );
299
+ }
300
+
301
+ STARTUPINFO startup = new STARTUPINFO();
302
+ startup.cb = Marshal.SizeOf(typeof(STARTUPINFO));
303
+
304
+ PROCESS_INFORMATION processInfo;
305
+
306
+ bool started = CreateProcessW(
307
+ versionedExe,
308
+ commandLine,
309
+ IntPtr.Zero,
310
+ IntPtr.Zero,
311
+
312
+ // Do not let the Razer process inherit console/stdout
313
+ // handles from anything that happened to launch us.
314
+ false,
315
+
316
+ // If RazerAppEngine is a console-subsystem executable,
317
+ // explicitly create it without a console.
318
+ CREATE_NO_WINDOW,
319
+
320
+ IntPtr.Zero,
321
+ Path.GetDirectoryName(versionedExe),
322
+ ref startup,
323
+ out processInfo
324
+ );
325
+
326
+ if (!started)
327
+ {
328
+ int error = Marshal.GetLastWin32Error();
329
+
330
+ LogError(
331
+ "CreateProcessW failed: " +
332
+ error + " (" +
333
+ new Win32Exception(error).Message +
334
+ ")"
335
+ );
336
+
337
+ return error != 0 ? error : 194;
338
+ }
339
+
340
+ // We deliberately do not wait for Synapse.
341
+ // Windows does not tie a normal child process lifetime
342
+ // to its parent, so this shim exits immediately.
343
+ if (processInfo.hThread != IntPtr.Zero)
344
+ {
345
+ CloseHandle(processInfo.hThread);
346
+ }
347
+
348
+ if (processInfo.hProcess != IntPtr.Zero)
349
+ {
350
+ CloseHandle(processInfo.hProcess);
351
+ }
352
+
353
+ return 0;
354
+ }
355
+ catch (Exception ex)
356
+ {
357
+ LogError(ex.ToString());
358
+ return 195;
359
+ }
360
+ }
361
+
362
+ private static List<string> BuildArguments(string[] args)
363
+ {
364
+ List<string> forwarded = new List<string>();
365
+ forwarded.Add("--inspect=127.0.0.1:9229");
366
+
367
+ for (int i = 0; i < args.Length; i++)
368
+ {
369
+ string arg = args[i] ?? String.Empty;
370
+ // IFEO supplies the intercepted executable first; never launch it
371
+ // again, because it is the filtered stable path and would recurse.
372
+ if (i == 0 && Path.IsPathRooted(arg) && String.Equals(
373
+ Path.GetFileName(arg), "RazerAppEngine.exe",
374
+ StringComparison.OrdinalIgnoreCase))
375
+ {
376
+ continue;
377
+ }
378
+
379
+ // Node also accepts underscores in option names. Never preserve an
380
+ // incoming host/port, break-on-start, or inspector-disabling flag.
381
+ string option = arg.Split('=')[0].Replace('_', '-');
382
+ if (option.StartsWith("--inspect", StringComparison.OrdinalIgnoreCase) ||
383
+ option.StartsWith("--no-inspect", StringComparison.OrdinalIgnoreCase) ||
384
+ option.StartsWith("--debug", StringComparison.OrdinalIgnoreCase) ||
385
+ option.StartsWith("--no-debug", StringComparison.OrdinalIgnoreCase))
386
+ {
387
+ if (arg.IndexOf('=') < 0 && i + 1 < args.Length)
388
+ {
389
+ string next = args[i + 1] ?? String.Empty;
390
+ int port;
391
+ // Consume a separate --inspect-port value, or an endpoint
392
+ // after --inspect. Preserve ordinary application arguments.
393
+ if ((option.EndsWith("-port", StringComparison.OrdinalIgnoreCase) &&
394
+ !next.StartsWith("-", StringComparison.Ordinal)) ||
395
+ Int32.TryParse(next, out port) ||
396
+ (next.IndexOf(':') >= 0 && !next.StartsWith("-", StringComparison.Ordinal)))
397
+ {
398
+ i++;
399
+ }
400
+ }
401
+ continue;
402
+ }
403
+ forwarded.Add(arg);
404
+ }
405
+ return forwarded;
406
+ }
407
+
408
+ private static string FindNewestVersionedExe(string root)
409
+ {
410
+ if (!Directory.Exists(root))
411
+ {
412
+ return null;
413
+ }
414
+
415
+ string[] directories = Directory.GetDirectories(
416
+ root,
417
+ "app-*",
418
+ SearchOption.TopDirectoryOnly
419
+ );
420
+
421
+ Version newestVersion = null;
422
+ string newestExe = null;
423
+
424
+ for (int i = 0; i < directories.Length; i++)
425
+ {
426
+ string directory = directories[i];
427
+ string name = Path.GetFileName(directory);
428
+
429
+ if (
430
+ String.IsNullOrEmpty(name) ||
431
+ !name.StartsWith(
432
+ "app-",
433
+ StringComparison.OrdinalIgnoreCase
434
+ )
435
+ )
436
+ {
437
+ continue;
438
+ }
439
+
440
+ Version version;
441
+
442
+ if (!Version.TryParse(name.Substring(4), out version))
443
+ {
444
+ continue;
445
+ }
446
+
447
+ string exe = Path.Combine(
448
+ directory,
449
+ "RazerAppEngine.exe"
450
+ );
451
+
452
+ if (!File.Exists(exe))
453
+ {
454
+ continue;
455
+ }
456
+
457
+ if (
458
+ newestVersion == null ||
459
+ version > newestVersion
460
+ )
461
+ {
462
+ newestVersion = version;
463
+ newestExe = exe;
464
+ }
465
+ }
466
+
467
+ return newestExe;
468
+ }
469
+
470
+ private static string QuoteWindowsArgument(string value)
471
+ {
472
+ if (value == null)
473
+ {
474
+ return "\"\"";
475
+ }
476
+
477
+ if (value.Length == 0)
478
+ {
479
+ return "\"\"";
480
+ }
481
+
482
+ bool needsQuotes = false;
483
+
484
+ for (int i = 0; i < value.Length; i++)
485
+ {
486
+ char c = value[i];
487
+
488
+ if (
489
+ Char.IsWhiteSpace(c) ||
490
+ c == '"'
491
+ )
492
+ {
493
+ needsQuotes = true;
494
+ break;
495
+ }
496
+ }
497
+
498
+ if (!needsQuotes)
499
+ {
500
+ return value;
501
+ }
502
+
503
+ StringBuilder result = new StringBuilder();
504
+ result.Append('"');
505
+
506
+ int slashCount = 0;
507
+
508
+ for (int i = 0; i < value.Length; i++)
509
+ {
510
+ char c = value[i];
511
+
512
+ if (c == '\\')
513
+ {
514
+ slashCount++;
515
+ continue;
516
+ }
517
+
518
+ if (c == '"')
519
+ {
520
+ result.Append(
521
+ new string(
522
+ '\\',
523
+ slashCount * 2 + 1
524
+ )
525
+ );
526
+
527
+ result.Append('"');
528
+ slashCount = 0;
529
+ continue;
530
+ }
531
+
532
+ if (slashCount > 0)
533
+ {
534
+ result.Append(
535
+ new string('\\', slashCount)
536
+ );
537
+
538
+ slashCount = 0;
539
+ }
540
+
541
+ result.Append(c);
542
+ }
543
+
544
+ if (slashCount > 0)
545
+ {
546
+ result.Append(
547
+ new string(
548
+ '\\',
549
+ slashCount * 2
550
+ )
551
+ );
552
+ }
553
+
554
+ result.Append('"');
555
+
556
+ return result.ToString();
557
+ }
558
+
559
+ private static void LogError(string message)
560
+ {
561
+ try
562
+ {
563
+ string directory = Path.Combine(
564
+ Environment.GetFolderPath(
565
+ Environment.SpecialFolder.CommonApplicationData
566
+ ),
567
+ "SynapseCTRL"
568
+ );
569
+
570
+ Directory.CreateDirectory(directory);
571
+
572
+ File.AppendAllText(
573
+ Path.Combine(
574
+ directory,
575
+ "shim-error.log"
576
+ ),
577
+ DateTime.Now.ToString("O") +
578
+ Environment.NewLine +
579
+ message +
580
+ Environment.NewLine +
581
+ Environment.NewLine
582
+ );
583
+ }
584
+ catch
585
+ {
586
+ }
587
+ }
588
+ }
589
+ '@
590
+
591
+ $tempShim = Join-Path `
592
+ $InstallDir `
593
+ ("RazerInspectShim-" + [Guid]::NewGuid().ToString('N') + ".exe")
594
+
595
+ try {
596
+ Add-Type `
597
+ -TypeDefinition $source `
598
+ -Language CSharp `
599
+ -OutputAssembly $tempShim `
600
+ -OutputType WindowsApplication
601
+
602
+ if (-not (Test-Path $tempShim)) {
603
+ throw "C# compiler did not produce the launcher executable."
604
+ }
605
+
606
+ Copy-Item `
607
+ -LiteralPath $tempShim `
608
+ -Destination $ShimExe `
609
+ -Force
610
+ }
611
+ finally {
612
+ Remove-Item `
613
+ -LiteralPath $tempShim `
614
+ -Force `
615
+ -ErrorAction SilentlyContinue
616
+ }
617
+
618
+ # Delete the old PowerShell-based shim if this is an upgrade.
619
+ Remove-Item `
620
+ -LiteralPath $OldShimPs1 `
621
+ -Force `
622
+ -ErrorAction SilentlyContinue
623
+
624
+ New-Item `
625
+ -Path $IFEOBase `
626
+ -Force | Out-Null
627
+
628
+ New-ItemProperty `
629
+ -Path $IFEOBase `
630
+ -Name UseFilter `
631
+ -PropertyType DWord `
632
+ -Value 1 `
633
+ -Force | Out-Null
634
+
635
+ New-Item `
636
+ -Path $FilterKey `
637
+ -Force | Out-Null
638
+
639
+ if (-not $filterProperties) {
640
+ $useFilterWasPresent = [int]($null -ne $baseProperties -and $null -ne $baseProperties.UseFilter)
641
+ New-ItemProperty -LiteralPath $FilterKey -Name UseFilterWasPresent -PropertyType DWord -Value $useFilterWasPresent -Force | Out-Null
642
+ if ($useFilterWasPresent) {
643
+ New-ItemProperty -LiteralPath $FilterKey -Name PreviousUseFilter -PropertyType DWord -Value $baseProperties.UseFilter -Force | Out-Null
644
+ }
645
+ }
646
+ New-ItemProperty -LiteralPath $FilterKey -Name HookVersion -PropertyType DWord -Value 2 -Force | Out-Null
647
+
648
+ New-ItemProperty `
649
+ -Path $FilterKey `
650
+ -Name FilterFullPath `
651
+ -PropertyType String `
652
+ -Value $StableExe `
653
+ -Force | Out-Null
654
+
655
+ # The critical change:
656
+ # IFEO now invokes a GUI-subsystem .exe directly.
657
+ # powershell.exe is no longer anywhere in the normal Synapse launch path.
658
+ $debuggerCommand = "`"$ShimExe`""
659
+
660
+ New-ItemProperty `
661
+ -Path $FilterKey `
662
+ -Name Debugger `
663
+ -PropertyType String `
664
+ -Value $debuggerCommand `
665
+ -Force | Out-Null
666
+
667
+ Write-Host ""
668
+ Write-Host "Installed native no-console Synapse inspector hook."
669
+ Write-Host ""
670
+ Write-Host "IFEO launcher:"
671
+ Write-Host " $ShimExe"
672
+ Write-Host ""
673
+ Write-Host "Synapse will receive:"
674
+ Write-Host " --inspect=127.0.0.1:9229"
675
+ Write-Host ""
676
+ Write-Host "No PowerShell process participates in normal Synapse launches."
677
+ Write-Host ""
678
+ Write-Host "Fully Exit Synapse once, then open the normal Razer Synapse shortcut."
679
+ Write-Host ""
680
+ Write-Host "Verify inspector:"
681
+ Write-Host " Invoke-RestMethod http://127.0.0.1:9229/json/list"
682
+ Write-Host ""
683
+ Write-Host "Verify no shim is lingering:"
684
+ Write-Host " Get-Process RazerInspectShim -ErrorAction SilentlyContinue"
685
+ Write-Host ""
686
+ Write-Host "That command should return nothing after Synapse has started."
687
+ Write-Host ""
688
+ }
689
+
690
+ try {
691
+ Invoke-SynapseHook
692
+ $resultCode = 0
693
+ } catch {
694
+ Write-Error "SynapseCTRL hook operation failed: $_" -ErrorAction Continue
695
+ $resultCode = 1
696
+ }
697
+ if (-not $NoPause) { Read-Host "Press Enter to continue..." | Out-Null }
698
+ exit $resultCode