Skip to content

Commit

Permalink
Instance communication via Pipe
Browse files Browse the repository at this point in the history
  • Loading branch information
valnoxy committed Aug 23, 2024
1 parent bc04f43 commit 29e3fd1
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 111 deletions.
35 changes: 32 additions & 3 deletions GoAwayEdge/Common/Configuration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Windows;
using GoAwayEdge.Common.Debugging;
using ManagedShell;
Expand Down Expand Up @@ -73,7 +75,14 @@ public static bool InitialEnvironment()
if (NoEdgeInstalled)
return true;

ShellManager = new ShellManager();
if (!CopilotDockPipeAvailable())
{
try { ShellManager = new ShellManager(); }
catch (Exception ex)
{
Logging.Log("An error has occurred while initializing the ShellManager: " + ex.Message, Logging.LogLevel.ERROR);
}
}

FileConfiguration.EdgePath = RegistryConfig.GetKey("EdgeFilePath");
FileConfiguration.NonIfeoPath = RegistryConfig.GetKey("EdgeNonIEFOFilePath");
Expand All @@ -91,9 +100,9 @@ public static bool InitialEnvironment()
CustomProviderUrl = RegistryConfig.GetKey("CustomProviderUrl", userSetting: true);
}
}
catch
catch (Exception ex)
{
// ignored
Logging.Log("An error has occurred while reading the registry: " + ex.Message, Logging.LogLevel.ERROR);
}
return true;
}
Expand All @@ -106,6 +115,26 @@ public static bool InitialEnvironment()
}
}

public static bool CopilotDockPipeAvailable()
{
try
{
using var pipeClient = new NamedPipeClientStream(".", "CopilotDockPipe", PipeDirection.In);
pipeClient.Connect(1000);
return true;
}
catch (TimeoutException)
{
// Pipe does not exist or is not available
return false;
}
catch (Exception ex)
{
Console.WriteLine($"Failed to check for the pipe 'CopilotDockPipe': {ex.Message}");
return false;
}
}

/// <summary>
/// Get a list of all available Edge Channels.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion GoAwayEdge/UserInterface/CopilotDock/CopilotDock.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

<StackPanel Grid.Row="0">
<!-- This needs a custom taskbar -->
<ui:TextBlock Text="GoAwayEdge - Copilot Dock" FontSize="14" Margin="16,16,0,0"/>
<ui:TextBlock Text="GoAwayEdge - Copilot Dock" Foreground="{DynamicResource TextFillColorPrimaryBrush}" FontSize="14" Margin="16,16,0,0"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,-25,16,0">
<ui:Button x:Name="DockButton" Icon="{ui:SymbolIcon Pin28}" FontSize="16" Margin="0,0,8,0" Click="DockButton_OnClick"/>
<ui:Button x:Name="CloseButton" Icon="{ui:SymbolIcon ArrowExit20}" FontSize="16" Click="CloseButton_OnClick"/>
Expand Down
17 changes: 8 additions & 9 deletions GoAwayEdge/UserInterface/CopilotDock/CopilotDock.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using GoAwayEdge.Common;
using GoAwayEdge.Common.Debugging;
Expand All @@ -17,15 +16,19 @@ namespace GoAwayEdge.UserInterface.CopilotDock
/// </summary>
public partial class CopilotDock
{
public static CopilotDock? Instance;
public CopilotDock(ShellManager shellManager, AppBarScreen screen, AppBarEdge edge, double desiredHeight, AppBarMode mode)
: base(shellManager.AppBarManager, shellManager.ExplorerHelper, shellManager.FullScreenHelper, screen, edge, mode, desiredHeight)
{
this.MaxHeight = SystemParameters.WorkArea.Height;
this.MinHeight = SystemParameters.WorkArea.Height;
Configuration.AppBarIsAttached = mode != AppBarMode.None;
Instance = this;

InitializeComponent();
_ = InitializeWebViewAsync();

DockButton.Icon = Configuration.AppBarIsAttached ? new SymbolIcon(SymbolRegular.PinOff28) : new SymbolIcon(SymbolRegular.Pin28);
}

private async Task InitializeWebViewAsync()
Expand Down Expand Up @@ -70,17 +73,17 @@ private void CloseButton_OnClick(object sender, RoutedEventArgs e)

private void DockButton_OnClick(object sender, RoutedEventArgs e)
{
Configuration.ShellManager.AppBarManager.RegisterBar(this, Width, Height, AppBarEdge.Right);

if (Configuration.AppBarIsAttached)
{
DockButton.Icon = new SymbolIcon(SymbolRegular.Pin28);
RegistryConfig.SetKey("CopilotDockState", "Detached", userSetting: true);
this.AppBarMode = AppBarMode.None;
}
else
{
DockButton.Icon = new SymbolIcon(SymbolRegular.PinOff28);
RegistryConfig.SetKey("CopilotDockState", "Docked", userSetting: true);
this.AppBarMode = AppBarMode.Normal;
}
Configuration.AppBarIsAttached = !Configuration.AppBarIsAttached;
}
Expand All @@ -92,13 +95,9 @@ private void CopilotDock_OnDeactivated(object? sender, EventArgs e)
var currentTitle = currentProcess.MainWindowTitle;
var currentId = currentProcess.Id;
Logging.Log($"Deactivated CopilotDock (ID: {currentId}, Title: {currentTitle})", Logging.LogLevel.INFO);
Hide();
this.Visibility = Visibility.Collapsed;
}


