Skip to content

Commit

Permalink
CavernPipe ready for duty
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Jan 16, 2025
1 parent 6a4d22f commit 0c281c8
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 26 deletions.
3 changes: 3 additions & 0 deletions Cavern.Format/Utilities/BlockBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ public static BlockBuffer<byte> Create(Stream reader, int blockSize) {
public static BlockBuffer<byte> CreateForConstantPacketSize(Stream reader, int maxBlockSize) {
int blockSize = maxBlockSize;
try {
if (reader is QueueStream queue) {
queue.WaitForData(); // If CavernPipe is in use and didn't fill the queue yet, 0 could be passed to blockSize
}
if (reader.Length < blockSize) {
blockSize = (int)reader.Length;
}
Expand Down
24 changes: 21 additions & 3 deletions Cavern.Format/Utilities/QueueStream.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Threading;

namespace Cavern.Format.Utilities {
/// <summary>
/// A thread-safe FIFO <see cref="MemoryStream"/>.
/// </summary>
public class QueueStream : Stream {
/// <summary>
/// When data is available, lets the threads through, otherwise waits until the data arrives.
/// </summary>
readonly ManualResetEventSlim dataAvailable = new ManualResetEventSlim(false);

/// <summary>
/// The underlying FIFO.
/// </summary>
Expand Down Expand Up @@ -42,9 +48,6 @@ public override long Position {
set => throw new NotSupportedException();
}

/// <inheritdoc/>
public override void Flush() { }

/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count) {
int read = 0;
Expand All @@ -56,6 +59,12 @@ public override int Read(byte[] buffer, int offset, int count) {
}
}
}

lock (queue) {
if (queue.Count == 0) {
dataAvailable.Reset();
}
}
return count;
}

Expand All @@ -66,8 +75,17 @@ public override void Write(byte[] buffer, int offset, int count) {
queue.Enqueue(buffer[offset + i]);
}
}
dataAvailable.Set();
}

/// <summary>
/// If the <see cref="queue"/> is empty, waits for a bulk <see cref="Write"/> so any complete data becomes available.
/// </summary>
public void WaitForData() => dataAvailable.Wait();

/// <inheritdoc/>
public override void Flush() => queue.Clear();

/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();

Expand Down
3 changes: 0 additions & 3 deletions CavernSamples/CavernPipeServer/CavernPipeServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@
<ItemGroup>
<ProjectReference Include="..\..\Cavern.Format\Cavern.Format.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\" />
</ItemGroup>
<ItemGroup>
<_DeploymentManifestIconFile Remove="..\Icon.ico" />
</ItemGroup>
Expand Down
6 changes: 6 additions & 0 deletions CavernSamples/CavernPipeServer/CavernPipeServer.csproj.user
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,11 @@
<Page Update="MainWindow.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Resources\MainWindowStrings.hu-HU.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Resources\MainWindowStrings.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>
</Project>
43 changes: 43 additions & 0 deletions CavernSamples/CavernPipeServer/Consts/Language.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Globalization;
using System.Windows;

namespace CavernPipeServer.Consts {
/// <summary>
/// Handle fetching of language strings and translations.
/// </summary>
static class Language {
/// <summary>
/// Get the <see cref="MainWindow"/>'s translation.
/// </summary>
public static ResourceDictionary GetMainWindowStrings() => mainWindowCache ??= GetFor("MainWindowStrings");

/// <summary>
/// Show the message in an error dialog with the localized &quot;Error&quot; title.
/// </summary>
public static void ShowError(string message) =>
MessageBox.Show(message, (string)GetMainWindowStrings()["Error"], MessageBoxButton.OK, MessageBoxImage.Error);

/// <summary>
/// Get the translation of a resource file in the user's language, or in English if a translation couldn't be found.
/// </summary>
static ResourceDictionary GetFor(string resource) {
if (Array.BinarySearch(supported, CultureInfo.CurrentUICulture.Name) >= 0) {
resource += '.' + CultureInfo.CurrentUICulture.Name;
}
return new() {
Source = new Uri($";component/Resources/{resource}.xaml", UriKind.RelativeOrAbsolute)
};
}

/// <summary>
/// Languages supported that are not the default English.
/// </summary>
static readonly string[] supported = ["hu-HU"];

/// <summary>
/// The loaded translation of the <see cref="MainWindow"/> for reuse.
/// </summary>
static ResourceDictionary mainWindowCache;
}
}
28 changes: 24 additions & 4 deletions CavernSamples/CavernPipeServer/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Windows.Forms;

