mirror of
https://github.com/EnderIce2/SDR-RPC.git
synced 2025-05-25 19:54:26 +00:00
510 lines
17 KiB
C#
510 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using DiscordRPC.Logging;
|
|
using System.IO.Pipes;
|
|
using System.Threading;
|
|
using System.IO;
|
|
|
|
namespace DiscordRPC.IO
|
|
{
|
|
/// <summary>
|
|
/// A named pipe client using the .NET framework <see cref="NamedPipeClientStream"/>
|
|
/// </summary>
|
|
public sealed class ManagedNamedPipeClient : INamedPipeClient
|
|
{
|
|
/// <summary>
|
|
/// Name format of the pipe
|
|
/// </summary>
|
|
const string PIPE_NAME = @"discord-ipc-{0}";
|
|
|
|
/// <summary>
|
|
/// The logger for the Pipe client to use
|
|
/// </summary>
|
|
public ILogger Logger { get; set; }
|
|
|
|
/// <summary>
|
|
/// Checks if the client is connected
|
|
/// </summary>
|
|
public bool IsConnected
|
|
{
|
|
get
|
|
{
|
|
//This will trigger if the stream is disabled. This should prevent the lock check
|
|
if (_isClosed) return false;
|
|
lock (l_stream)
|
|
{
|
|
//We cannot be sure its still connected, so lets double check
|
|
return _stream != null && _stream.IsConnected;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The pipe we are currently connected too.
|
|
/// </summary>
|
|
public int ConnectedPipe { get { return _connectedPipe; } }
|
|
|
|
private int _connectedPipe;
|
|
private NamedPipeClientStream _stream;
|
|
|
|
private byte[] _buffer = new byte[PipeFrame.MAX_SIZE];
|
|
|
|
private Queue<PipeFrame> _framequeue = new Queue<PipeFrame>();
|
|
private object _framequeuelock = new object();
|
|
|
|
private volatile bool _isDisposed = false;
|
|
private volatile bool _isClosed = true;
|
|
|
|
private object l_stream = new object();
|
|
|
|
/// <summary>
|
|
/// Creates a new instance of a Managed NamedPipe client. Doesn't connect to anything yet, just setups the values.
|
|
/// </summary>
|
|
public ManagedNamedPipeClient()
|
|
{
|
|
_buffer = new byte[PipeFrame.MAX_SIZE];
|
|
Logger = new NullLogger();
|
|
_stream = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Connects to the pipe
|
|
/// </summary>
|
|
/// <param name="pipe"></param>
|
|
/// <returns></returns>
|
|
public bool Connect(int pipe)
|
|
{
|
|
Logger.Trace("ManagedNamedPipeClient.Connection(" + pipe + ")");
|
|
|
|
if (_isDisposed)
|
|
throw new ObjectDisposedException("NamedPipe");
|
|
|
|
if (pipe > 9)
|
|
throw new ArgumentOutOfRangeException("pipe", "Argument cannot be greater than 9");
|
|
|
|
if (pipe < 0)
|
|
{
|
|
//Iterate until we connect to a pipe
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
if (AttemptConnection(i) || AttemptConnection(i, true))
|
|
{
|
|
BeginReadStream();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//Attempt to connect to a specific pipe
|
|
if (AttemptConnection(pipe) || AttemptConnection(pipe, true))
|
|
{
|
|
BeginReadStream();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
//We failed to connect
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts a new connection
|
|
/// </summary>
|
|
/// <param name="pipe">The pipe number to connect too.</param>
|
|
/// <param name="isSandbox">Should the connection to a sandbox be attempted?</param>
|
|
/// <returns></returns>
|
|
private bool AttemptConnection(int pipe, bool isSandbox = false)
|
|
{
|
|
if (_isDisposed)
|
|
throw new ObjectDisposedException("_stream");
|
|
|
|
//If we are sandbox but we dont support sandbox, then skip
|
|
string sandbox = isSandbox ? GetPipeSandbox() : "";
|
|
if (isSandbox && sandbox == null)
|
|
{
|
|
Logger.Trace("Skipping sandbox connection.");
|
|
return false;
|
|
}
|
|
|
|
//Prepare the pipename
|
|
Logger.Trace("Connection Attempt " + pipe + " (" + sandbox + ")");
|
|
string pipename = GetPipeName(pipe, sandbox);
|
|
|
|
try
|
|
{
|
|
//Create the client
|
|
lock (l_stream)
|
|
{
|
|
Logger.Info("Attempting to connect to " + pipename);
|
|
_stream = new NamedPipeClientStream(".", pipename, PipeDirection.InOut, PipeOptions.Asynchronous);
|
|
_stream.Connect(1000);
|
|
|
|
//Spin for a bit while we wait for it to finish connecting
|
|
Logger.Trace("Waiting for connection...");
|
|
do { Thread.Sleep(10); } while (!_stream.IsConnected);
|
|
}
|
|
|
|
//Store the value
|
|
Logger.Info("Connected to " + pipename);
|
|
_connectedPipe = pipe;
|
|
_isClosed = false;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
//Something happened, try again
|
|
//TODO: Log the failure condition
|
|
Logger.Error("Failed connection to {0}. {1}", pipename, e.Message);
|
|
Close();
|
|
}
|
|
|
|
Logger.Trace("Done. Result: {0}", _isClosed);
|
|
return !_isClosed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts a read. Can be executed in another thread.
|
|
/// </summary>
|
|
private void BeginReadStream()
|
|
{
|
|
if (_isClosed) return;
|
|
try
|
|
{
|
|
lock (l_stream)
|
|
{
|
|
//Make sure the stream is valid
|
|
if (_stream == null || !_stream.IsConnected) return;
|
|
|
|
Logger.Trace("Begining Read of {0} bytes", _buffer.Length);
|
|
_stream.BeginRead(_buffer, 0, _buffer.Length, new AsyncCallback(EndReadStream), _stream.IsConnected);
|
|
}
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
Logger.Warning("Attempted to start reading from a disposed pipe");
|
|
return;
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
//The pipe has been closed
|
|
Logger.Warning("Attempted to start reading from a closed pipe");
|
|
return;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Error("An exception occured while starting to read a stream: {0}", e.Message);
|
|
Logger.Error(e.StackTrace);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ends a read. Can be executed in another thread.
|
|
/// </summary>
|
|
/// <param name="callback"></param>
|
|
private void EndReadStream(IAsyncResult callback)
|
|
{
|
|
Logger.Trace("Ending Read");
|
|
int bytes = 0;
|
|
|
|
try
|
|
{
|
|
//Attempt to read the bytes, catching for IO exceptions or dispose exceptions
|
|
lock (l_stream)
|
|
{
|
|
//Make sure the stream is still valid
|
|
if (_stream == null || !_stream.IsConnected) return;
|
|
|
|
//Read our btyes
|
|
bytes = _stream.EndRead(callback);
|
|
}
|
|
}
|
|
catch (IOException)
|
|
{
|
|
Logger.Warning("Attempted to end reading from a closed pipe");
|
|
return;
|
|
}
|
|
catch (NullReferenceException)
|
|
{
|
|
Logger.Warning("Attempted to read from a null pipe");
|
|
return;
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
Logger.Warning("Attemped to end reading from a disposed pipe");
|
|
return;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Error("An exception occured while ending a read of a stream: {0}", e.Message);
|
|
Logger.Error(e.StackTrace);
|
|
return;
|
|
}
|
|
|
|
//How much did we read?
|
|
Logger.Trace("Read {0} bytes", bytes);
|
|
|
|
//Did we read anything? If we did we should enqueue it.
|
|
if (bytes > 0)
|
|
{
|
|
//Load it into a memory stream and read the frame
|
|
using (MemoryStream memory = new MemoryStream(_buffer, 0, bytes))
|
|
{
|
|
try
|
|
{
|
|
PipeFrame frame = new PipeFrame();
|
|
if (frame.ReadStream(memory))
|
|
{
|
|
Logger.Trace("Read a frame: {0}", frame.Opcode);
|
|
|
|
//Enqueue the stream
|
|
lock (_framequeuelock)
|
|
_framequeue.Enqueue(frame);
|
|
}
|
|
else
|
|
{
|
|
//TODO: Enqueue a pipe close event here as we failed to read something.
|
|
Logger.Error("Pipe failed to read from the data received by the stream.");
|
|
Close();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Error("A exception has occured while trying to parse the pipe data: " + e.Message);
|
|
Close();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//If we read 0 bytes, its probably a broken pipe. However, I have only confirmed this is the case for MacOSX.
|
|
// I have added this check here just so the Windows builds are not effected and continue to work as expected.
|
|
if (IsUnix())
|
|
{
|
|
Logger.Error("Empty frame was read on " + Environment.OSVersion.ToString() + ", aborting.");
|
|
Close();
|
|
}
|
|
else
|
|
{
|
|
Logger.Warning("Empty frame was read. Please send report to Lachee.");
|
|
}
|
|
}
|
|
|
|
//We are still connected, so continue to read
|
|
if (!_isClosed && IsConnected)
|
|
{
|
|
Logger.Trace("Starting another read");
|
|
BeginReadStream();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a frame, returning false if none are available
|
|
/// </summary>
|
|
/// <param name="frame"></param>
|
|
/// <returns></returns>
|
|
public bool ReadFrame(out PipeFrame frame)
|
|
{
|
|
if (_isDisposed)
|
|
throw new ObjectDisposedException("_stream");
|
|
|
|
//Check the queue, returning the pipe if we have anything available. Otherwise null.
|
|
lock (_framequeuelock)
|
|
{
|
|
if (_framequeue.Count == 0)
|
|
{
|
|
//We found nothing, so just default and return null
|
|
frame = default(PipeFrame);
|
|
return false;
|
|
}
|
|
|
|
//Return the dequed frame
|
|
frame = _framequeue.Dequeue();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a frame to the pipe
|
|
/// </summary>
|
|
/// <param name="frame"></param>
|
|
/// <returns></returns>
|
|
public bool WriteFrame(PipeFrame frame)
|
|
{
|
|
if (_isDisposed)
|
|
throw new ObjectDisposedException("_stream");
|
|
|
|
//Write the frame. We are assuming proper duplex connection here
|
|
if (_isClosed || !IsConnected)
|
|
{
|
|
Logger.Error("Failed to write frame because the stream is closed");
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
//Write the pipe
|
|
//This can only happen on the main thread so it should be fine.
|
|
frame.WriteStream(_stream);
|
|
return true;
|
|
}
|
|
catch (IOException io)
|
|
{
|
|
Logger.Error("Failed to write frame because of a IO Exception: {0}", io.Message);
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
Logger.Warning("Failed to write frame as the stream was already disposed");
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
Logger.Warning("Failed to write frame because of a invalid operation");
|
|
}
|
|
|
|
//We must have failed the try catch
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the pipe
|
|
/// </summary>
|
|
public void Close()
|
|
{
|
|
//If we are already closed, jsut exit
|
|
if (_isClosed)
|
|
{
|
|
Logger.Warning("Tried to close a already closed pipe.");
|
|
return;
|
|
}
|
|
|
|
//flush and dispose
|
|
try
|
|
{
|
|
//Wait for the stream object to become available.
|
|
lock (l_stream)
|
|
{
|
|
if (_stream != null)
|
|
{
|
|
try
|
|
{
|
|
//Stream isn't null, so flush it and then dispose of it.\
|
|
// We are doing a catch here because it may throw an error during this process and we dont care if it fails.
|
|
_stream.Flush();
|
|
_stream.Dispose();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
//We caught an error, but we dont care anyways because we are disposing of the stream.
|
|
}
|
|
|
|
//Make the stream null and set our flag.
|
|
_stream = null;
|
|
_isClosed = true;
|
|
}
|
|
else
|
|
{
|
|
//The stream is already null?
|
|
Logger.Warning("Stream was closed, but no stream was available to begin with!");
|
|
}
|
|
}
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
//ITs already been disposed
|
|
Logger.Warning("Tried to dispose already disposed stream");
|
|
}
|
|
finally
|
|
{
|
|
//For good measures, we will mark the pipe as closed anyways
|
|
_isClosed = true;
|
|
_connectedPipe = -1;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes of the stream
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
//Prevent double disposing
|
|
if (_isDisposed) return;
|
|
|
|
//Close the stream (disposing of it too)
|
|
if (!_isClosed) Close();
|
|
|
|
//Dispose of the stream if it hasnt been destroyed already.
|
|
lock (l_stream)
|
|
{
|
|
if (_stream != null)
|
|
{
|
|
_stream.Dispose();
|
|
_stream = null;
|
|
}
|
|
}
|
|
|
|
//Set our dispose flag
|
|
_isDisposed = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a platform specific path that Discord is hosting the IPC on.
|
|
/// </summary>
|
|
/// <param name="pipe">The pipe number.</param>
|
|
/// <param name="sandbox">The sandbox the pipe is in. Leave blank for no sandbox.</param>
|
|
/// <returns></returns>
|
|
public static string GetPipeName(int pipe, string sandbox = "")
|
|
{
|
|
if (!IsUnix()) return sandbox + string.Format(PIPE_NAME, pipe);
|
|
return Path.Combine(GetTemporaryDirectory(), sandbox + string.Format(PIPE_NAME, pipe));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the name of the possible sandbox enviroment the pipe might be located within. If the platform doesn't support sandboxed Discord, then it will return null.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public static string GetPipeSandbox()
|
|
{
|
|
switch (Environment.OSVersion.Platform)
|
|
{
|
|
default:
|
|
return null;
|
|
case PlatformID.Unix:
|
|
return "snap.discord/";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the temporary path for the current enviroment. Only applicable for UNIX based systems.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private static string GetTemporaryDirectory()
|
|
{
|
|
string temp = null;
|
|
temp = temp ?? Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
|
|
temp = temp ?? Environment.GetEnvironmentVariable("TMPDIR");
|
|
temp = temp ?? Environment.GetEnvironmentVariable("TMP");
|
|
temp = temp ?? Environment.GetEnvironmentVariable("TEMP");
|
|
temp = temp ?? "/tmp";
|
|
return temp;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the current OS platform is Unix based (Unix or MacOSX).
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public static bool IsUnix()
|
|
{
|
|
switch (Environment.OSVersion.Platform)
|
|
{
|
|
default:
|
|
return false;
|
|
|
|
case PlatformID.Unix:
|
|
case PlatformID.MacOSX:
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|