diff --git a/LenovoLegionToolkit.Lib.Automation/AutomationEnvironment.cs b/LenovoLegionToolkit.Lib.Automation/AutomationEnvironment.cs index 180e34c31e..06b8fd9462 100644 --- a/LenovoLegionToolkit.Lib.Automation/AutomationEnvironment.cs +++ b/LenovoLegionToolkit.Lib.Automation/AutomationEnvironment.cs @@ -29,6 +29,7 @@ public class AutomationEnvironment private const string USER_ACTIVE = "LLT_IS_USER_ACTIVE"; private const string WIFI_CONNECTED = "LLT_WIFI_CONNECTED"; private const string WIFI_SSID = "LLT_WIFI_SSID"; + private const string SESSION_LOCKED = "LLT_SESSION_LOCKED"; private const string VALUE_TRUE = "TRUE"; private const string VALUE_FALSE = "FALSE"; @@ -98,6 +99,8 @@ public PowerModeState PowerMode public string? WiFiSsid { set => _dictionary[WIFI_SSID] = value; } + public bool SessionLocked { set => _dictionary[SESSION_LOCKED] = value ? VALUE_TRUE : VALUE_FALSE; } + public Dictionary Dictionary => new(_dictionary); private readonly Dictionary _dictionary = []; diff --git a/LenovoLegionToolkit.Lib.Automation/AutomationProcessor.cs b/LenovoLegionToolkit.Lib.Automation/AutomationProcessor.cs index 2d449eeeef..112045fdf2 100644 --- a/LenovoLegionToolkit.Lib.Automation/AutomationProcessor.cs +++ b/LenovoLegionToolkit.Lib.Automation/AutomationProcessor.cs @@ -23,6 +23,7 @@ public class AutomationProcessor( GodModeController godModeController, GameAutoListener gameAutoListener, ProcessAutoListener processAutoListener, + SessionLockUnlockListener sessionLockUnlockListener, TimeAutoListener timeAutoListener, UserInactivityAutoListener userInactivityAutoListener, WiFiAutoListener wifiAutoListener) @@ -48,6 +49,7 @@ public async Task InitializeAsync() powerStateListener.Changed += PowerStateListener_Changed; powerModeListener.Changed += PowerModeListener_Changed; godModeController.PresetChanged += GodModeController_PresetChanged; + sessionLockUnlockListener.Changed += SessionLockUnlockListener_Changed; _pipelines = [.. settings.Store.Pipelines]; @@ -277,6 +279,12 @@ private async void ProcessAutoListener_Changed(object? sender, ProcessAutoListen await ProcessEvent(e).ConfigureAwait(false); } + private async void SessionLockUnlockListener_Changed(object? sender, SessionLockUnlockListener.ChangedEventArgs args) + { + var e = new SessionLockUnlockAutomationEvent(args.Locked); + await ProcessEvent(e).ConfigureAwait(false); + } + private async void TimeAutoListener_Changed(object? sender, TimeAutoListener.ChangedEventArgs args) { var e = new TimeAutomationEvent(args.Time, args.Day); diff --git a/LenovoLegionToolkit.Lib.Automation/IAutomationEvent.cs b/LenovoLegionToolkit.Lib.Automation/IAutomationEvent.cs index 6cb237af83..62fb27c408 100644 --- a/LenovoLegionToolkit.Lib.Automation/IAutomationEvent.cs +++ b/LenovoLegionToolkit.Lib.Automation/IAutomationEvent.cs @@ -46,6 +46,11 @@ public readonly struct ProcessAutomationEvent(ProcessEventInfoType type, Process public ProcessInfo ProcessInfo { get; } = processInfo; } +public readonly struct SessionLockUnlockAutomationEvent(bool locked) : IAutomationEvent +{ + public bool Locked { get; } = locked; +} + public readonly struct TimeAutomationEvent(Time time, DayOfWeek day) : IAutomationEvent { public Time Time { get; } = time; diff --git a/LenovoLegionToolkit.Lib.Automation/Pipeline/Triggers/IAutomationPipelineTrigger.cs b/LenovoLegionToolkit.Lib.Automation/Pipeline/Triggers/IAutomationPipelineTrigger.cs index 070a362e59..0942d0e0b6 100644 --- a/LenovoLegionToolkit.Lib.Automation/Pipeline/Triggers/IAutomationPipelineTrigger.cs +++ b/LenovoLegionToolkit.Lib.Automation/Pipeline/Triggers/IAutomationPipelineTrigger.cs @@ -65,6 +65,10 @@ public interface IProcessesAutomationPipelineTrigger : IAutomationPipelineTrigge IProcessesAutomationPipelineTrigger DeepCopy(ProcessInfo[] processes); } +public interface ISessionLockPipelineTrigger : IDisallowDuplicatesAutomationPipelineTrigger; + +public interface ISessionUnlockPipelineTrigger : IDisallowDuplicatesAutomationPipelineTrigger; + public interface ITimeAutomationPipelineTrigger : IAutomationPipelineTrigger { bool IsSunrise { get; } diff --git a/LenovoLegionToolkit.Lib.Automation/Pipeline/Triggers/SessionLockAutomationPipelineTrigger.cs b/LenovoLegionToolkit.Lib.Automation/Pipeline/Triggers/SessionLockAutomationPipelineTrigger.cs new file mode 100644 index 0000000000..22ff38c86c --- /dev/null +++ b/LenovoLegionToolkit.Lib.Automation/Pipeline/Triggers/SessionLockAutomationPipelineTrigger.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using LenovoLegionToolkit.Lib.Automation.Resources; +using LenovoLegionToolkit.Lib.Listeners; +using Newtonsoft.Json; + +namespace LenovoLegionToolkit.Lib.Automation.Pipeline.Triggers; + +public class SessionLockAutomationPipelineTrigger : ISessionLockPipelineTrigger +{ + [JsonIgnore] + public string DisplayName => Resource.SessionLockAutomationPipelineTrigger_DisplayName; + + public Task IsMatchingEvent(IAutomationEvent automationEvent) + { + var result = automationEvent is SessionLockUnlockAutomationEvent { Locked: true }; + return Task.FromResult(result); + } + + public Task IsMatchingState() + { + var listener = IoCContainer.Resolve(); + var result = listener.IsLocked; + return Task.FromResult(result.HasValue && result.Value); + } + + public void UpdateEnvironment(AutomationEnvironment environment) => environment.SessionLocked = true; + + public IAutomationPipelineTrigger DeepCopy() => new SessionLockAutomationPipelineTrigger(); + + public override bool Equals(object? obj) => obj is SessionLockAutomationPipelineTrigger; + + public override int GetHashCode() => HashCode.Combine(DisplayName); +} diff --git a/LenovoLegionToolkit.Lib.Automation/Pipeline/Triggers/SessionUnlockAutomationPipelineTrigger.cs b/LenovoLegionToolkit.Lib.Automation/Pipeline/Triggers/SessionUnlockAutomationPipelineTrigger.cs new file mode 100644 index 0000000000..032788a8df --- /dev/null +++ b/LenovoLegionToolkit.Lib.Automation/Pipeline/Triggers/SessionUnlockAutomationPipelineTrigger.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using LenovoLegionToolkit.Lib.Automation.Resources; +using LenovoLegionToolkit.Lib.Listeners; +using Newtonsoft.Json; + +namespace LenovoLegionToolkit.Lib.Automation.Pipeline.Triggers; + +public class SessionUnlockAutomationPipelineTrigger : ISessionUnlockPipelineTrigger +{ + [JsonIgnore] + public string DisplayName => Resource.SessionUnlockAutomationPipelineTrigger_DisplayName; + + public Task IsMatchingEvent(IAutomationEvent automationEvent) + { + var result = automationEvent is SessionLockUnlockAutomationEvent { Locked: false }; + return Task.FromResult(result); + } + + public Task IsMatchingState() + { + var listener = IoCContainer.Resolve(); + var result = listener.IsLocked; + return Task.FromResult(result.HasValue && !result.Value); + } + + public void UpdateEnvironment(AutomationEnvironment environment) => environment.SessionLocked = false; + + public IAutomationPipelineTrigger DeepCopy() => new SessionUnlockAutomationPipelineTrigger(); + + public override bool Equals(object? obj) => obj is SessionUnlockAutomationPipelineTrigger; + + public override int GetHashCode() => HashCode.Combine(DisplayName); +} diff --git a/LenovoLegionToolkit.Lib.Automation/Resources/Resource.Designer.cs b/LenovoLegionToolkit.Lib.Automation/Resources/Resource.Designer.cs index 873060fbda..fcb26f0db9 100644 --- a/LenovoLegionToolkit.Lib.Automation/Resources/Resource.Designer.cs +++ b/LenovoLegionToolkit.Lib.Automation/Resources/Resource.Designer.cs @@ -339,6 +339,24 @@ public static string ProcessesStopRunningAutomationPipelineTrigger_DisplayName { } } + /// + /// Looks up a localized string similar to Session locked. + /// + public static string SessionLockAutomationPipelineTrigger_DisplayName { + get { + return ResourceManager.GetString("SessionLockAutomationPipelineTrigger_DisplayName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Session unlocked. + /// + public static string SessionUnlockAutomationPipelineTrigger_DisplayName { + get { + return ResourceManager.GetString("SessionUnlockAutomationPipelineTrigger_DisplayName", resourceCulture); + } + } + /// /// Looks up a localized string similar to At specified time. /// diff --git a/LenovoLegionToolkit.Lib.Automation/Resources/Resource.resx b/LenovoLegionToolkit.Lib.Automation/Resources/Resource.resx index fd8a826798..b69a43abea 100644 --- a/LenovoLegionToolkit.Lib.Automation/Resources/Resource.resx +++ b/LenovoLegionToolkit.Lib.Automation/Resources/Resource.resx @@ -227,4 +227,10 @@ When device is connected + + Session locked + + + Session unlocked + \ No newline at end of file diff --git a/LenovoLegionToolkit.Lib/IoCModule.cs b/LenovoLegionToolkit.Lib/IoCModule.cs index e3987708de..34ffe7386e 100644 --- a/LenovoLegionToolkit.Lib/IoCModule.cs +++ b/LenovoLegionToolkit.Lib/IoCModule.cs @@ -92,6 +92,7 @@ protected override void Load(ContainerBuilder builder) builder.Register().AutoActivateListener(); builder.Register().AutoActivateListener(); builder.Register().AutoActivateListener(); + builder.Register().AutoActivateListener(); builder.Register().AutoActivateListener(); builder.Register().AutoActivateListener(); builder.Register().AutoActivateListener(); diff --git a/LenovoLegionToolkit.Lib/Listeners/SessionLockUnlockListener.cs b/LenovoLegionToolkit.Lib/Listeners/SessionLockUnlockListener.cs new file mode 100644 index 0000000000..afae4d259c --- /dev/null +++ b/LenovoLegionToolkit.Lib/Listeners/SessionLockUnlockListener.cs @@ -0,0 +1,74 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using LenovoLegionToolkit.Lib.Utils; +using Microsoft.Win32; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.RemoteDesktop; + +namespace LenovoLegionToolkit.Lib.Listeners; + +public class SessionLockUnlockListener : IListener +{ + public class ChangedEventArgs(bool locked) : EventArgs + { + public bool Locked { get; } = locked; + } + + public event EventHandler? Changed; + + public bool? IsLocked { get; private set; } + + public Task StartAsync() + { + SystemEvents.SessionSwitch += SystemEvents_SessionSwitch; + return Task.CompletedTask; + } + + public Task StopAsync() + { + SystemEvents.SessionSwitch -= SystemEvents_SessionSwitch; + return Task.CompletedTask; + } + + private void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e) + { + if (e.Reason != SessionSwitchReason.SessionLock && e.Reason != SessionSwitchReason.SessionUnlock) + return; + + var flags = GetActiveConsoleSessionFlags(); + if (flags == PInvoke.WTS_SESSIONSTATE_UNKNOWN) + { + IsLocked = null; + if (Log.Instance.IsTraceEnabled) + Log.Instance.Trace($"Unknown error occured when getting active console session flags."); + return; + } + var locked = (flags == PInvoke.WTS_SESSIONSTATE_LOCK); + if (Log.Instance.IsTraceEnabled) + Log.Instance.Trace($"Session lock unlock state switched. [locked={locked}]"); + IsLocked = locked; + Changed?.Invoke(this, new(locked)); + } + + private unsafe uint GetActiveConsoleSessionFlags() + { + WTS_INFO_CLASS wtsic = WTS_INFO_CLASS.WTSSessionInfoEx; + var dwSessionId = PInvoke.WTSGetActiveConsoleSessionId(); + var sessionFlags = PInvoke.WTS_SESSIONSTATE_UNKNOWN; + if (PInvoke.WTSQuerySessionInformation(HANDLE.WTS_CURRENT_SERVER_HANDLE, dwSessionId, wtsic, out var ppBuffer, out var pBytesReturned)) + { + if (pBytesReturned > 0) + { + WTSINFOEXW info = Marshal.PtrToStructure((IntPtr)ppBuffer.Value); + if (info.Level == 1) + { + sessionFlags = (uint)info.Data.WTSInfoExLevel1.SessionFlags; + } + } + PInvoke.WTSFreeMemory(ppBuffer); + } + return sessionFlags; + } +} diff --git a/LenovoLegionToolkit.Lib/NativeMethods.txt b/LenovoLegionToolkit.Lib/NativeMethods.txt index 4a96161d93..d3570a9109 100644 --- a/LenovoLegionToolkit.Lib/NativeMethods.txt +++ b/LenovoLegionToolkit.Lib/NativeMethods.txt @@ -224,3 +224,13 @@ ShowWindow UpdateLayeredWindow WINDOW_STYLE WINDOW_EX_STYLE + +WTSINFOEXW +WTS_INFO_CLASS +WTS_SESSIONSTATE_UNKNOWN +WTSGetActiveConsoleSessionId +WTSQuerySessionInformation +WTS_CURRENT_SERVER_HANDLE +WTSFreeMemory +WTS_SESSIONSTATE_LOCK +WTS_SESSIONSTATE_UNLOCK diff --git a/LenovoLegionToolkit.WPF/App.xaml.cs b/LenovoLegionToolkit.WPF/App.xaml.cs index afd0b1fe1b..a4e51ff276 100644 --- a/LenovoLegionToolkit.WPF/App.xaml.cs +++ b/LenovoLegionToolkit.WPF/App.xaml.cs @@ -232,6 +232,15 @@ public async Task ShutdownAsync() } catch { /* Ignored. */ } + try + { + if (IoCContainer.TryResolve() is { } sessionLockUnlockListener) + { + await sessionLockUnlockListener.StopAsync(); + } + } + catch { /* Ignored. */ } + try { if (IoCContainer.TryResolve() is { } hwinfoIntegration) diff --git a/LenovoLegionToolkit.WPF/Extensions/AutomationPipelineTriggerExtensions.cs b/LenovoLegionToolkit.WPF/Extensions/AutomationPipelineTriggerExtensions.cs index 42daa53673..4cfce70c5e 100644 --- a/LenovoLegionToolkit.WPF/Extensions/AutomationPipelineTriggerExtensions.cs +++ b/LenovoLegionToolkit.WPF/Extensions/AutomationPipelineTriggerExtensions.cs @@ -15,6 +15,8 @@ public static class AutomationPipelineTriggerExtensions IHDRPipelineTrigger => SymbolRegular.Hdr24, IProcessesAutomationPipelineTrigger => SymbolRegular.WindowConsole20, IUserInactivityPipelineTrigger => SymbolRegular.ClockAlarm24, + ISessionLockPipelineTrigger => SymbolRegular.LockClosed24, + ISessionUnlockPipelineTrigger => SymbolRegular.LockOpen24, ITimeAutomationPipelineTrigger => SymbolRegular.HourglassHalf24, IDeviceAutomationPipelineTrigger => SymbolRegular.UsbPlug24, INativeWindowsMessagePipelineTrigger => SymbolRegular.Desktop24, diff --git a/LenovoLegionToolkit.WPF/Windows/Automation/CreateAutomationPipelineWindow.xaml.cs b/LenovoLegionToolkit.WPF/Windows/Automation/CreateAutomationPipelineWindow.xaml.cs index 08657df1c7..4092d05db9 100644 --- a/LenovoLegionToolkit.WPF/Windows/Automation/CreateAutomationPipelineWindow.xaml.cs +++ b/LenovoLegionToolkit.WPF/Windows/Automation/CreateAutomationPipelineWindow.xaml.cs @@ -31,6 +31,8 @@ public partial class CreateAutomationPipelineWindow new ProcessesStopRunningAutomationPipelineTrigger([]), new UserInactivityAutomationPipelineTrigger(TimeSpan.Zero), new UserInactivityAutomationPipelineTrigger(TimeSpan.FromMinutes(1)), + new SessionLockAutomationPipelineTrigger(), + new SessionUnlockAutomationPipelineTrigger(), new LidOpenedAutomationPipelineTrigger(), new LidClosedAutomationPipelineTrigger(), new DisplayOnAutomationPipelineTrigger(),