18using System.Collections.Generic;
19using System.ComponentModel;
20using System.Diagnostics;
23using System.Runtime.ExceptionServices;
24using System.Runtime.InteropServices;
25using System.Threading;
26using System.Windows.Forms;
44 public static class HiddenDesktop
46 private const uint GenericAll = 0x10000000;
47 private const uint DesktopSwitchDesktop = 0x0100;
53 public const string DefaultName =
"GPAL.Hidden";
56 internal const int DefaultPeekMs = 5_000;
59 private static readonly Dictionary<string, IntPtr> desktops =
new Dictionary<string, IntPtr>();
60 private static readonly
object gate =
new object();
63 private static int unnamed = 0;
67 private static IntPtr cameFrom = IntPtr.Zero;
69 private delegate
bool EnumDesktopWindowsProc(IntPtr hWnd, IntPtr lParam);
74 static HiddenDesktop()
76 AppDomain.CurrentDomain.ProcessExit += (sender, args) => Return();
77 AppDomain.CurrentDomain.UnhandledException += (sender, args) => Return();
80 [DllImport(
"user32.dll", SetLastError =
true, CharSet = CharSet.Unicode)]
81 private static extern IntPtr CreateDesktop(
string lpszDesktop, IntPtr lpszDevice, IntPtr pDevmode,
int dwFlags, uint dwDesiredAccess, IntPtr lpsa);
83 [DllImport(
"user32.dll", SetLastError =
true)]
84 private static extern bool SetThreadDesktop(IntPtr hDesktop);
86 [DllImport(
"user32.dll", SetLastError =
true)]
87 private static extern bool CloseDesktop(IntPtr hDesktop);
89 [DllImport(
"user32.dll", SetLastError =
true)]
90 private static extern bool EnumDesktopWindows(IntPtr hDesktop, EnumDesktopWindowsProc lpfn, IntPtr lParam);
92 [DllImport(
"user32.dll")]
93 private static extern bool IsWindowVisible(IntPtr hWnd);
95 [DllImport(
"user32.dll", SetLastError =
true)]
96 private static extern bool SwitchDesktop(IntPtr hDesktop);
98 [DllImport(
"user32.dll", SetLastError =
true)]
99 private static extern IntPtr OpenInputDesktop(uint dwFlags,
bool fInherit, uint dwDesiredAccess);
101 [DllImport(
"user32.dll", SetLastError =
true)]
102 private static extern bool RegisterHotKey(IntPtr hWnd,
int id, uint fsModifiers, uint vk);
104 [DllImport(
"user32.dll", SetLastError =
true)]
105 private static extern bool UnregisterHotKey(IntPtr hWnd,
int id);
113 internal static string StartupNameFor(
string name)
115 return $@"WinSta0\{name}";
123 internal static string NextName()
125 return $
"{DefaultName}.{Interlocked.Increment(ref unnamed)}";
129 internal static IReadOnlyList<string> Names
134 return desktops.Keys.ToList();
144 internal static bool Ensure(
string name)
150 if (
false == desktops.ContainsKey(name))
152 IntPtr made = CreateDesktop(name, IntPtr.Zero, IntPtr.Zero, 0, GenericAll, IntPtr.Zero);
154 if (IntPtr.Zero == made)
155 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Could not make the desktop [{name}]",
null, GPALObjectType.Browser,
156 new Win32Exception(Marshal.GetLastWin32Error()));
158 desktops[name] = made;
161 retVal =
true == desktops.ContainsKey(name);
179 internal static T Launch<T>(
string name, Func<T> launch)
181 T retVal =
default(T);
182 ExceptionDispatchInfo failure =
null;
183 IntPtr handle = HandleFor(name);
185 Thread launcher =
new Thread(() =>
187 if (
false == SetThreadDesktop(handle))
188 failure = ExceptionDispatchInfo.Capture(
189 new Win32Exception(Marshal.GetLastWin32Error(), $
"Could not put the launching thread on [{name}]"));
200 failure = ExceptionDispatchInfo.Capture(ex);
205 launcher.IsBackground =
true;
220 internal static int WindowCount(
string name)
223 IntPtr handle = HandleFor(name);
225 if (IntPtr.Zero != handle)
226 EnumDesktopWindows(handle, (window, param) =>
228 if (
true == IsWindowVisible(window))
242 internal static int WindowCount()
244 return Names.Sum(name => WindowCount(name));
255 internal static bool HasWindows(
string name,
int timeoutInMs)
258 DateTime giveUpAt = DateTime.Now.AddMilliseconds(timeoutInMs);
260 while (
false == retVal && DateTime.Now < giveUpAt)
262 retVal = 0 < WindowCount(name);
274 internal static bool IsShowing
279 return IntPtr.Zero != cameFrom;
292 internal static bool Return()
300 cameFrom = IntPtr.Zero;
303 if (IntPtr.Zero != back)
305 retVal = SwitchDesktop(back);
330 internal static string Peek(
int forMsEach)
332 List<KeyValuePair<string, IntPtr>> walk;
335 walk = desktops.Where(desktop => 0 < WindowCount(desktop.Key)).ToList();
337 return Walk(walk, forMsEach,
"there is nothing on any hidden desktop to look at");
347 internal static string PeekOne(
string name,
int forMs)
349 List<KeyValuePair<string, IntPtr>> walk;
352 walk = desktops.Where(desktop => desktop.Key == name && 0 < WindowCount(desktop.Key)).ToList();
354 return Walk(walk, forMs, $
"there is nothing on the hidden desktop [{name}] to look at");
364 private static string Walk(List<KeyValuePair<string, IntPtr>> walk,
int forMsEach,
string nothingToSee)
366 string retVal =
null;
369 retVal = nothingToSee;
370 else if (
true == IsShowing)
371 retVal =
"the screen is already on a hidden desktop";
377 IntPtr from = OpenInputDesktop(0,
false, DesktopSwitchDesktop);
379 if (IntPtr.Zero == from)
380 retVal = $
"no handle on the desktop you are on [{Marshal.GetLastWin32Error()}], so nothing was switched";
388 Process guard = DesktopGuard.Start(0 < forMsEach ? forMsEach * walk.Count : 0);
392 Thread viewer =
new Thread(() =>
396 foreach (KeyValuePair<string, IntPtr> desktop
in walk)
400 using (Escape escape = Escape.ShowOn(desktop.Value, desktop.Key))
402 if (
false == SwitchDesktop(desktop.Value))
405 if (
true == escape.WaitToLeave(forMsEach))
413 DesktopGuard.Stop(guard);
417 viewer.IsBackground =
false;
423 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Cannot look at the hidden desktops: {retVal}",
null, GPALObjectType.Browser);
438 internal static void CloseAll()
445 foreach (IntPtr handle
in desktops.Values)
446 CloseDesktop(handle);
457 internal static void Close(
string name)
460 if (
true == desktops.TryGetValue(name, out IntPtr handle))
462 CloseDesktop(handle);
463 desktops.Remove(name);
472 private static IntPtr HandleFor(
string name)
477 desktops.TryGetValue(name, out retVal);
496 private sealed class Escape : IDisposable
498 private const int HotKeyId = 0xE5CA;
499 private const uint VkEscape = 0x1B;
500 private const uint ModNoRepeat = 0x4000;
504 private const int BlindHoldInMs = 15_000;
509 private const int MaxHoldInMs = 600_000;
511 private readonly ManualResetEvent leaving =
new ManualResetEvent(
false);
512 private readonly ManualResetEvent ready =
new ManualResetEvent(
false);
513 private readonly Thread thread;
515 private volatile EscapeForm form;
516 private volatile bool live;
518 private Escape(IntPtr desktop,
string name)
520 thread =
new Thread(() =>
522 if (
false == SetThreadDesktop(desktop))
526 EscapeForm showing =
new EscapeForm(name, Leave);
528 showing.Shown += (sender, args) =>
536 System.Windows.Forms.Application.Run(showing);
540 thread.IsBackground =
true;
541 thread.SetApartmentState(ApartmentState.STA);
552 internal static Escape ShowOn(IntPtr desktop,
string name)
554 Escape retVal =
new Escape(desktop, name);
556 retVal.ready.WaitOne(5_000);
558 if (
false == retVal.live)
559 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No way out could be put on the hidden desktop [{name}], so the clock is the only way back",
560 null, GPALObjectType.Browser);
570 internal bool WaitToLeave(
int forMs)
573 int limit = 0 < forMs ? forMs : (
true == live ? MaxHoldInMs : BlindHoldInMs);
575 retVal = leaving.WaitOne(limit);
581 public void Dispose()
583 EscapeForm closing = form;
585 if (
null != closing &&
true == closing.IsHandleCreated)
586 closing.BeginInvoke(
new Action(() => closing.Close()));
605 private sealed class EscapeForm : Form
607 private const int WmHotKey = 0x0312;
609 private readonly Action leave;
611 internal EscapeForm(
string desktop, Action leave)
615 Rectangle screen = Screen.PrimaryScreen.Bounds;
617 FormBorderStyle = FormBorderStyle.None;
618 StartPosition = FormStartPosition.Manual;
619 ShowInTaskbar =
false;
621 BackColor = Color.FromArgb(24, 24, 24);
622 ForeColor = Color.White;
623 Padding =
new Padding(6);
624 Size =
new Size(300, 62);
628 Location =
new Point(screen.Left + 12, screen.Bottom - Height - 12);
631 Button back =
new Button
633 Text =
"Return (Esc)",
634 Dock = DockStyle.Fill,
635 FlatStyle = FlatStyle.System,
636 Font =
new Font(
"Segoe UI", 9F, FontStyle.Bold),
639 Label caption =
new Label
642 Dock = DockStyle.Top,
644 TextAlign = ContentAlignment.MiddleCenter,
645 Font =
new Font(
"Segoe UI", 8F, FontStyle.Regular),
648 back.Click += (sender, args) => this.leave();
652 Controls.Add(caption);
654 KeyDown += (sender, args) =>
656 if (Keys.Escape == args.KeyCode)
661 protected override void OnHandleCreated(EventArgs e)
663 base.OnHandleCreated(e);
667 if (
false == RegisterHotKey(Handle, HotKeyId, ModNoRepeat, VkEscape))
668 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Escape could not be taken on the hidden desktop, so the button is the way out",
669 null, GPALObjectType.Browser,
new Win32Exception(Marshal.GetLastWin32Error()));
672 protected override void OnHandleDestroyed(EventArgs e)
674 UnregisterHotKey(Handle, HotKeyId);
676 base.OnHandleDestroyed(e);
679 protected override void WndProc(ref Message m)
681 if (WmHotKey == m.Msg && HotKeyId == m.WParam.ToInt32())
697 internal GPALHiddenDesktop(
string name)
703 public string Name {
get; }
706 public int WindowCount => HiddenDesktop.WindowCount(Name);
710 public string Peek() => HiddenDesktop.PeekOne(Name, HiddenDesktop.DefaultPeekMs);
715 public string Peek(
int forMs) => HiddenDesktop.PeekOne(Name, forMs);
718 public void Close() => HiddenDesktop.Close(Name);
728 public IReadOnlyList<string> Names => HiddenDesktop.Names;
736 public IHiddenDesktop
this[
string desktopName] =>
new GPALHiddenDesktop(desktopName);
739 public int WindowCount => HiddenDesktop.WindowCount();
742 public bool IsShowing => HiddenDesktop.IsShowing;
746 public string Peek() => HiddenDesktop.Peek(HiddenDesktop.DefaultPeekMs);
751 public string Peek(
int forMsEach) => HiddenDesktop.Peek(forMsEach);
756 public string Peek(
string desktopName) => HiddenDesktop.PeekOne(desktopName, HiddenDesktop.DefaultPeekMs);
760 public bool Return() => HiddenDesktop.Return();
763 public void CloseAll() => HiddenDesktop.CloseAll();
One Win32 desktop object a browser is running on. Reached from the browser that is on it,...
Every hidden desktop this process has made, and the screen itself. Peek and Return move the screen,...