diff --git a/AutoShutdown/Plogon.cs b/AutoShutdown/Plogon.cs index f4c489a..f7e132b 100644 --- a/AutoShutdown/Plogon.cs +++ b/AutoShutdown/Plogon.cs @@ -1,17 +1,13 @@ using System; using System.Collections.Generic; -using System.IO.Pipes; using System.Linq; +using System.Net.Http; using System.Reflection; -using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using AutoRetainerAPI; using Dalamud; using Dalamud.Game; -using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; -using Dalamud.Memory; using Dalamud.Plugin; using Dalamud.Plugin.Services; using ECommons; @@ -26,6 +22,7 @@ namespace AutoShutdown; public sealed class Plogon : IDalamudPlugin { + private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(1) }; private readonly nint _skipMovieAddress; private readonly byte[] _skipMovieOriginalBytes; private readonly AutoRetainerApi _autoRetainerApi; @@ -69,7 +66,7 @@ public sealed class Plogon : IDalamudPlugin private unsafe void FrameworkUpdate(IFramework _) { - if (DateTime.Now > ShutdownAt && _gameGui.TryGetAddonByName("_TitleMenu", out var addon)) + if (DateTime.Now > ShutdownAt && _gameGui.TryGetAddonByName("_TitleMenu", out var _)) { Environment.Exit(0); } @@ -126,22 +123,21 @@ public sealed class Plogon : IDalamudPlugin { TimeSpan next = nextVesselTimes.Min(); _pluginLog.Information($"Next vessel time: {next}"); - Task.Factory.StartNew(async () => + if (next != TimeSpan.Zero) { - try + Task.Factory.StartNew(async () => { - await using NamedPipeClientStream pipe = - new NamedPipeClientStream(".", "ffxiv_TheWatcher", PipeDirection.Out); - await pipe.ConnectAsync(1000); - - byte[] content = Encoding.UTF8.GetBytes($"{next.TotalSeconds}"); - await pipe.WriteAsync(content, 0, content.Length); - } - catch (Exception e) - { - _pluginLog.Warning(e, "Unable to send IPC"); - } - }, TaskCreationOptions.LongRunning); + try + { + await _httpClient.PostAsync(new Uri($"http://localhost:12994/{next.TotalSeconds:F0}"), + null); + } + catch (Exception e) + { + _pluginLog.Warning(e, "Unable to send IPC"); + } + }, TaskCreationOptions.LongRunning); + } } } diff --git a/TheWatcher/Http.cs b/TheWatcher/Http.cs new file mode 100644 index 0000000..6cb2379 --- /dev/null +++ b/TheWatcher/Http.cs @@ -0,0 +1,73 @@ +using System.Net; +using System.Text; + +namespace TheWatcher; + +internal sealed class Http +{ + private readonly HttpListener _listener; + private readonly byte[] _data = "OK"u8.ToArray(); + + public Http() + { + _listener = new HttpListener(); + + // netsh http add urlacl url=http://+:12994/ user=username + _listener.Prefixes.Add("http://+:12994/"); + + _listener.Start(); + } + + public async Task RunAsync(CancellationToken token) + { + token.Register(Close); + + while (!token.IsCancellationRequested) + { + // wait for the next request + HttpListenerContext ctx = await _listener.GetContextAsync(); + + HttpListenerRequest request = ctx.Request; + HttpListenerResponse response = ctx.Response; + + if (request is { HttpMethod: "POST", Url.Segments.Length: >= 2 } && + double.TryParse(request.Url.Segments[1].TrimEnd('/'), out double duration)) + { + TimeSpan timeSpan = TimeSpan.FromSeconds(duration); + if (!SleepCtrl.Enabled) + { + Console.WriteLine($"[{DateTime.Now}] Ignoring sleep, not enabled: {timeSpan}"); + } + else if (timeSpan < TimeSpan.FromMinutes(10)) + { + Console.WriteLine($"[{DateTime.Now}] Not enough sleep duration: {timeSpan}"); + } + else + { + _ = Task.Factory.StartNew(async () => + { + DateTime until = DateTime.Now.Add(timeSpan).Add(TimeSpan.FromMinutes(-5)); + Console.WriteLine($"[{DateTime.Now}] Waiting a few seconds before sleeping for {timeSpan} until {until}"); + + await Task.Delay(TimeSpan.FromSeconds(35), token); + if (SleepCtrl.Enabled) + SleepCtrl.SleepUntil(until); + else + Console.WriteLine($"[{DateTime.Now}] Ignoring sleep (II), not enabled: {timeSpan}"); + }, TaskCreationOptions.LongRunning); + } + } + + response.ContentType = "text/plain"; + response.ContentEncoding = Encoding.UTF8; + response.ContentLength64 = _data.LongLength; + await response.OutputStream.WriteAsync(_data, token); + response.Close(); + } + } + + private void Close() + { + _listener.Close(); + } +} diff --git a/TheWatcher/Program.cs b/TheWatcher/Program.cs index 3c6f15f..3e9bc58 100644 --- a/TheWatcher/Program.cs +++ b/TheWatcher/Program.cs @@ -1,28 +1,25 @@ using System.Diagnostics; -using System.IO.Pipes; -using System.Runtime.InteropServices; -using System.Text; namespace TheWatcher; internal sealed class Program { private static CancellationToken CancellationToken { get; set; } - private static bool EnableSleep { get; set; } + private static readonly Http Http = new(); - public static async Task Main(string[] args) + public static async Task Main() { Console.WriteLine("o.O"); using CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken = cts.Token; - _ = Task.Factory.StartNew(async () => { await RunServer(); }, TaskCreationOptions.LongRunning); + _ = Task.Factory.StartNew(async () => { await Http.RunAsync(CancellationToken); }, TaskCreationOptions.LongRunning); Console.CancelKeyPress += (_, e) => { - EnableSleep = !EnableSleep; - Console.WriteLine($"[{DateTime.Now}] Sleep => {EnableSleep}"); + SleepCtrl.Enabled = !SleepCtrl.Enabled; + Console.WriteLine($"[{DateTime.Now}] Sleep => {SleepCtrl.Enabled}"); e.Cancel = true; }; @@ -102,102 +99,4 @@ internal sealed class Program } } } - - private static void SleepUntil(DateTime dt) - { - Console.WriteLine($"[{DateTime.Now}] Sleeping until {dt}..."); - - nint wakeTimer = SetWakeAt(dt); - if (wakeTimer != nint.Zero) - { - bool suspended = NativeMethods.SetSuspendState(false, false, false); - if (suspended) - { - if (DateTime.Now < dt) - { - Console.WriteLine($"[{DateTime.Now}] Resumed from suspend, but expected to sleep until {dt} => deactivating sleep"); - EnableSleep = false; - } - else - { - Console.WriteLine($"[{DateTime.Now}] Resumed from suspend"); - } - } - else - Console.WriteLine( - $"[{DateTime.Now}] Not Suspended / {Marshal.GetLastPInvokeError()} - {Marshal.GetLastPInvokeErrorMessage()}"); - NativeMethods.CancelWaitableTimer(wakeTimer); - } - else - { - Console.WriteLine( - $"[{DateTime.Now}] Not able to set wait timer / {Marshal.GetLastPInvokeError()} - {Marshal.GetLastPInvokeErrorMessage()}"); - } - } - - private static IntPtr SetWakeAt(DateTime dt) - { - NativeMethods.TimerCompleteDelegate? timerComplete = null; - - // read the manual for SetWaitableTimer to understand how this number is interpreted. - long interval = dt.ToFileTimeUtc(); - IntPtr handle = NativeMethods.CreateWaitableTimer(IntPtr.Zero, true, "WaitableTimer"); - NativeMethods.SetWaitableTimer(handle, ref interval, 0, timerComplete, IntPtr.Zero, true); - return handle; - } - - private static async Task RunServer() - { - while (!CancellationToken.IsCancellationRequested) - { - await using NamedPipeServerStream pipeServer = - new NamedPipeServerStream("ffxiv_TheWatcher", PipeDirection.In, 1, PipeTransmissionMode.Message); - Console.WriteLine($"[{DateTime.Now}] Waiting for client..."); - - await pipeServer.WaitForConnectionAsync(); - Console.WriteLine($"[{DateTime.Now}] Client connected..."); - try - { - string message = await ReadMessage(pipeServer); - Console.WriteLine($"[{DateTime.Now}] Client message: {message}"); - - if (!EnableSleep) - { - Console.WriteLine($"[{DateTime.Now}] Ignoring sleep"); - } - else if (double.TryParse(message, out double duration)) - { - TimeSpan timeSpan = TimeSpan.FromSeconds(duration); - if (timeSpan >= TimeSpan.FromMinutes(10)) - { - DateTime sleep = DateTime.Now.Add(timeSpan).Add(TimeSpan.FromMinutes(-5)); - await Task.Delay(TimeSpan.FromSeconds(35)); - SleepUntil(sleep); - } - else - Console.WriteLine($"[{DateTime.Now}] Not enough sleep duration"); - } - } - catch (IOException e) - { - Console.WriteLine("ERROR: {0}", e.Message); - } - - Console.WriteLine($"[{DateTime.Now}] Finished client loop..."); - } - } - - - private static async Task ReadMessage(PipeStream pipe) - { - byte[] buffer = new byte[1024]; - using var ms = new MemoryStream(); - do - { - var readBytes = await pipe.ReadAsync(buffer, 0, buffer.Length); - await ms.WriteAsync(buffer, 0, readBytes); - } while (!pipe.IsMessageComplete); - - return Encoding.UTF8.GetString(ms.ToArray()); - } } diff --git a/TheWatcher/SleepCtrl.cs b/TheWatcher/SleepCtrl.cs new file mode 100644 index 0000000..05f5d9f --- /dev/null +++ b/TheWatcher/SleepCtrl.cs @@ -0,0 +1,51 @@ +using System.Runtime.InteropServices; + +namespace TheWatcher; + +public class SleepCtrl +{ + public static bool Enabled { get; set; } + + public static void SleepUntil(DateTime dt) + { + Console.WriteLine($"[{DateTime.Now}] Sleeping until {dt}..."); + + nint wakeTimer = SetWakeAt(dt); + if (wakeTimer != nint.Zero) + { + bool suspended = NativeMethods.SetSuspendState(false, false, false); + if (suspended) + { + if (DateTime.Now < dt) + { + Console.WriteLine($"[{DateTime.Now}] Resumed from suspend, but expected to sleep until {dt} => deactivating sleep"); + Enabled = false; + } + else + { + Console.WriteLine($"[{DateTime.Now}] Resumed from suspend"); + } + } + else + Console.WriteLine( + $"[{DateTime.Now}] Not Suspended / {Marshal.GetLastPInvokeError()} - {Marshal.GetLastPInvokeErrorMessage()}"); + NativeMethods.CancelWaitableTimer(wakeTimer); + } + else + { + Console.WriteLine( + $"[{DateTime.Now}] Not able to set wait timer / {Marshal.GetLastPInvokeError()} - {Marshal.GetLastPInvokeErrorMessage()}"); + } + } + + private static IntPtr SetWakeAt(DateTime dt) + { + NativeMethods.TimerCompleteDelegate? timerComplete = null; + + // read the manual for SetWaitableTimer to understand how this number is interpreted. + long interval = dt.ToFileTimeUtc(); + IntPtr handle = NativeMethods.CreateWaitableTimer(IntPtr.Zero, true, "WaitableTimer"); + NativeMethods.SetWaitableTimer(handle, ref interval, 0, timerComplete, IntPtr.Zero, true); + return handle; + } +}