using Application = System.Windows.Application;
using MessageBox = System.Windows.MessageBox;

namespace CavernPipeServer {
/// <summary>
Expand All @@ -26,15 +27,23 @@ public partial class MainWindow : Window {
/// </summary>
readonly PipeHandler handler = new PipeHandler();

/// <summary>
/// Language string access.
/// </summary>
readonly ResourceDictionary language = Consts.Language.GetMainWindowStrings();

/// <summary>
/// Main status/configuration window and background operation handler.
/// </summary>
public MainWindow() {
InitializeComponent();

contextMenu = new ContextMenuStrip();
contextMenu.Items.Add("Open", null, Open);
contextMenu.Items.Add("Exit", null, Exit);
contextMenu.Items.Add((string)language["MOpen"], null, Open);
contextMenu.Items.Add(new ToolStripSeparator());
contextMenu.Items.Add((string)language["MRest"], null, Restart);
contextMenu.Items.Add(new ToolStripSeparator());
contextMenu.Items.Add((string)language["MExit"], null, Exit);

icon = new NotifyIcon {
Icon = new Icon(Application.GetResourceStream(new Uri("Resources/Icon.ico", UriKind.Relative)).Stream),
Expand All @@ -55,15 +64,26 @@ protected override void OnClosing(CancelEventArgs e) {
/// <summary>
/// Show the configuration dialog.
/// </summary>
void Open(object sender, EventArgs e) {
void Open(object _, EventArgs e) {
Show();
WindowState = WindowState.Normal;
}

/// <summary>
/// Try to restart the pipe.
/// </summary>
void Restart(object _, EventArgs __) {
try {
handler.Start();
} catch (InvalidOperationException e) {
Consts.Language.ShowError(e.Message);
}
}

/// <summary>
/// Close the CavernPipe server.
/// </summary>
void Exit(object sender, EventArgs e) {
void Exit(object _, EventArgs e) {
handler.Dispose();
icon.Dispose();
Application.Current.Shutdown();
Expand Down
71 changes: 55 additions & 16 deletions CavernSamples/CavernPipeServer/PipeHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,65 @@

using Cavern.Format.Utilities;

using CavernPipeServer.Consts;

namespace CavernPipeServer {
/// <summary>
/// Handles the network communication of CavernPipe. A watchdog for a self-created named pipe called &quot;CavernPipe&quot;.
/// </summary>
public class PipeHandler : IDisposable {
/// <summary>
/// A thread that keeps the named pipe active in a loop - if the pipe was closed by a client and CavernPipe is still <see cref="running"/>,
/// The network connection is kept alive.
/// </summary>
public bool Running { get; private set; }

/// <summary>
/// Used for providing thread safety.
/// </summary>
readonly object locker = new object();

/// <summary>
/// A thread that keeps the named pipe active in a loop - if the pipe was closed by a client and CavernPipe is still <see cref="Running"/>,
/// the named pipe is recreated, waiting for the next application to connect to CavernPipe.
/// </summary>
readonly Thread thread;
Thread thread;

/// <summary>
/// Cancels waiting for a player/consumer when quitting the application.
/// </summary>
readonly CancellationTokenSource canceler = new CancellationTokenSource();
CancellationTokenSource canceler;

/// <summary>
/// Network endpoint instance.
/// </summary>
NamedPipeServerStream server;

/// <summary>
/// The network connection shall be kept alive.
/// Handles the network communication of CavernPipe. A watchdog for a self-created named pipe called &quot;CavernPipe&quot;.
/// </summary>
bool running = true;
public PipeHandler() => Start();

/// <summary>
/// Handles the network communication of CavernPipe. A watchdog for a self-created named pipe called &quot;CavernPipe&quot;.
/// Start the named pipe watchdog. If it's already running, an <see cref="InvalidOperationException"/> is thrown.
/// </summary>
public PipeHandler() {
thread = new Thread(ThreadProc);
thread.Start();
public void Start() {
lock (locker) {
if (Running) {
throw new InvalidOperationException((string)Language.GetMainWindowStrings()[server == null ? "EServ" : "ERest"]);
}
canceler = new CancellationTokenSource();
thread = new Thread(ThreadProc);
thread.Start();
Running = true;
}
}

/// <summary>
/// Stop keeping the named pipe alive.
/// </summary>
public void Dispose() {
running = false;
lock (server) {
lock (locker) {
Running = false;
if (server != null) {
if (server.IsConnected) {
server.Close();
Expand All @@ -61,15 +80,14 @@ public void Dispose() {
/// Watchdog for the CavernPipe named pipe. Allows a single instance of this named pipe to exist.
/// </summary>
async void ThreadProc() {
while (running) {
server = new NamedPipeServerStream("CavernPipe");
while (Running) {
try {
TryStartServer();
await server.WaitForConnectionAsync(canceler.Token);
// TODO: fix second connections
using CavernPipeRenderer renderer = new CavernPipeRenderer(server);
byte[] inBuffer = [],
outBuffer = [];
while (running) {
while (Running) {
int length = server.ReadInt32();
if (inBuffer.Length < length) {
inBuffer = new byte[length];
Expand All @@ -88,18 +106,39 @@ async void ThreadProc() {
server.Write(BitConverter.GetBytes(length));
server.Write(outBuffer, 0, length);
}
} catch (TimeoutException) {
Language.ShowError((string)Language.GetMainWindowStrings()["EPipe"]);
return;
} catch { // Content type change or server/stream closed
if (server.IsConnected) {
server.Flush();
}
}
lock (server) {
lock (locker) {
server.Dispose();
server = null;
}
}
}

/// <summary>
/// Try to open the CavernPipe and assign the <see cref="server"/> variable if it was successful. If not, the thread stops,
/// and the user gets a message that it should be restarted.
/// </summary>
void TryStartServer() {
DateTime tryUntil = DateTime.Now + TimeSpan.FromSeconds(3);
while (DateTime.Now < tryUntil) {
try {
server = new NamedPipeServerStream("CavernPipe");
return;
} catch {
server = null;
}
}
Running = false;
throw new TimeoutException();
}

/// <summary>
/// Read a specific number of bytes from the stream or throw an <see cref="EndOfStreamException"/> if it was closed midway.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<!-- Context menu -->
<system:String x:Key="MOpen">Megnyitás</system:String>
<system:String x:Key="MRest">Szerver újraindítása</system:String>
<system:String x:Key="MExit">Kilépés</system:String>

<!-- Errors -->
<system:String x:Key="Error">Hiba</system:String>
<system:String x:Key="ERest">A CavernPipe szerver már fut, és készen áll a lejátszásra.</system:String>
<system:String x:Key="EServ">A CavernPipe szerver újraindítása már folyamatban van.</system:String>
<system:String x:Key="EPipe">A CavernPipe szerver nem tudott elindulni 3 másodpercen keresztül. Menj biztosra, hogy nincs CavernPipe nevű named pipe,
és használd az értesítési sáv CavernPipe ikonjának jobbklikkes menüjét, hogy újraindítsd a pipe-ot.</system:String>
</ResourceDictionary>
15 changes: 15 additions & 0 deletions CavernSamples/CavernPipeServer/Resources/MainWindowStrings.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<!-- Context menu -->
<system:String x:Key="MOpen">Open</system:String>
<system:String x:Key="MRest">Restart server</system:String>
<system:String x:Key="MExit">Exit</system:String>

<!-- Errors -->
<system:String x:Key="Error">Error</system:String>
<system:String x:Key="ERest">The CavernPipe server is already running and ready for playback.</system:String>
<system:String x:Key="EServ">The CavernPipe server is already being restarted.</system:String>
<system:String x:Key="EPipe">CavernPipe server failed to start for 3 seconds. Make sure no named pipe is called CavernPipe, and use the right click menu
of the CavernPipe icon on your system tray to restart the pipe.</system:String>
</ResourceDictionary>

0 comments on commit 0c281c8

Please sign in to comment.