First commit

This commit is contained in:
EnderIce2
2020-10-25 16:19:43 +02:00
parent 9d78d84acb
commit b91db81473
73 changed files with 7634 additions and 0 deletions

View File

@ -0,0 +1,23 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DiscordRPC.IO
{
internal class Handshake
{
/// <summary>
/// Version of the IPC API we are using
/// </summary>
[JsonProperty("v")]
public int Version { get; set; }
/// <summary>
/// The ID of the app.
/// </summary>
[JsonProperty("client_id")]
public string ClientID { get; set; }
}
}

View File

@ -0,0 +1,56 @@
using DiscordRPC.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DiscordRPC.IO
{
/// <summary>
/// Pipe Client used to communicate with Discord.
/// </summary>
public interface INamedPipeClient : IDisposable
{
/// <summary>
/// The logger for the Pipe client to use
/// </summary>
ILogger Logger { get; set; }
/// <summary>
/// Is the pipe client currently connected?
/// </summary>
bool IsConnected { get; }
/// <summary>
/// The pipe the client is currently connected too
/// </summary>
int ConnectedPipe { get; }
/// <summary>
/// Attempts to connect to the pipe. If 0-9 is passed to pipe, it should try to only connect to the specified pipe. If -1 is passed, the pipe will find the first available pipe.
/// </summary>
/// <param name="pipe">If -1 is passed, the pipe will find the first available pipe, otherwise it connects to the pipe that was supplied</param>
/// <returns></returns>
bool Connect(int pipe);
/// <summary>
/// Reads a frame if there is one available. Returns false if there is none. This should be non blocking (aka use a Peek first).
/// </summary>
/// <param name="frame">The frame that has been read. Will be <code>default(PipeFrame)</code> if it fails to read</param>
/// <returns>Returns true if a frame has been read, otherwise false.</returns>
bool ReadFrame(out PipeFrame frame);
/// <summary>
/// Writes the frame to the pipe. Returns false if any errors occur.
/// </summary>
/// <param name="frame">The frame to be written</param>
bool WriteFrame(PipeFrame frame);
/// <summary>
/// Closes the connection
/// </summary>
void Close();
}
}

View File

@ -0,0 +1,509 @@
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;
}
}
}
}

33
DiscordAPI/IO/Opcode.cs Normal file
View File

@ -0,0 +1,33 @@
namespace DiscordRPC.IO
{
/// <summary>
/// The operation code that the <see cref="PipeFrame"/> was sent under. This defines the type of frame and the data to expect.
/// </summary>
public enum Opcode : uint
{
/// <summary>
/// Initial handshake frame
/// </summary>
Handshake = 0,
/// <summary>
/// Generic message frame
/// </summary>
Frame = 1,
/// <summary>
/// Discord has closed the connection
/// </summary>
Close = 2,
/// <summary>
/// Ping frame (not used?)
/// </summary>
Ping = 3,
/// <summary>
/// Pong frame (not used?)
/// </summary>
Pong = 4
}
}

204
DiscordAPI/IO/PipeFrame.cs Normal file
View File

