Implement desktop sync protocol handling

This commit is contained in:
Misaka
2026-05-15 21:19:27 +08:00
parent 498251ad79
commit 4c314c510b
9 changed files with 253 additions and 14 deletions

View File

@@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WirelessTextSyncer.Windows", "WirelessTextSyncer.Windows\WirelessTextSyncer.Windows.csproj", "{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WirelessTextSyncer.Windows.Tests", "WirelessTextSyncer.Windows.Tests\WirelessTextSyncer.Windows.Tests.csproj", "{FD53F0F1-EAE0-4CB2-9AC8-57797F0B95D1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -18,5 +20,9 @@ Global
{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Release|Any CPU.Build.0 = Release|Any CPU
{FD53F0F1-EAE0-4CB2-9AC8-57797F0B95D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FD53F0F1-EAE0-4CB2-9AC8-57797F0B95D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FD53F0F1-EAE0-4CB2-9AC8-57797F0B95D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FD53F0F1-EAE0-4CB2-9AC8-57797F0B95D1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,31 @@
using System.Text.Json;
using WirelessTextSyncer.Windows.Models;
namespace WirelessTextSyncer.Windows.Tests;
[TestClass]
public sealed class SyncMessageTests
{
[TestMethod]
public void DeserializeAndroidInsertTextMessage()
{
var message = JsonSerializer.Deserialize<SyncMessage>(
"""{"action":"insertText","text":"hello"}""",
new JsonSerializerOptions(JsonSerializerDefaults.Web));
Assert.IsNotNull(message);
Assert.AreEqual(SyncAction.InsertText, message.Action);
Assert.AreEqual("hello", message.Text);
}
[TestMethod]
public void DeserializeAndroidBackspaceMessage()
{
var message = JsonSerializer.Deserialize<SyncMessage>(
"""{"action":"backspace"}""",
new JsonSerializerOptions(JsonSerializerDefaults.Web));
Assert.IsNotNull(message);
Assert.AreEqual(SyncAction.Backspace, message.Action);
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WirelessTextSyncer.Windows\WirelessTextSyncer.Windows.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,5 +1,8 @@
namespace WirelessTextSyncer.Windows.Models;
using System.Text.Json.Serialization;
[JsonConverter(typeof(SyncActionJsonConverter))]
public enum SyncAction
{
InsertText,

View File

@@ -0,0 +1,41 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace WirelessTextSyncer.Windows.Models;
public sealed class SyncActionJsonConverter : JsonConverter<SyncAction>
{
public override SyncAction Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
var value = reader.GetString();
return value switch
{
"insertText" => SyncAction.InsertText,
"backspace" => SyncAction.Backspace,
"enter" => SyncAction.Enter,
"ping" => SyncAction.Ping,
_ => throw new JsonException($"Unsupported sync action: {value}")
};
}
public override void Write(
Utf8JsonWriter writer,
SyncAction value,
JsonSerializerOptions options)
{
var action = value switch
{
SyncAction.InsertText => "insertText",
SyncAction.Backspace => "backspace",
SyncAction.Enter => "enter",
SyncAction.Ping => "ping",
_ => throw new JsonException($"Unsupported sync action: {value}")
};
writer.WriteStringValue(action);
}
}

View File

@@ -1,7 +1,15 @@
namespace WirelessTextSyncer.Windows.Services;
using System.Runtime.InteropServices;
public sealed class KeyboardInjectionService : IKeyboardInjectionService
{
private const uint InputKeyboard = 1;
private const uint KeyEventFKeyUp = 0x0002;
private const uint KeyEventFUnicode = 0x0004;
private const ushort VirtualKeyBack = 0x08;
private const ushort VirtualKeyReturn = 0x0D;
public void InsertText(string text)
{
if (string.IsNullOrEmpty(text))
@@ -9,16 +17,103 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService
return;
}
SendKeys.SendWait(text);
foreach (var character in text)
{
SendUnicode(character);
}
}
public void Backspace()
{
SendKeys.SendWait("{BACKSPACE}");
SendVirtualKey(VirtualKeyBack);
}
public void Enter()
{
SendKeys.SendWait("{ENTER}");
SendVirtualKey(VirtualKeyReturn);
}
private static void SendUnicode(char character)
{
Send([
CreateUnicodeInput(character, 0),
CreateUnicodeInput(character, KeyEventFKeyUp)
]);
}
private static void SendVirtualKey(ushort virtualKey)
{
Send([
CreateVirtualKeyInput(virtualKey, 0),
CreateVirtualKeyInput(virtualKey, KeyEventFKeyUp)
]);
}
private static void Send(INPUT[] inputs)
{
var sent = SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
if (sent != inputs.Length)
{
throw new InvalidOperationException("Windows did not accept all keyboard input events.");
}
}
private static INPUT CreateUnicodeInput(char character, uint flags)
{
return new INPUT
{
type = InputKeyboard,
U = new InputUnion
{
ki = new KEYBDINPUT
{
wScan = character,
dwFlags = KeyEventFUnicode | flags
}
}
};
}
private static INPUT CreateVirtualKeyInput(ushort virtualKey, uint flags)
{
return new INPUT
{
type = InputKeyboard,
U = new InputUnion
{
ki = new KEYBDINPUT
{
wVk = virtualKey,
dwFlags = flags
}
}
};
}
[DllImport("user32.dll", SetLastError = true)]
private static extern uint SendInput(uint numberOfInputs, INPUT[] inputs, int sizeOfInputStructure);
[StructLayout(LayoutKind.Sequential)]
private struct INPUT
{
public uint type;
public InputUnion U;
}
[StructLayout(LayoutKind.Explicit)]
private struct InputUnion
{
[FieldOffset(0)]
public KEYBDINPUT ki;
}
[StructLayout(LayoutKind.Sequential)]
private struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public UIntPtr dwExtraInfo;
}
}

View File

@@ -8,8 +8,14 @@ namespace WirelessTextSyncer.Windows.Services;
public sealed class WebSocketServerService : IDisposable
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true
};
private readonly SyncMessageHandler messageHandler;
private readonly List<IWebSocketConnection> clients = [];
private readonly object clientsLock = new();
private WebSocketServer? server;
public WebSocketServerService(SyncMessageHandler messageHandler)
@@ -21,7 +27,16 @@ public sealed class WebSocketServerService : IDisposable
public string LocalIpAddress { get; private set; } = "127.0.0.1";
public bool HasClient => clients.Count > 0;
public bool HasClient
{
get
{
lock (clientsLock)
{
return clients.Count > 0;
}
}
}
public event EventHandler? StatusChanged;
@@ -34,13 +49,21 @@ public sealed class WebSocketServerService : IDisposable
server.Start(socket =>
{
socket.OnOpen = () =>
{
lock (clientsLock)
{
clients.Add(socket);
}
StatusChanged?.Invoke(this, EventArgs.Empty);
};
socket.OnClose = () =>
{
lock (clientsLock)
{
clients.Remove(socket);
}
StatusChanged?.Invoke(this, EventArgs.Empty);
};
socket.OnMessage = HandleRawMessage;
@@ -49,18 +72,26 @@ public sealed class WebSocketServerService : IDisposable
public void Dispose()
{
foreach (var client in clients.ToArray())
IWebSocketConnection[] currentClients;
lock (clientsLock)
{
currentClients = clients.ToArray();
clients.Clear();
}
foreach (var client in currentClients)
{
client.Close();
}
clients.Clear();
server?.Dispose();
}
private void HandleRawMessage(string rawMessage)
{
var message = JsonSerializer.Deserialize<SyncMessage>(rawMessage);
try
{
var message = JsonSerializer.Deserialize<SyncMessage>(rawMessage, JsonOptions);
if (message is null)
{
return;
@@ -68,6 +99,11 @@ public sealed class WebSocketServerService : IDisposable
messageHandler.Handle(message);
}
catch (JsonException)
{
return;
}
}
private static string ResolveLocalIpAddress()
{

View File

@@ -75,6 +75,7 @@ public sealed class TrayApplicationContext : ApplicationContext
private void UpdateTrayText()
{
var status = server.HasClient ? "connected" : "waiting";
notifyIcon.Text = $"WirelessTextSyncer {server.LocalIpAddress}:{server.Port} ({status})";
var text = $"WirelessTextSyncer {server.LocalIpAddress}:{server.Port} ({status})";
notifyIcon.Text = text.Length > 63 ? text[..63] : text;
}
}

View File

@@ -10,7 +10,6 @@
<ItemGroup>
<PackageReference Include="Fleck" Version="1.2.0" />
<PackageReference Include="InputSimulatorPlus" Version="1.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
</ItemGroup>