[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
private const int SW_HIDE = 0;
public void ShowWindow() => this.Visibility = Visibility.Visible;
}
}
197 changes: 99 additions & 98 deletions GoAwayEdge/UserInterface/CopilotDock/InterfaceManager.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.IO.Pipes;
using System.IO;
using System.Windows;
using GoAwayEdge.Common;
using GoAwayEdge.Common.Debugging;
using ManagedShell.AppBar;
Expand All @@ -9,6 +9,11 @@ namespace GoAwayEdge.UserInterface.CopilotDock
{
public class InterfaceManager
{
private static AppBarWindow? _dockWindow;
private static NamedPipeServerStream? _pipeServer;
private static Thread? _pipeThread;
private static bool _closed = false;

public static void ShowDock()
{
using var mutex = new Mutex(true, "GoAwayEdge_CopilotDock", out var createdNew);
Expand Down Expand Up @@ -37,21 +42,30 @@ public static void ShowDock()
RegistryConfig.SetKey("CopilotDockState", "Docked", userSetting: true);
}

var closed = false;
var copilotDock = new CopilotDock(
_dockWindow = new CopilotDock(
Configuration.ShellManager,
AppBarScreen.FromPrimaryScreen(),
AppBarEdge.Right,
500, // temporary size
mode);
copilotDock.Closed += (sender, args) => closed = true;
copilotDock.ShowDialog();

while (!closed)
_dockWindow.Closed += (_, _) =>
{
_closed = true;
StopNamedPipeServer(); // Stop the pipe when the dock is closed
};
_dockWindow.ShowDialog();

// Dock is inactive, going now into loop...
while (!_closed)
{
StartNamedPipeServer(); // Start pipe server if the dock is inactive
Thread.Sleep(1000);
}

// Dock was closed
Logging.Log("Closed CopilotDock");
Environment.Exit(0);
}
else
{
Expand All @@ -60,112 +74,99 @@ public static void ShowDock()
}
}

private static void BringToFront()
private static void StartNamedPipeServer()
{
var currentProcess = Process.GetCurrentProcess();
var currentTitle = currentProcess.MainWindowTitle;
var currentId = currentProcess.Id;
var processes = Process.GetProcessesByName(currentProcess.ProcessName);
foreach (var process in processes)
if (_pipeServer != null) return;

_pipeThread = new Thread(() =>
{
if (process.Id != currentProcess.Id)
try
{
if (process.MainWindowHandle == IntPtr.Zero)
_pipeServer = new NamedPipeServerStream("CopilotDockPipe", PipeDirection.InOut);
Logging.Log("Named Pipe Server opened...");
while (true)
{
var handle = GetWindowHandle(process.Id, process.MainWindowTitle);
if (handle != IntPtr.Zero)
_pipeServer.WaitForConnection();
using (var reader = new StreamReader(_pipeServer))
{
// show window
ShowWindow(handle, 5);
// send WM_SHOWWINDOW message to toggle the visibility flag
SendMessage(handle, WM_SHOWWINDOW, IntPtr.Zero, new IntPtr(SW_PARENTOPENING));
var message = reader.ReadLine();
if (message == "BringToFront")
{
Logging.Log("Received 'BringToFront' command via Named Pipe.");
try
{
Application.Current.Dispatcher.Invoke(() =>
{
CopilotDock.Instance!.Visibility = Visibility.Visible;
try
{
_dockWindow.Visibility = Visibility.Visible;

Check warning on line 107 in GoAwayEdge/UserInterface/CopilotDock/InterfaceManager.cs

View workflow job for this annotation

GitHub Actions / build (Debug)

Dereference of a possibly null reference.

Check warning on line 107 in GoAwayEdge/UserInterface/CopilotDock/InterfaceManager.cs

View workflow job for this annotation

GitHub Actions / build (Debug)

Dereference of a possibly null reference.

Check warning on line 107 in GoAwayEdge/UserInterface/CopilotDock/InterfaceManager.cs

View workflow job for this annotation

GitHub Actions / build (Release)

Dereference of a possibly null reference.

Check warning on line 107 in GoAwayEdge/UserInterface/CopilotDock/InterfaceManager.cs

View workflow job for this annotation

GitHub Actions / build (Release)

Dereference of a possibly null reference.
_dockWindow.Activate();
}
catch (Exception ex)
{
Logging.Log("Stage 2: Failed to bring CopilotDock to the front: " + ex.Message, Logging.LogLevel.ERROR);
}
});
}
catch (Exception ex)
{
Logging.Log("Stage 1: Failed to bring CopilotDock to the front: " + ex.Message, Logging.LogLevel.ERROR);
}
}
}
_pipeServer.Disconnect();
}
}
}
}

// "stolen" from StackOverflow >:)
// https://stackoverflow.com/questions/21154693/activate-a-hidden-wpf-application-when-trying-to-run-a-second-instance
const int GWL_EXSTYLE = (-20);
const uint WS_EX_APPWINDOW = 0x40000;

const uint WM_SHOWWINDOW = 0x0018;
const int SW_PARENTOPENING = 3;

[DllImport("user32.dll")]
private static extern Boolean ShowWindow(IntPtr hWnd, Int32 nCmdShow);

[DllImport("user32.dll")]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);

[DllImport("user32.dll")]
private static extern bool EnumDesktopWindows(IntPtr hDesktop, EnumWindowsProc ewp, int lParam);

[DllImport("user32.dll")]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);

