diff --git a/Services/Logging/InteractiveUserPath.cs b/Services/Logging/InteractiveUserPath.cs new file mode 100644 index 0000000..35c69ce --- /dev/null +++ b/Services/Logging/InteractiveUserPath.cs @@ -0,0 +1,58 @@ +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace UtopiaCanteenSystem.Services.Logging; + +/// +/// Resolves the logged-on Windows user's profile paths when the app runs as a service (SYSTEM). +/// +internal static class InteractiveUserPath +{ + public static string? TryGetLocalAppDataPath() + { + var profile = TryGetProfileDirectory(); + if (string.IsNullOrWhiteSpace(profile)) + return null; + + var localAppData = Path.Combine(profile, "AppData", "Local"); + return Directory.Exists(localAppData) ? localAppData : null; + } + + private static string? TryGetProfileDirectory() + { + var sessionId = WTSGetActiveConsoleSessionId(); + if (sessionId == 0xFFFFFFFF) + return null; + + if (!WTSQueryUserToken(sessionId, out var userToken) || userToken == IntPtr.Zero) + return null; + + try + { + var sb = new StringBuilder(512); + var size = sb.Capacity; + if (!GetUserProfileDirectory(userToken, sb, ref size, out _)) + return null; + + var path = sb.ToString(); + return string.IsNullOrWhiteSpace(path) ? null : path; + } + finally + { + CloseHandle(userToken); + } + } + + [DllImport("kernel32.dll")] + private static extern uint WTSGetActiveConsoleSessionId(); + + [DllImport("wtsapi32.dll", SetLastError = true)] + private static extern bool WTSQueryUserToken(uint sessionId, out IntPtr phToken); + + [DllImport("userenv.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool GetUserProfileDirectory(IntPtr hToken, StringBuilder lpProfileDir, ref int lpcchSize, out int lpDwFlags); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr hObject); +} diff --git a/Services/Logging/LogPaths.cs b/Services/Logging/LogPaths.cs new file mode 100644 index 0000000..ad23b7e --- /dev/null +++ b/Services/Logging/LogPaths.cs @@ -0,0 +1,65 @@ +using System.IO; + +namespace UtopiaCanteenSystem.Services.Logging; + +/// +/// File log directories. Client uses the current user's LocalAppData. +/// Backend (Windows Service) uses the interactive user's LocalAppData when possible +/// so logs appear under the logged-on user's profile instead of systemprofile. +/// +public static class LogPaths +{ + private static string? _backendLogsDirectoryOverride; + private static string? _cachedLocalAppDataRoot; + + /// + /// Optional absolute path (e.g. from appsettings LogsDirectory). Used by the backend service only. + /// + public static void SetBackendLogsDirectory(string? directory) + { + _backendLogsDirectoryOverride = string.IsNullOrWhiteSpace(directory) + ? null + : directory.Trim(); + } + + /// + /// Client logs folder: %LocalAppData%\UtopiaCanteenClient\Logs. + /// + public static string ClientLogsDirectory => + Path.Combine(ResolveLocalAppDataRoot(), "UtopiaCanteenClient", "Logs"); + + /// + /// Backend logs folder: %LocalAppData%\UtopiaCanteenBackend\Logs + /// (interactive user when running as a service). + /// + public static string BackendLogsDirectory => + _backendLogsDirectoryOverride ?? Path.Combine(ResolveLocalAppDataRoot(), "UtopiaCanteenBackend", "Logs"); + + public static string GetBackendLogFilePath(DateTime? date = null) + { + var d = date ?? DateTime.Now; + return Path.Combine(BackendLogsDirectory, $"backend-{d:yyyy-MM-dd}.log"); + } + + public static string GetClientLogFilePath(DateTime? date = null) + { + var d = date ?? DateTime.Now; + return Path.Combine(ClientLogsDirectory, $"client-{d:yyyy-MM-dd}.log"); + } + + /// + /// User's LocalAppData, or SYSTEM profile when no interactive session (service at login screen). + /// + public static string ResolveLocalAppDataRoot() + { + if (!string.IsNullOrEmpty(_cachedLocalAppDataRoot)) + return _cachedLocalAppDataRoot; + + var interactive = InteractiveUserPath.TryGetLocalAppDataPath(); + if (!string.IsNullOrWhiteSpace(interactive)) + return _cachedLocalAppDataRoot = interactive; + + return _cachedLocalAppDataRoot = + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + } +}