Skip to content

Commit

Permalink
Separate UI thread dedicated for the KeeAgent Plugin UI interactions
Browse files Browse the repository at this point in the history
  • Loading branch information
ExtraClock committed Jan 20, 2024
1 parent e3dd949 commit 4288080
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 9 deletions.
1 change: 1 addition & 0 deletions KeeAgent/KeeAgent.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
<DependentUpon>ManageDialog.cs</DependentUpon>
</Compile>
<Compile Include="KeeAgentExt.cs" />
<Compile Include="KeeAgentInteractiveUi.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UI\OptionsPanel.cs">
<SubType>UserControl</SubType>
Expand Down
19 changes: 10 additions & 9 deletions KeeAgent/KeeAgentExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
using KeePass.Util.Spr;
using KeePassLib;
using KeePassLib.Utility;
using SshAgentLib.Keys;

namespace KeeAgent
{
Expand All @@ -43,6 +42,7 @@ public sealed partial class KeeAgentExt : Plugin
bool saveBeforeCloseQuestionMessageShown = false;
Dictionary<string, KeyFileInfo> keyFileMap = new Dictionary<string, KeyFileInfo>();
KeeAgentColumnProvider columnProvider;
KeeAgentInteractiveUi interactiveUi;

const string pluginNamespace = "KeeAgent";
const string alwaysConfirmOptionName = pluginNamespace + ".AlwaysConfirm";
Expand Down Expand Up @@ -103,6 +103,7 @@ public override bool Initialize(IPluginHost host)
var domainSocketPath =
Environment.GetEnvironmentVariable(UnixClient.SshAuthSockName);
try {
interactiveUi = new KeeAgentInteractiveUi();
if (Options.AgentMode != AgentMode.Client) {
if (isWindows) {
// In windows, try to start an agent. If Pageant is running, we will
Expand Down Expand Up @@ -213,9 +214,7 @@ private bool ConfirmUserPermissionCallback(ISshKey key, Process process, string
string toHost)
{
var result = false;
pluginHost.MainWindow.Invoke(new Action(() =>
result = Default.ConfirmCallback(key, process, user, fromHost, toHost)
));
interactiveUi.Invoke(() => result = Default.ConfirmCallback(key, process, user, fromHost, toHost));
return result;
}

Expand All @@ -237,6 +236,10 @@ public override void Terminate()
// need to shutdown agent or app won't exit
agentModeAgent.Dispose();
}

if (interactiveUi != null) {
interactiveUi.Dispose();
}
}

public override Image SmallIcon {
Expand Down Expand Up @@ -1238,22 +1241,20 @@ IEnumerable<ISshKey> FilterKeyList(IEnumerable<ISshKey> list)
return list;
}

// TODO: Using the main thread here will cause a lockup with IOProtocolExt
pluginHost.MainWindow.Invoke((MethodInvoker)delegate {
interactiveUi.Invoke(() => {
//var zIndex = pluginHost.MainWindow.GetZIndex();
var dialog = new KeyPicker(list);
dialog.Shown += (sender, e) => dialog.Activate();
dialog.StartPosition = FormStartPosition.CenterScreen;
dialog.TopMost = true;
dialog.ShowDialog(pluginHost.MainWindow);
dialog.ShowDialog();
if (dialog.DialogResult == DialogResult.OK) {
list = dialog.SelectedKeys.ToList();
}
else {
list = Enumerable.Empty<ISshKey>();
}

pluginHost.MainWindow.SetWindowPosBottom();
});

return list;
Expand Down
79 changes: 79 additions & 0 deletions KeeAgent/KeeAgentInteractiveUi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.Threading;
using System.Windows.Forms;

namespace KeeAgent
{
/// <summary>
/// Provides a separate UI thread for interactive communication without interlocking with the MainWindow.
/// </summary>
public sealed class KeeAgentInteractiveUi : IDisposable
{
private const string ComponentName = "KeeAgent Interactive UI";
private SynchronizationContext _synchronizationContext;
private ApplicationContext _applicationContext;

public KeeAgentInteractiveUi()
{
var uiThread = new Thread(UiThreadMain) {
Name = ComponentName,
IsBackground = true // active background thread will not block the application shutdown process
};
uiThread.SetApartmentState(ApartmentState.STA);
uiThread.Start();
}

private void UiThreadMain()
{
var mainForm = new Form {
// actually we don't need the window, but there is no other way to capture the SynchronizationContext
Name = ComponentName,
Text = ComponentName,
// some recommendations from https://stackoverflow.com/a/683991
FormBorderStyle = FormBorderStyle.FixedToolWindow, // to exclude from appearing in Alt-Tab
ShowInTaskbar = false, // to exclude from the Taskbar
};

mainForm.Load += (sender, args) => {
// capture SynchronizationContext to send messages to the message loop
_synchronizationContext = SynchronizationContext.Current;
// hide the form... we have to postpone the call, otherwise it stays visible
_synchronizationContext.Post(_ => mainForm.Hide(), null);
};

_applicationContext = new ApplicationContext(mainForm);
Application.Run(_applicationContext);
}

/// <summary>
/// Performs message loop shutdown.
/// </summary>
public void Dispose()
{
if (_synchronizationContext == null) {
return;
}

var synchronizationContext = _synchronizationContext;
_synchronizationContext = null;
synchronizationContext.Post(_ => _applicationContext.ExitThread(), null);
}

/// <summary>
/// Invokes a delegate on a separate UI Thread, dedicated for the KeeAgent Plugin
/// </summary>
/// <param name="action">The delegate to call.</param>
/// <exception cref="InvalidOperationException">The method was called in while the SynchronizationContext is not
/// captured yet or have been disposed already</exception>
public void Invoke(Action action)
{
if (_synchronizationContext == null) {
throw new InvalidOperationException(
"KeeAgentInteractiveUi: the SynchronizationContext is not captured yet or have been disposed already");
}

_synchronizationContext.Send(_ => action.Invoke(), null);
}
}
}

0 comments on commit 4288080

Please sign in to comment.