[DllImport("user32.dll")]
private static extern uint GetWindowTextLength(IntPtr hWnd);

[DllImport("user32.dll")]
private static extern uint GetWindowText(IntPtr hWnd, StringBuilder lpString, uint nMaxCount);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern bool GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
catch (Exception ex)
{
Logging.Log("Error in Named Pipe Server: " + ex.Message, Logging.LogLevel.ERROR);
}
finally
{
_pipeServer?.Dispose();
_pipeServer = null;
}
});

[DllImport("user32.dll")]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);
_pipeThread.IsBackground = true;
_pipeThread.Start();
}

delegate bool EnumWindowsProc(IntPtr hWnd, int lParam);

static bool IsApplicationWindow(IntPtr hWnd)
private static void StopNamedPipeServer()
{
return (GetWindowLong(hWnd, GWL_EXSTYLE) & WS_EX_APPWINDOW) != 0;
if (_pipeServer != null)
{
_pipeServer.Disconnect();
_pipeServer.Dispose();
_pipeServer = null;
}

if (_pipeThread is not { IsAlive: true }) return;
_pipeThread.Join();
_pipeThread = null;
}

static IntPtr GetWindowHandle(int pid, string title)
private static void BringToFront()
{
var result = IntPtr.Zero;

EnumWindowsProc enumerateHandle = delegate (IntPtr hWnd, int lParam)
using var pipeClient = new NamedPipeClientStream(".", "CopilotDockPipe", PipeDirection.Out);
try
{
int id;
GetWindowThreadProcessId(hWnd, out id);

if (pid == id)
{
var clsName = new StringBuilder(256);
var hasClass = GetClassName(hWnd, clsName, 256);
if (hasClass)
{

var maxLength = (int)GetWindowTextLength(hWnd);
var builder = new StringBuilder(maxLength + 1);
GetWindowText(hWnd, builder, (uint)builder.Capacity);

var text = builder.ToString();
var className = clsName.ToString();

// There could be multiple handle associated with our pid,
// so we return the first handle that satisfy:
// 1) the handle title/ caption matches our window title,
// 2) the window class name starts with HwndWrapper (WPF specific)
// 3) the window has WS_EX_APPWINDOW style

if (title == text && className.StartsWith("HwndWrapper") && IsApplicationWindow(hWnd))
{
result = hWnd;
return false;
}
}
}
return true;
};

EnumDesktopWindows(IntPtr.Zero, enumerateHandle, 0);

return result;
pipeClient.Connect(1000);
using var writer = new StreamWriter(pipeClient);
writer.WriteLine("BringToFront");
writer.Flush();
}
catch (Exception ex)
{
Logging.Log("Failed to connect to CopilotDock pipe: " + ex.Message, Logging.LogLevel.ERROR);
}
}
}
}

0 comments on commit 29e3fd1

Please sign in to comment.