-
Notifications
You must be signed in to change notification settings - Fork 186
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
15ca061
commit 444ea5c
Showing
56 changed files
with
1,242 additions
and
481 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
using System.Reactive.Linq; | ||
using System.Text.Json; | ||
using GlazeWM.Infrastructure.Common; | ||
using GlazeWM.Infrastructure.Utils; | ||
|
||
namespace GlazeWM.App.Cli | ||
{ | ||
public sealed class CliStartup | ||
{ | ||
public static async Task<ExitCode> Run( | ||
string[] args, | ||
int ipcServerPort, | ||
bool isSubscribeMessage) | ||
{ | ||
var client = new WebsocketClient(ipcServerPort); | ||
|
||
try | ||
{ | ||
var isConnected = client.Connect(); | ||
|
||
if (!isConnected) | ||
throw new Exception("Unable to connect to IPC server."); | ||
|
||
client.ReceiveAsync(); | ||
|
||
var message = string.Join(" ", args); | ||
var sendSuccess = client.SendTextAsync(message); | ||
|
||
if (!sendSuccess) | ||
throw new Exception("Failed to send message to IPC server."); | ||
|
||
var serverMessages = GetMessagesObservable(client); | ||
|
||
// Wait for server to respond with a message. | ||
var firstMessage = await serverMessages | ||
.Timeout(TimeSpan.FromSeconds(5)) | ||
.FirstAsync(); | ||
|
||
// Exit on first message received when not subscribing to an event. | ||
if (!isSubscribeMessage) | ||
{ | ||
Console.WriteLine(firstMessage); | ||
client.Disconnect(); | ||
return ExitCode.Success; | ||
} | ||
|
||
// Special handling is needed for event subscriptions. | ||
serverMessages.Subscribe( | ||
onNext: Console.WriteLine, | ||
onError: Console.Error.WriteLine | ||
); | ||
|
||
var _ = Console.ReadLine(); | ||
|
||
client.Disconnect(); | ||
return ExitCode.Success; | ||
} | ||
catch (Exception exception) | ||
{ | ||
Console.Error.WriteLine(exception.Message); | ||
client.Disconnect(); | ||
return ExitCode.Error; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Get `IObservable` of parsed server messages. | ||
/// </summary> | ||
private static IObservable<string> GetMessagesObservable(WebsocketClient client) | ||
{ | ||
return client.Messages.Select(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(); | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<TargetFramework>net7-windows10.0.17763</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\GlazeWM.Domain\GlazeWM.Domain.csproj" /> | ||
<ProjectReference Include="..\GlazeWM.Infrastructure\GlazeWM.Infrastructure.csproj" /> | ||
</ItemGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace GlazeWM.App.IpcServer | ||
{ | ||
public static class DependencyInjection | ||
{ | ||
public static IServiceCollection AddIpcServerServices(this IServiceCollection services) | ||
{ | ||
services.AddSingleton<IpcMessageHandler>(); | ||
services.AddSingleton<IpcServerManager>(); | ||
|
||
return services; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<TargetFramework>net7-windows10.0.17763.0</TargetFramework> | ||
<DebugType>embedded</DebugType> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\GlazeWM.Domain\GlazeWM.Domain.csproj" /> | ||
<ProjectReference Include="..\GlazeWM.Infrastructure\GlazeWM.Infrastructure.csproj" /> | ||
</ItemGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text.Json; | ||
using System.Text.RegularExpressions; | ||
using CommandLine; | ||
using GlazeWM.App.IpcServer.Messages; | ||
using GlazeWM.App.IpcServer.Server; | ||
using GlazeWM.Domain.Containers; | ||
using GlazeWM.Domain.Monitors; | ||
using GlazeWM.Domain.UserConfigs; | ||
using GlazeWM.Domain.Windows; | ||
using GlazeWM.Domain.Workspaces; | ||
using GlazeWM.Infrastructure.Bussing; | ||
using GlazeWM.Infrastructure.Serialization; | ||
using GlazeWM.Infrastructure.Utils; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace GlazeWM.App.IpcServer | ||
{ | ||
public sealed class IpcMessageHandler | ||
{ | ||
private readonly Bus _bus; | ||
private readonly CommandParsingService _commandParsingService; | ||
private readonly ContainerService _containerService; | ||
private readonly ILogger<IpcMessageHandler> _logger; | ||
private readonly MonitorService _monitorService; | ||
private readonly WorkspaceService _workspaceService; | ||
private readonly WindowService _windowService; | ||
|
||
private readonly JsonSerializerOptions _serializeOptions = | ||
JsonParser.OptionsFactory((options) => | ||
{ | ||
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; | ||
options.Converters.Add(new JsonContainerConverter()); | ||
}); | ||
|
||
/// <summary> | ||
/// Dictionary of event names and session IDs subscribed to that event. | ||
/// </summary> | ||
internal Dictionary<string, List<Guid>> SubscribedSessions = new(); | ||
|
||
/// <summary> | ||
/// Matches words separated by spaces when not surrounded by double quotes. | ||
/// Example: "a \"b c\" d" -> ["a", "\"b c\"", "d"] | ||
/// </summary> | ||
private static readonly Regex _messagePartsRegex = new("(\".*?\"|\\S+)"); | ||
|
||
public IpcMessageHandler( | ||
Bus bus, | ||
CommandParsingService commandParsingService, | ||
ContainerService containerService, | ||
ILogger<IpcMessageHandler> logger, | ||
MonitorService monitorService, | ||
WorkspaceService workspaceService, | ||
WindowService windowService) | ||
{ | ||
_bus = bus; | ||
_commandParsingService = commandParsingService; | ||
_containerService = containerService; | ||
_logger = logger; | ||
_monitorService = monitorService; | ||
_workspaceService = workspaceService; | ||
_windowService = windowService; | ||
} | ||
|
||
internal string GetResponseMessage(ClientMessage message) | ||
{ | ||
var (sessionId, messageString) = message; | ||
|
||
_logger.LogDebug( | ||
"IPC message from session {Session}: {Message}.", | ||
sessionId, | ||
messageString | ||
); | ||
|
||
try | ||
{ | ||
var messageParts = _messagePartsRegex.Matches(messageString) | ||
.Select(match => match.Value) | ||
.Where(match => match is not null); | ||
|
||
var parsedArgs = Parser.Default.ParseArguments< | ||
InvokeCommandMessage, | ||
SubscribeMessage, | ||
GetMonitorsMessage, | ||
GetWorkspacesMessage, | ||
GetWindowsMessage | ||
>(messageParts); | ||
|
||
object? data = parsedArgs.Value switch | ||
{ | ||
InvokeCommandMessage commandMsg => HandleInvokeCommandMessage(commandMsg), | ||
SubscribeMessage subscribeMsg => HandleSubscribeMessage(subscribeMsg, sessionId), | ||
GetMonitorsMessage => _monitorService.GetMonitors(), | ||
GetWorkspacesMessage => _workspaceService.GetActiveWorkspaces(), | ||
GetWindowsMessage => _windowService.GetWindows(), | ||
_ => throw new Exception($"Invalid message '{messageString}'") | ||
}; | ||
|
||
return ToResponseMessage( | ||
success: true, | ||
data: data, | ||
clientMessage: messageString | ||
); | ||
} | ||
catch (Exception exception) | ||
{ | ||
return ToResponseMessage<bool?>( | ||
success: false, | ||
data: null, | ||
clientMessage: messageString, | ||
error: exception.Message | ||
); | ||
} | ||
} | ||
|
||
private bool? HandleInvokeCommandMessage(InvokeCommandMessage message) | ||
{ | ||
var contextContainer = | ||
_containerService.GetContainerById(message.ContextContainerId) ?? | ||
_containerService.FocusedContainer; | ||
|
||
var commandString = CommandParsingService.FormatCommand(message.Command); | ||
|
||
var command = _commandParsingService.ParseCommand( | ||
commandString, | ||
contextContainer | ||
); | ||
|
||
_bus.Invoke((dynamic)command); | ||
return null; | ||
} | ||
|
||
private bool? HandleSubscribeMessage(SubscribeMessage message, Guid sessionId) | ||
{ | ||
foreach (var eventName in message.Events.Split(',')) | ||
{ | ||
if (SubscribedSessions.ContainsKey(eventName)) | ||
{ | ||
var sessionIds = SubscribedSessions.GetValueOrThrow(eventName); | ||
sessionIds.Add(sessionId); | ||
continue; | ||
} | ||
|
||
SubscribedSessions.Add(eventName, new() { sessionId }); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
private string ToResponseMessage<T>( | ||
bool success, | ||
T? data, | ||
string clientMessage, | ||
string? error = null) | ||
{ | ||
var responseMessage = new ServerMessage<T>( | ||
Success: success, | ||
MessageType: ServerMessageType.ClientResponse, | ||
Data: data, | ||
Error: error, | ||
ClientMessage: clientMessage | ||
); | ||
|
||
return JsonParser.ToString((dynamic)responseMessage, _serializeOptions); | ||
} | ||
|
||
internal string ToEventMessage(Event @event) | ||
{ | ||
var eventMessage = new ServerMessage<Event>( | ||
Success: true, | ||
MessageType: ServerMessageType.SubscribedEvent, | ||
Data: @event, | ||
Error: null, | ||
ClientMessage: null | ||
); | ||
|
||
return JsonParser.ToString((dynamic)eventMessage, _serializeOptions); | ||
} | ||
} | ||
} |
Oops, something went wrong.