Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: watcher process for cleaning up after crashes #360

Merged
merged 17 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/GlazeWM.App/bin/Debug/net7-windows10.0.17763.0/GlazeWM.dll",
"program": "${workspaceFolder}/GlazeWM.App/bin/Debug/net7-windows10.0.17763.0/GlazeWM.exe",
"args": [],
"cwd": "${workspaceFolder}/GlazeWM.App",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
Expand Down
39 changes: 10 additions & 29 deletions GlazeWM.App.Cli/CliStartup.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Text.Json;
using GlazeWM.Domain.Common;
using GlazeWM.Infrastructure.Common;
using GlazeWM.Infrastructure.Utils;

namespace GlazeWM.App.Cli
{
Expand All @@ -11,55 +10,37 @@ public static async Task<ExitCode> Run(
int ipcServerPort,
bool isSubscribeMessage)
{
var client = new WebSocketClient(ipcServerPort);
var client = new IpcClient(ipcServerPort);

try
{
await client.ConnectAsync(CancellationToken.None);

var clientMessage = string.Join(" ", args);
await client.SendTextAsync(clientMessage, CancellationToken.None);
await client.ConnectAsync();

// Wait for server to respond with a message.
var serverResponse = await client.ReceiveTextAsync(CancellationToken.None);
var clientMessage = string.Join(" ", args);
var firstResponse = await client.SendAndWaitReplyAsync(clientMessage);

// Exit on first message received when not subscribing to an event.
if (!isSubscribeMessage)
{
var parsedMessage = ParseMessage(serverResponse);
Console.WriteLine(parsedMessage);
await client.DisconnectAsync(CancellationToken.None);
Console.WriteLine(firstResponse);
await client.DisconnectAsync();
return ExitCode.Success;
}

// When subscribing to events, continuously listen for server messages.
while (true)
{
var message = await client.ReceiveTextAsync(CancellationToken.None);
var parsedMessage = ParseMessage(message);
Console.WriteLine(parsedMessage);
var eventResponse = await client.ReceiveAsync();
Console.WriteLine(eventResponse);
}
}
catch (Exception exception)
{
Console.Error.WriteLine(exception.Message);
await client.DisconnectAsync(CancellationToken.None);
await client.DisconnectAsync();
return ExitCode.Error;
}
}

/// <summary>
/// Parse JSON in server message.
/// </summary>
private static string ParseMessage(string message)
{
var parsedMessage = JsonDocument.Parse(message).RootElement;
var error = parsedMessage.GetProperty("error").GetString();

if (error is not null)
throw new Exception(error);

return parsedMessage.GetProperty("data").ToString();
}
}
}
13 changes: 13 additions & 0 deletions GlazeWM.App.Watcher/GlazeWM.App.Watcher.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows10.0.17763</TargetFramework>
<OutputType>Library</OutputType>
<DebugType>embedded</DebugType>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\GlazeWM.Domain\GlazeWM.Domain.csproj" />
<ProjectReference Include="..\GlazeWM.Infrastructure\GlazeWM.Infrastructure.csproj" />
</ItemGroup>
</Project>
96 changes: 96 additions & 0 deletions GlazeWM.App.Watcher/WatcherStartup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GlazeWM.Domain.Common;
using GlazeWM.Infrastructure.Common;
using static GlazeWM.Infrastructure.WindowsApi.WindowsApiService;

