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))
237 [DllImport(
"user32.dll", SetLastError =
true)]
238 private static extern int GetWindowThreadProcessId(IntPtr window, out
int processId);
240 [DllImport(
"user32.dll", SetLastError =
true)]
241 private static extern bool PostMessage(IntPtr window, uint message, IntPtr wParam, IntPtr lParam);
243 private const uint WM_CLOSE = 0x0010;
255 internal static bool CloseWindowsOf(
string name,
int processId)
258 IntPtr handle = HandleFor(name);
260 if (IntPtr.Zero != handle)
261 EnumDesktopWindows(handle, (window, param) =>
263 GetWindowThreadProcessId(window, out
int owner);
265 if (processId == owner &&
true == IsWindowVisible(window))
266 retVal = PostMessage(window, WM_CLOSE, IntPtr.Zero, IntPtr.Zero) || retVal;
285 internal static IntPtr MainWindowOf(
string name,
int processId)
287 IntPtr retVal = IntPtr.Zero;
288 IntPtr handle = HandleFor(name);
290 if (IntPtr.Zero != handle)
291 EnumDesktopWindows(handle, (window, param) =>
293 GetWindowThreadProcessId(window, out
int owner);
295 if (processId == owner &&
true == IsWindowVisible(window))
298 return IntPtr.Zero == retVal;
314 internal static List<IntPtr> WindowsOn(
string name)
316 List<IntPtr> retVal =
new List<IntPtr>();
317 IntPtr handle = HandleFor(name);
319 if (IntPtr.Zero != handle)
320 EnumDesktopWindows(handle, (window, param) =>
322 if (
true == IsWindowVisible(window))
343 internal static IntPtr WaitForWindowOn(
string name, Func<string, bool> titleMatches,
int waitTimeInSeconds)
345 IntPtr retVal = IntPtr.Zero;
346 IntPtr handle = HandleFor(name);
348 for (
int second = 0; second < waitTimeInSeconds && IntPtr.Zero == retVal && IntPtr.Zero != handle; second++)
352 EnumDesktopWindows(handle, (window, param) =>
354 if (
true == IsWindowVisible(window) &&
true == titleMatches(GenerallyPositive.GPALForm.FormHelper.TitleOf(window)))
357 return IntPtr.Zero == retVal;
369 internal static int WindowCount()
371 return Names.Sum(name => WindowCount(name));
382 internal static bool HasWindows(
string name,
int timeoutInMs)
385 DateTime giveUpAt = DateTime.Now.AddMilliseconds(timeoutInMs);
387 while (
false == retVal && DateTime.Now < giveUpAt)
389 retVal = 0 < WindowCount(name);
401 internal static bool IsShowing
406 return IntPtr.Zero != cameFrom;
419 internal static bool Return()
427 cameFrom = IntPtr.Zero;
430 if (IntPtr.Zero != back)
432 retVal = SwitchDesktop(back);
458 internal static string Peek(
int forMsEach)
460 List<KeyValuePair<string, IntPtr>> walk;
463 walk = desktops.Where(desktop => 0 < WindowCount(desktop.Key)).ToList();
465 return Walk(walk, forMsEach,
"there is nothing on any hidden desktop to look at");
475 internal static string PeekOne(
string name,
int forMs)
477 List<KeyValuePair<string, IntPtr>> walk;
480 walk = desktops.Where(desktop => desktop.Key == name && 0 < WindowCount(desktop.Key)).ToList();
482 return Walk(walk, forMs, $
"there is nothing on the hidden desktop [{name}] to look at");
492 private static string Walk(List<KeyValuePair<string, IntPtr>> walk,
int forMsEach,
string nothingToSee)
494 string retVal =
null;
497 retVal = nothingToSee;
498 else if (
true == IsShowing)
499 retVal =
"the screen is already on a hidden desktop";
505 IntPtr from = OpenInputDesktop(0,
false, DesktopSwitchDesktop);
507 if (IntPtr.Zero == from)
508 retVal = $
"no handle on the desktop you are on [{Marshal.GetLastWin32Error()}], so nothing was switched";
516 Process guard = DesktopGuard.Start(0 < forMsEach ? forMsEach * walk.Count : 0);
520 Thread viewer =
new Thread(() =>
524 foreach (KeyValuePair<string, IntPtr> desktop
in walk)
528 using (Escape escape = Escape.ShowOn(desktop.Value, desktop.Key, forMsEach))
530 if (
false == SwitchDesktop(desktop.Value))
533 if (
true == escape.WaitToLeave(forMsEach))
541 DesktopGuard.Stop(guard);
545 viewer.IsBackground =
false;
551 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Cannot look at the hidden desktops: {retVal}",
null, GPALObjectType.Browser);
566 internal static void CloseAll()
573 foreach (IntPtr handle
in desktops.Values)
574 CloseDesktop(handle);
596 internal static void CloseEverythingOn(
string name)
598 foreach (IntPtr window
in WindowsOn(name))
599 PostMessage(window, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
601 DateTime giveUpAt = DateTime.Now.AddMilliseconds(CLOSING_MS);
603 while (0 < WindowCount(name) && DateTime.Now < giveUpAt)
604 Thread.Sleep(BETWEEN_LOOKS_MS);
606 foreach (IntPtr window
in WindowsOn(name))
608 GetWindowThreadProcessId(window, out
int owner);
611 try { Process.GetProcessById(owner).Kill(); }
612 catch (Exception) { }
619 private const int CLOSING_MS = 5000;
622 private const int BETWEEN_LOOKS_MS = 200;
624 internal static void Close(
string name)
627 if (
true == desktops.TryGetValue(name, out IntPtr handle))
629 CloseDesktop(handle);
630 desktops.Remove(name);
639 private static IntPtr HandleFor(
string name)
644 desktops.TryGetValue(name, out retVal);
664 private sealed class Escape : IDisposable
666 private const int HotKeyId = 0xE5CA;
667 private const uint VkEscape = 0x1B;
668 private const uint ModNoRepeat = 0x4000;
672 private const int BlindHoldInMs = 15_000;
677 private const int MaxHoldInMs = 600_000;
679 private readonly ManualResetEvent leaving =
new ManualResetEvent(
false);
680 private readonly ManualResetEvent ready =
new ManualResetEvent(
false);
681 private readonly Thread thread;
683 private volatile EscapeForm form;
684 private volatile bool live;
686 private Escape(IntPtr desktop,
string name,
int forMs)
688 thread =
new Thread(() =>
690 if (
false == SetThreadDesktop(desktop))
694 EscapeForm showing =
new EscapeForm(name, Leave, 0 < forMs ? forMs : MaxHoldInMs);
696 showing.Shown += (sender, args) =>
704 System.Windows.Forms.Application.Run(showing);
708 thread.IsBackground =
true;
709 thread.SetApartmentState(ApartmentState.STA);
721 internal static Escape ShowOn(IntPtr desktop,
string name,
int forMs)
723 Escape retVal =
new Escape(desktop, name, forMs);
725 retVal.ready.WaitOne(5_000);
727 if (
false == retVal.live)
728 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No way out could be put on the hidden desktop [{name}], so the clock is the only way back",
729 null, GPALObjectType.Browser);
739 internal bool WaitToLeave(
int forMs)
742 int limit = 0 < forMs ? forMs : (
true == live ? MaxHoldInMs : BlindHoldInMs);
744 retVal = leaving.WaitOne(limit);
750 public void Dispose()
752 EscapeForm closing = form;
754 if (
null != closing &&
true == closing.IsHandleCreated)
755 closing.BeginInvoke(
new Action(() => closing.Close()));
774 private sealed class EscapeForm : Form
776 private const int WmHotKey = 0x0312;
778 private readonly Action leave;
781 private readonly System.Windows.Forms.Timer countdown =
new System.Windows.Forms.Timer { Interval = 1_000 };
783 internal EscapeForm(
string desktop, Action leave,
int holdMs)
787 Rectangle screen = Screen.PrimaryScreen.Bounds;
789 FormBorderStyle = FormBorderStyle.None;
790 StartPosition = FormStartPosition.Manual;
791 ShowInTaskbar =
false;
793 BackColor = Color.FromArgb(24, 24, 24);
794 ForeColor = Color.White;
795 Padding =
new Padding(6);
796 Size =
new Size(300, 62);
800 Location =
new Point(screen.Left + 12, screen.Bottom - Height - 12);
803 Button back =
new Button
805 Text =
"Return (Esc)",
806 Dock = DockStyle.Fill,
807 FlatStyle = FlatStyle.System,
808 Font =
new Font(
"Segoe UI", 9F, FontStyle.Bold),
811 Label caption =
new Label
814 Dock = DockStyle.Top,
816 TextAlign = ContentAlignment.MiddleCenter,
817 Font =
new Font(
"Segoe UI", 8F, FontStyle.Regular),
820 back.Click += (sender, args) => this.leave();
823 DateTime returnAt = DateTime.MinValue;
825 Shown += (sender, args) =>
827 returnAt = DateTime.Now.AddMilliseconds(holdMs);
828 caption.Text = CaptionFor(desktop, returnAt);
832 countdown.Tick += (sender, args) => caption.Text = CaptionFor(desktop, returnAt);
836 Controls.Add(caption);
838 KeyDown += (sender, args) =>
840 if (Keys.Escape == args.KeyCode)
845 protected override void OnHandleCreated(EventArgs e)
847 base.OnHandleCreated(e);
851 if (
false == RegisterHotKey(Handle, HotKeyId, ModNoRepeat, VkEscape))
852 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Escape could not be taken on the hidden desktop, so the button is the way out",
853 null, GPALObjectType.Browser,
new Win32Exception(Marshal.GetLastWin32Error()));
857 private static string CaptionFor(
string desktop, DateTime returnAt)
859 TimeSpan left = returnAt - DateTime.Now;
861 if (TimeSpan.Zero > left)
862 left = TimeSpan.Zero;
864 return $
"{desktop} returning in {(int)left.TotalMinutes}:{left.Seconds:00}";
867 protected override void OnHandleDestroyed(EventArgs e)
871 UnregisterHotKey(Handle, HotKeyId);
873 base.OnHandleDestroyed(e);
876 protected override void WndProc(ref Message m)
878 if (WmHotKey == m.Msg && HotKeyId == m.WParam.ToInt32())
894 internal GPALHiddenDesktop(
string name)
900 public string Name {
get; }
903 public int WindowCount => HiddenDesktop.WindowCount(Name);
907 public string Peek() => HiddenDesktop.PeekOne(Name, HiddenDesktop.DefaultPeekMs);
912 public string Peek(
int forMs) => HiddenDesktop.PeekOne(Name, forMs);
915 public void Close() => HiddenDesktop.Close(Name);
921 public void CloseEverything() => HiddenDesktop.CloseEverythingOn(Name);
931 public IReadOnlyList<string> Names => HiddenDesktop.Names;
939 public IHiddenDesktop
this[
string desktopName] =>
new GPALHiddenDesktop(desktopName);
942 public int WindowCount => HiddenDesktop.WindowCount();
945 public bool IsShowing => HiddenDesktop.IsShowing;
949 public string Peek() => HiddenDesktop.Peek(HiddenDesktop.DefaultPeekMs);
954 public string Peek(
int forMsEach) => HiddenDesktop.Peek(forMsEach);
959 public string Peek(
string desktopName) => HiddenDesktop.PeekOne(desktopName, HiddenDesktop.DefaultPeekMs);
963 public bool Return() => HiddenDesktop.Return();
966 public void CloseAll() => HiddenDesktop.CloseAll();
971 public void CloseEverything()
973 foreach (
string name
in HiddenDesktop.Names.ToList())
974 HiddenDesktop.CloseEverythingOn(name);
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,...