@ -0,0 +1,204 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace DiscordRPC.IO
{
/// <summary>
/// A frame received and sent to the Discord client for RPC communications.
/// </summary>
public struct PipeFrame
{
/// <summary>
/// The maxium size of a pipe frame (16kb).
/// </summary>
public static readonly int MAX_SIZE = 16 * 1024;
/// <summary>
/// The opcode of the frame
/// </summary>
public Opcode Opcode { get; set; }
/// <summary>
/// The length of the frame data
/// </summary>
public uint Length { get { return (uint) Data.Length; } }
/// <summary>
/// The data in the frame
/// </summary>
public byte[] Data { get; set; }
/// <summary>
/// The data represented as a string.
/// </summary>
public string Message
{
get { return GetMessage(); }
set { SetMessage(value); }
}
/// <summary>
/// Creates a new pipe frame instance
/// </summary>
/// <param name="opcode">The opcode of the frame</param>
/// <param name="data">The data of the frame that will be serialized as JSON</param>
public PipeFrame(Opcode opcode, object data)
{
//Set the opcode and a temp field for data
Opcode = opcode;
Data = null;
//Set the data
SetObject(data);
}
/// <summary>
/// Gets the encoding used for the pipe frames
/// </summary>
public Encoding MessageEncoding { get { return Encoding.UTF8; } }
/// <summary>
/// Sets the data based of a string
/// </summary>
/// <param name="str"></param>
private void SetMessage(string str) { Data = MessageEncoding.GetBytes(str); }
/// <summary>
/// Gets a string based of the data
/// </summary>
/// <returns></returns>
private string GetMessage() { return MessageEncoding.GetString(Data); }
/// <summary>
/// Serializes the object into json string then encodes it into <see cref="Data"/>.
/// </summary>
/// <param name="obj"></param>
public void SetObject(object obj)
{
string json = JsonConvert.SerializeObject(obj);
SetMessage(json);
}
/// <summary>
/// Sets the opcodes and serializes the object into a json string.
/// </summary>
/// <param name="opcode"></param>
/// <param name="obj"></param>
public void SetObject(Opcode opcode, object obj)
{
Opcode = opcode;
SetObject(obj);
}
/// <summary>
/// Deserializes the data into the supplied type using JSON.
/// </summary>
/// <typeparam name="T">The type to deserialize into</typeparam>
/// <returns></returns>
public T GetObject<T>()
{
string json = GetMessage();
return JsonConvert.DeserializeObject<T>(json);
}
/// <summary>
/// Attempts to read the contents of the frame from the stream
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
public bool ReadStream(Stream stream)
{
//Try to read the opcode
uint op;
if (!TryReadUInt32(stream, out op))
return false;
//Try to read the length
uint len;
if (!TryReadUInt32(stream, out len))
return false;
uint readsRemaining = len;
//Read the contents
using (var mem = new MemoryStream())
{
byte[] buffer = new byte[Min(2048, len)]; // read in chunks of 2KB
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, Min(buffer.Length, readsRemaining))) > 0)
{
readsRemaining -= len;
mem.Write(buffer, 0, bytesRead);
}
byte[] result = mem.ToArray();
if (result.LongLength != len)
return false;
Opcode = (Opcode)op;
Data = result;
return true;
}
//fun
//if (a != null) { do { yield return true; switch (a) { case 1: await new Task(); default: lock (obj) { foreach (b in c) { for (int d = 0; d < 1; d++) { a++; } } } while (a is typeof(int) || (new Class()) != null) } goto MY_LABEL;
}
/// <summary>
/// Returns minimum value between a int and a unsigned int
/// </summary>
private int Min(int a, uint b)
{
if (b >= a) return a;
return (int) b;
}
/// <summary>
/// Attempts to read a UInt32
/// </summary>
/// <param name="stream"></param>
/// <param name="value"></param>
/// <returns></returns>
private bool TryReadUInt32(Stream stream, out uint value)
{
//Read the bytes available to us
byte[] bytes = new byte[4];
int cnt = stream.Read(bytes, 0, bytes.Length);
//Make sure we actually have a valid value
if (cnt != 4)
{
value = default(uint);
return false;
}
value = BitConverter.ToUInt32(bytes, 0);
return true;
}
/// <summary>
/// Writes the frame into the target frame as one big byte block.
/// </summary>
/// <param name="stream"></param>
public void WriteStream(Stream stream)
{
//Get all the bytes
byte[] op = BitConverter.GetBytes((uint) Opcode);
byte[] len = BitConverter.GetBytes(Length);
//Copy it all into a buffer
byte[] buff = new byte[op.Length + len.Length + Data.Length];
op.CopyTo(buff, 0);
len.CopyTo(buff, op.Length);
Data.CopyTo(buff, op.Length + len.Length);
//Write it to the stream
stream.Write(buff, 0, buff.Length);
}
}
}