namespace GlazeWM.App.Watcher
{
public sealed class WatcherStartup
{
public static async Task<ExitCode> Run(int ipcServerPort)
{
var client = new IpcClient(ipcServerPort);
var managedHandles = new List<IntPtr>();

try
{
await client.ConnectAsync();

// Get window handles that are initially managed on startup.
foreach (var handle in await GetInitialHandles(client))
managedHandles.Add(handle);

// Subscribe to manage + unmanage window events.
_ = await client.SendAndWaitReplyAsync(
"subscribe -e window_managed,window_unmanaged"
);

// Continuously listen for manage + unmanage events.
while (true)
{
var (isManaged, handle) = await GetManagedEvent(client);

if (isManaged)
managedHandles.Add(handle);
else
managedHandles.Remove(handle);
}
}
catch (Exception)
{
// Restore managed handles on failure to communicate with the main process'
// IPC server.
RestoreHandles(managedHandles);
await client.DisconnectAsync();
return ExitCode.Success;
}
}

/// <summary>
/// Query for initial window handles via IPC server.
/// </summary>
private static async Task<IEnumerable<IntPtr>> GetInitialHandles(IpcClient client)
{
var response = await client.SendAndWaitReplyAsync("windows");

return response
.EnumerateArray()
.Select(value => new IntPtr(value.GetInt64()));
}

/// <summary>
/// Get window handles from managed and unmanaged window events.
/// </summary>
/// <returns>Tuple of whether the handle is managed, and the handle itself</returns>
private static async Task<(bool, IntPtr)> GetManagedEvent(IpcClient client)
{
var response = await client.ReceiveAsync();

return response.GetProperty("friendlyName").GetString() switch
{
"window_managed" => (
true,
new IntPtr(response.GetProperty("window").GetProperty("handle").GetInt64())
),
"window_unmanaged" => (
false,
new IntPtr(response.GetProperty("removedHandle").GetInt64())
),
_ => throw new Exception("Received unrecognized event.")
};
}

/// <summary>
/// Restore given window handles.
/// </summary>
private static void RestoreHandles(List<IntPtr> handles)
{
foreach (var handle in handles)
// TODO: Change this.
ShowWindow(handle, ShowWindowFlags.ShowDefault);
}
}
}
1 change: 1 addition & 0 deletions GlazeWM.App/GlazeWM.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ItemGroup>
<ProjectReference Include="..\GlazeWM.App.Cli\GlazeWM.App.Cli.csproj" />
<ProjectReference Include="..\GlazeWM.App.IpcServer\GlazeWM.App.IpcServer.csproj" />
<ProjectReference Include="..\GlazeWM.App.Watcher\GlazeWM.App.Watcher.csproj" />
<ProjectReference Include="..\GlazeWM.App.WindowManager\GlazeWM.App.WindowManager.csproj" />
<ProjectReference Include="..\GlazeWM.Bar\GlazeWM.Bar.csproj" />
<ProjectReference Include="..\GlazeWM.Domain\GlazeWM.Domain.csproj" />
Expand Down
19 changes: 19 additions & 0 deletions GlazeWM.App/Program.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using GlazeWM.App.Cli;
using GlazeWM.App.IpcServer;
using GlazeWM.App.IpcServer.Messages;
using GlazeWM.App.Watcher;
using GlazeWM.App.WindowManager;
using GlazeWM.Bar;
using GlazeWM.Domain;
Expand Down Expand Up @@ -49,6 +52,7 @@ public static async Task<int> Main(string[] args)

var parsedArgs = Parser.Default.ParseArguments<
WmStartupOptions,
WatcherStartupOptions,
InvokeCommandMessage,
SubscribeMessage,
GetMonitorsMessage,
Expand All @@ -59,6 +63,7 @@ public static async Task<int> Main(string[] args)
var exitCode = parsedArgs.Value switch
{
WmStartupOptions options => StartWm(options, isSingleInstance),
WatcherStartupOptions => await StartWatcher(isSingleInstance),
InvokeCommandMessage or
GetMonitorsMessage or
GetWorkspacesMessage or
Expand Down Expand Up @@ -90,6 +95,9 @@ private static ExitCode StartWm(WmStartupOptions options, bool isSingleInstance)
ThreadUtils.CreateSTA("GlazeWMBar", barService.StartApp);
ThreadUtils.Create("GlazeWMIPC", () => ipcServerStartup.Run(IpcServerPort));

// Spawn separate watcher process for cleaning up after crashes.
Process.Start(Environment.ProcessPath, "watcher");

// Run the window manager on the main thread.
return wmStartup.Run();
}
Expand All @@ -108,6 +116,17 @@ private static async Task<ExitCode> StartCli(
return await CliStartup.Run(args, IpcServerPort, isSubscribeMessage);
}

