a1331212cb
Self-hosted Minecraft modpack distribution + administration system.
- launcher/ Avalonia 12 desktop client; single-file win-x64 publish.
Microsoft auth via XboxAuthNet, manifest+SHA-1 mod sync,
portable install path, sidecar settings.
- server/ brass-sigil-server daemon (.NET 8, linux-x64). Wraps the
MC subprocess, embedded Kestrel admin panel with cookie
auth + rate limiting, RCON bridge, scheduled backups,
BlueMap CLI integration with player markers + skin proxy,
friend-side whitelist request flow, world wipe with seed
selection (keep current / random / custom).
- pack/ pack.lock.json (Modrinth + manual CurseForge entries),
data-only tweak source under tweaks/, build outputs in
overrides/ (gitignored).
- scripts/ Build-Pack / Build-Tweaks / Update-Pack / Check-Updates
plus Deploy-Brass.ps1 unified one-shot deploy with
version-bump pre-flight and daemon-state detection.
125 lines
4.4 KiB
C#
125 lines
4.4 KiB
C#
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace BrassAndSigil.Server.Services;
|
|
|
|
/// <summary>
|
|
/// Wraps a Windows Job Object configured with KILL_ON_JOB_CLOSE so that any process
|
|
/// assigned to it dies the moment our process does -- regardless of *how* we died
|
|
/// (X-button on the console, Task Manager End Task, parent BSOD, etc.). Without
|
|
/// this, a Java subprocess can outlive us and keep the server files locked.
|
|
///
|
|
/// On Linux, use systemd's cgroup management instead; the equivalent guarantee
|
|
/// comes for free when the tool runs as a systemd unit.
|
|
/// </summary>
|
|
public sealed class WindowsJobObject : IDisposable
|
|
{
|
|
private const int JobObjectExtendedLimitInformation = 9;
|
|
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000;
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
private static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string? lpName);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool SetInformationJobObject(IntPtr hJob, int infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool CloseHandle(IntPtr hObject);
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
|
|
{
|
|
public long PerProcessUserTimeLimit;
|
|
public long PerJobUserTimeLimit;
|
|
public uint LimitFlags;
|
|
public UIntPtr MinimumWorkingSetSize;
|
|
public UIntPtr MaximumWorkingSetSize;
|
|
public uint ActiveProcessLimit;
|
|
public UIntPtr Affinity;
|
|
public uint PriorityClass;
|
|
public uint SchedulingClass;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct IO_COUNTERS
|
|
{
|
|
public ulong ReadOperationCount;
|
|
public ulong WriteOperationCount;
|
|
public ulong OtherOperationCount;
|
|
public ulong ReadTransferCount;
|
|
public ulong WriteTransferCount;
|
|
public ulong OtherTransferCount;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
|
{
|
|
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
|
public IO_COUNTERS IoInfo;
|
|
public UIntPtr ProcessMemoryLimit;
|
|
public UIntPtr JobMemoryLimit;
|
|
public UIntPtr PeakProcessMemoryUsed;
|
|
public UIntPtr PeakJobMemoryUsed;
|
|
}
|
|
|
|
private IntPtr _handle;
|
|
private bool _disposed;
|
|
|
|
public WindowsJobObject()
|
|
{
|
|
if (!OperatingSystem.IsWindows())
|
|
throw new PlatformNotSupportedException("WindowsJobObject only works on Windows.");
|
|
|
|
_handle = CreateJobObject(IntPtr.Zero, null);
|
|
if (_handle == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateJobObject failed");
|
|
|
|
var info = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
|
{
|
|
BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
|
|
{
|
|
LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
}
|
|
};
|
|
var size = Marshal.SizeOf(info);
|
|
var ptr = Marshal.AllocHGlobal(size);
|
|
try
|
|
{
|
|
Marshal.StructureToPtr(info, ptr, fDeleteOld: false);
|
|
if (!SetInformationJobObject(_handle, JobObjectExtendedLimitInformation, ptr, (uint)size))
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "SetInformationJobObject failed");
|
|
}
|
|
finally
|
|
{
|
|
Marshal.FreeHGlobal(ptr);
|
|
}
|
|
}
|
|
|
|
public void AssignProcess(Process process)
|
|
{
|
|
if (_disposed) throw new ObjectDisposedException(nameof(WindowsJobObject));
|
|
if (!AssignProcessToJobObject(_handle, process.Handle))
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "AssignProcessToJobObject failed");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
if (_handle != IntPtr.Zero)
|
|
{
|
|
CloseHandle(_handle); // Closing the last handle triggers KILL_ON_JOB_CLOSE
|
|
_handle = IntPtr.Zero;
|
|
}
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
~WindowsJobObject() => Dispose();
|
|
}
|