mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-11-15 12:25:31 +08:00
332 lines
8 KiB
C#
332 lines
8 KiB
C#
using System;
|
|
using System.Text;
|
|
using System.Diagnostics;
|
|
using System.IO.Pipes;
|
|
using System.IO;
|
|
using System.Threading.Tasks;
|
|
using System.Threading;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
|
|
namespace ElixirKit;
|
|
|
|
public delegate void ReadyHandler();
|
|
|
|
public delegate void ExitHandler(int ExitCode);
|
|
|
|
public delegate void EventHandler(string Name, string Data);
|
|
|
|
public static class API
|
|
{
|
|
private static Release? release;
|
|
private static Mutex? mutex;
|
|
private static string? id;
|
|
private static bool mainInstance = true;
|
|
|
|
// On Windows we need to manually handle the app being launched multiple times.
|
|
// It can be opened directly or via its associated file types and URL schemes.
|
|
// This function checks if the current app is the "main instance".
|
|
public static bool IsMainInstance(String id)
|
|
{
|
|
if (mutex != null)
|
|
{
|
|
throw new Exception("IsMainInstance can only be called once");
|
|
}
|
|
|
|
API.id = id;
|
|
mutex = new Mutex(true, id, out mainInstance);
|
|
return mainInstance;
|
|
}
|
|
|
|
public static bool HasExited {
|
|
get {
|
|
ensureMainInstance();
|
|
return release!.HasExited;
|
|
}
|
|
}
|
|
|
|
public static void Start(string name, ReadyHandler ready, ExitHandler? exited = null, string? logPath = null)
|
|
{
|
|
ensureMainInstance();
|
|
|
|
release = new Release(name, ready, exited, logPath);
|
|
|
|
if (mutex != null)
|
|
{
|
|
var t = new Task(() => {
|
|
while (true) {
|
|
var line = PipeReadLine();
|
|
|
|
if (line != null)
|
|
{
|
|
release!.Send(line);
|
|
}
|
|
}
|
|
});
|
|
|
|
t.Start();
|
|
}
|
|
}
|
|
|
|
public static int Stop()
|
|
{
|
|
ensureMainInstance();
|
|
return release!.Stop();
|
|
}
|
|
|
|
public static int WaitForExit()
|
|
{
|
|
ensureMainInstance();
|
|
return release!.WaitForExit();
|
|
}
|
|
|
|
public static void Publish(string name, string data)
|
|
{
|
|
if (mainInstance)
|
|
{
|
|
release!.Send($"{name}:{data}");
|
|
}
|
|
else
|
|
{
|
|
PipeWriteLine($"{name}:{data}");
|
|
}
|
|
}
|
|
|
|
public static void Subscribe(EventHandler handler)
|
|
{
|
|
ensureMainInstance();
|
|
release!.Subscribe(handler);
|
|
}
|
|
|
|
static void ensureMainInstance()
|
|
{
|
|
if (!mainInstance)
|
|
{
|
|
throw new Exception("Not on main instance");
|
|
}
|
|
}
|
|
|
|
private static string? PipeReadLine()
|
|
{
|
|
using var pipe = new NamedPipeServerStream(id!);
|
|
pipe.WaitForConnection();
|
|
using var reader = new StreamReader(pipe);
|
|
var line = reader.ReadLine()!;
|
|
pipe.Disconnect();
|
|
return line;
|
|
}
|
|
|
|
private static void PipeWriteLine(string line)
|
|
{
|
|
using var pipe = new NamedPipeClientStream(id!);
|
|
pipe.Connect();
|
|
using var writer = new StreamWriter(pipe);
|
|
writer.WriteLine(line);
|
|
}
|
|
}
|
|
|
|
internal class Release
|
|
{
|
|
Process process;
|
|
Logger logger;
|
|
Listener listener;
|
|
Client client;
|
|
|
|
internal bool HasExited {
|
|
get {
|
|
return process.HasExited;
|
|
}
|
|
}
|
|
|
|
public Release(string name, ReadyHandler ready, ExitHandler? exited = null, string? logPath = null)
|
|
{
|
|
logger = new Logger(logPath);
|
|
listener = new Listener();
|
|
|
|
process = new Process()
|
|
{
|
|
StartInfo = new ProcessStartInfo()
|
|
{
|
|
FileName = relScript(name),
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardInput = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
}
|
|
};
|
|
|
|
process.StartInfo.Arguments = "start";
|
|
process.StartInfo.EnvironmentVariables.Add("ELIXIRKIT_PORT", $"{listener.Port}");
|
|
|
|
if (exited != null)
|
|
{
|
|
process.EnableRaisingEvents = true;
|
|
process.Exited += (sender, args) =>
|
|
{
|
|
exited(process.ExitCode);
|
|
};
|
|
}
|
|
|
|
process.OutputDataReceived += (sender, e) =>
|
|
{
|
|
if (!String.IsNullOrEmpty(e.Data)) { Console.WriteLine(e.Data); }
|
|
};
|
|
|
|
process.ErrorDataReceived += (sender, e) =>
|
|
{
|
|
if (!String.IsNullOrEmpty(e.Data)) { Console.Error.WriteLine(e.Data); }
|
|
};
|
|
|
|
logger.Capture(process);
|
|
process.Start();
|
|
process.BeginOutputReadLine();
|
|
process.BeginErrorReadLine();
|
|
|
|
Task.Run(() => {
|
|
var tcpClient = listener.TcpListener.AcceptTcpClient();
|
|
client = new Client(tcpClient);
|
|
ready();
|
|
});
|
|
}
|
|
|
|
public void Send(string message)
|
|
{
|
|
client.Send(message);
|
|
}
|
|
|
|
public void Subscribe(EventHandler handler)
|
|
{
|
|
client.Subscribe(handler);
|
|
}
|
|
|
|
public int Stop()
|
|
{
|
|
if (HasExited)
|
|
{
|
|
return process!.ExitCode;
|
|
}
|
|
|
|
client.TcpClient.Close();
|
|
listener.TcpListener.Stop();
|
|
return WaitForExit();
|
|
}
|
|
|
|
public int WaitForExit()
|
|
{
|
|
process!.WaitForExit();
|
|
return process!.ExitCode;
|
|
}
|
|
|
|
private string relScript(string name)
|
|
{
|
|
var exe = Process.GetCurrentProcess().MainModule!.FileName;
|
|
var dir = Path.GetDirectoryName(exe)!;
|
|
|
|
if (Path.GetExtension(exe) == ".exe")
|
|
{
|
|
return Path.Combine(dir, "rel", "bin", name + ".bat");
|
|
}
|
|
else
|
|
{
|
|
return Path.Combine(dir, "rel", "bin", name);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal class Logger {
|
|
private StreamWriter? writer;
|
|
|
|
public Logger(string? path)
|
|
{
|
|
if (path != null)
|
|
{
|
|
writer = File.AppendText(path);
|
|
writer.AutoFlush = true;
|
|
}
|
|
}
|
|
|
|
public void Capture(Process process)
|
|
{
|
|
if (writer != null)
|
|
{
|
|
process.OutputDataReceived += (sender, e) =>
|
|
{
|
|
if (!String.IsNullOrEmpty(e.Data)) { writer.WriteLine(e.Data); }
|
|
};
|
|
|
|
process.ErrorDataReceived += (sender, e) =>
|
|
{
|
|
if (!String.IsNullOrEmpty(e.Data)) { writer.WriteLine(e.Data); }
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
internal class Listener
|
|
{
|
|
public int Port;
|
|
public TcpListener TcpListener;
|
|
|
|
public Listener()
|
|
{
|
|
var endpoint = new IPEndPoint(IPAddress.Loopback, 0);
|
|
TcpListener = new(endpoint);
|
|
TcpListener.Start();
|
|
Port = ((IPEndPoint)TcpListener.LocalEndpoint).Port;
|
|
}
|
|
}
|
|
|
|
internal class Client
|
|
{
|
|
public TcpClient TcpClient;
|
|
private NetworkStream stream;
|
|
|
|
public Client(TcpClient tcpClient)
|
|
{
|
|
TcpClient = tcpClient;
|
|
stream = tcpClient.GetStream();
|
|
}
|
|
|
|
public void Send(string payload)
|
|
{
|
|
var payloadBytes = System.Text.Encoding.UTF8.GetBytes(payload);
|
|
var size = System.Convert.ToUInt32(IPAddress.HostToNetworkOrder(payloadBytes.Length));
|
|
var headerBytes = BitConverter.GetBytes(size);
|
|
stream.Write(headerBytes, 0, headerBytes.Length);
|
|
stream.Write(payloadBytes, 0, payloadBytes.Length);
|
|
}
|
|
|
|
public void Subscribe(EventHandler handler)
|
|
{
|
|
var t = new Task(() => {
|
|
receiveMessage(handler);
|
|
});
|
|
|
|
t.Start();
|
|
}
|
|
|
|
private void receiveMessage(EventHandler handler)
|
|
{
|
|
var sizeBuf = new byte[4];
|
|
var sizeBytesRead = stream.Read(sizeBuf, 0, sizeBuf.Length);
|
|
|
|
// socket is closed
|
|
if (sizeBytesRead == 0) { return; }
|
|
|
|
var size = IPAddress.NetworkToHostOrder((int)BitConverter.ToUInt32(sizeBuf, 0));
|
|
var payloadBuf = new byte[size];
|
|
var payloadBytesRead = stream.Read(payloadBuf, 0, size);
|
|
|
|
// socket is closed
|
|
if (payloadBytesRead == 0) { return; }
|
|
|
|
var payload = System.Text.Encoding.UTF8.GetString(payloadBuf);
|
|
var parts = payload.Split(new char[] {':'}, 2);
|
|
var name = parts[0];
|
|
var data = parts[1];
|
|
|
|
handler(name, data);
|
|
receiveMessage(handler);
|
|
}
|
|
}
|