private static async Task<ExitCode> StartWatcher(bool isSingleInstance)
{
if (isSingleInstance)
{
Console.Error.WriteLine("No running instance found. Cannot start watcher.");
return ExitCode.Error;
}

return await WatcherStartup.Run(IpcServerPort);
}

private static ExitCode ExitWithError(Error error)
{
Console.Error.WriteLine($"Failed to parse startup arguments: {error}.");
Expand Down
94 changes: 94 additions & 0 deletions GlazeWM.Domain/Common/IpcClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace GlazeWM.Domain.Common
{
public class IpcClient : IDisposable
{
private readonly ClientWebSocket _ws = new();
private readonly int _port;

public IpcClient(int port)
{
_port = port;
}

public async Task ConnectAsync()
{
await _ws.ConnectAsync(
new Uri($"ws://localhost:{_port}"),
CancellationToken.None
);
}

public async Task<JsonElement> ReceiveAsync()
{
var buffer = new byte[1024 * 4];

// Continuously listen until a text message is received.
while (true)
{
var result = await _ws.ReceiveAsync(
new ArraySegment<byte>(buffer),
CancellationToken.None
);

if (result.MessageType != WebSocketMessageType.Text)
continue;

var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
return ParseMessage(message);
}
}

private async Task SendTextAsync(string message)
{
var messageBytes = Encoding.UTF8.GetBytes(message);

await _ws.SendAsync(
new ArraySegment<byte>(messageBytes),
WebSocketMessageType.Text,
true,
CancellationToken.None
);
}

public async Task<JsonElement> SendAndWaitReplyAsync(string message)
{
await SendTextAsync(message);
return await ReceiveAsync();
}

/// <summary>
/// Parse JSON in server message.
/// </summary>
private static JsonElement ParseMessage(string message)
{
var parsedMessage = JsonDocument.Parse(message).RootElement;
var error = parsedMessage.GetProperty("error").GetString();

if (error is not null)
throw new Exception(error);

return parsedMessage.GetProperty("data");
}

public async Task DisconnectAsync()
{
await _ws.CloseAsync(
WebSocketCloseStatus.NormalClosure,
null,
CancellationToken.None
);
}

public void Dispose()
{
_ws.Dispose();
}
}
}
9 changes: 9 additions & 0 deletions GlazeWM.Domain/Common/WatcherStartupOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using CommandLine;

namespace GlazeWM.Domain.Common
{
[Verb("watcher")]
public class WatcherStartupOptions
{
}
}
6 changes: 4 additions & 2 deletions GlazeWM.Domain/Windows/CommandHandlers/ManageWindowHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using GlazeWM.Domain.Monitors;
using GlazeWM.Domain.UserConfigs;
using GlazeWM.Domain.Windows.Commands;
using GlazeWM.Domain.Windows.Events;
using GlazeWM.Domain.Workspaces;
using GlazeWM.Infrastructure.Bussing;
using GlazeWM.Infrastructure.WindowsApi;
Expand Down Expand Up @@ -52,8 +53,6 @@ public CommandResponse Handle(ManageWindowCommand command)
// Create the window instance.
var window = CreateWindow(windowHandle, targetParent);

_logger.LogWindowEvent("New window managed", window);

if (window is IResizable)
_bus.Invoke(new AttachAndResizeContainerCommand(window, targetParent, targetIndex));
else
Expand Down Expand Up @@ -86,6 +85,9 @@ public CommandResponse Handle(ManageWindowCommand command)
// Set OS focus to the newly added window in case it's not already focused.
_bus.Invoke(new SetNativeFocusCommand(window));

_logger.LogWindowEvent("New window managed", window);
_bus.Emit(new WindowManagedEvent(window));

return CommandResponse.Ok;
}

Expand Down
Loading