fohrloop / wakepy

Cross-platform keep-awake with python
MIT License
189 stars 13 forks source link

Windows SetThreadExecutionState based prevent sleep also prevents automatic screen lock if screen saver with password is not enabled #169

Open fohrloop opened 8 months ago

fohrloop commented 8 months ago

Background: State diagram

The current understanding of the Windows sleep & lock states diagram.

image

Windows may lock the screen automatically in two occasions:

  1. When user resumes from the screen saver if the ScreenSaverIsSecure is set, either by (1) "On resume, display log-on screen" tickbox the Screen Saver Settings or (2) Enforcing ScreenSaverIsSecure by a Group Policy (GPO). (this may be checked with SystemParametersInfoW).
  2. When user returns from the suspend state to the working state, if the setting "Require sign-in when PC wakes up from sleep" is set

This has the implication that wakepy might prevent automatic screen lock, because if Sleep is inhibited, the route (2) to lock screen is not possible. If route (1) to lock screen is also removed, Windows will never lock screen automatically.This happens when user does not a screen saver with password enabled and there is no Group Policy enforcing automatic and secure screen lock.

To think about

Should wakepy take care that the screen lock is started in the keep.running mode? Or is it the responsibility of the user to have Screen Saver enabled on Windows (and perhaps, on some other systems)?

Possible solutions

Solution option A

Solution option B

fohrloop commented 8 months ago

Comments which are merely debugging details mode under \<details> to reduce noise

Confirmation that screen is locked if Screen Saver with password is not enabled Just confirmed this again with another test. Left computer SetThreadExecutionState with ES_CONTINUOUS and ES_SYSTEM_REQUIRED for 8 hours. The laptop screen was shut down quite fast (I think I had 1 minute setting). After coming back, I moved mouse a bit which put the screen on, and everything was like I left it (script has been running the whole time). No screenlock and no passwords asked. Not sure if the screen lock could be there for other versions of Windows (This is Windows 10 version 22H2 build 19045.3324), or with some settings enabled. I also have this in my "Sign in Options" settings: ![image](https://github.com/fohrloop/wakepy/assets/17479683/9f6b9877-ebeb-4672-a2e2-378fc8f24178) the only another selectable option is Never. This gives the impression that Windows normally only requires sign in (=has screen lock on) when it *returns from sleep*. Could be that if the system never suspends (enters a form of sleep), it will never be returning from sleep, and thus, never requiring sign in. ## Screen Saver Settings I have also this in my Lock Screen -> Screen Saver Settings: ![image](https://github.com/fohrloop/wakepy/assets/17479683/c368544a-ad37-4177-9b13-5dc2697a237b) I.e. I do not have screen saver enabled. I'm not certain if this is the default setting or not. After setting the "On resume, display log-on screen" to True ![image](https://github.com/fohrloop/wakepy/assets/17479683/9dee00a9-36a5-43bc-882d-563c8b79698f) and repeating the test, the screen blanked in about a minute, and after a bit longer than minute, when moving mouse a bit, there is a screen lock asking for a password. --- Tested with SetThreadExecutionState with ES_CONTINUOUS + ES_SYSTEM_REQUIRED + ES_DISPLAY_REQUIRED and with the screen saver on with the "On resume, display log-on screen" selected. Ran 15 + minutes. As expected the screen saver / blank never occurred and I did not have to log in with my password.
How the Screen Saver Settings can be checked? ## How the Screen Saver Settings can be checked? I will write down here notes about how to check the Screen Saver Settings. I will be using a - **PC1**: home laptop (Windows 10 Pro version 22H2 build 19045.332) and a - **PC2**: company laptop (with company group policies and restricted settings access, and "Windows/company default settings", Windows 10 Enterprise version 22H2 build 19045.3803). ### SUMMARY Use Option 4, SystemParametersInfoW, to get the screen saver settings. This seems to be the most reliable as it supports both, getting values saved with the Settings UI and values enforced by a group policy (GPO). ### Option 1: Directly from Lock Screen -> Screen Saver Settings - From the Settings UI ("Windows + I") -> Lock Screen -> Screen Saver Settings, as shown in an above comment. There is "On resume, display log-on screen" checkbox (True/False) and "Wait X minutes" (integer) setting. - Alternatively, running ``` Start ms-settings:lockscreen ``` in cmd/powershell, and selecting Screen Saver Settings. #### PC1 All works #### PC2 *This option could not be used with the company laptop.*. The "Screen Saver Settings" could not be opened. This has been disabled with a group policy. ### Option 2: Reg query With these settings ![image](https://github.com/fohrloop/wakepy/assets/17479683/8a2f1aaf-281d-448e-9ade-7d0a366cfc10) #### PC1 I get: ``` PS C:\Users\niko> reg query "HKEY_CURRENT_USER\Control Panel\Desktop" /v ScreenSaveActive HKEY_CURRENT_USER\Control Panel\Desktop ScreenSaveActive REG_SZ 1 PS C:\Users\niko> reg query "HKEY_CURRENT_USER\Control Panel\Desktop" /v ScreenSaverIsSecure HKEY_CURRENT_USER\Control Panel\Desktop ScreenSaverIsSecure REG_SZ 1 PS C:\Users\niko> reg query "HKEY_CURRENT_USER\Control Panel\Desktop" /v ScreenSaveTimeout HKEY_CURRENT_USER\Control Panel\Desktop ScreenSaveTimeout REG_SZ 60 ``` I checked also to change the settings. These are my observations - The `"HKEY_CURRENT_USER\Control Panel\Desktop" /v ScreenSaveTimeout` is the "Wait X minutes" / screen saver idle timer timeout value in seconds. - The `"HKEY_CURRENT_USER\Control Panel\Desktop" /v ScreenSaverIsSecure` is 1 if the "On resume, display log-on screen" is selected (True). - The ScreenSaveActive is still a mystery. #### PC2 *It says "ERROR: The system was unable to find the specified registry key or value."* on ScreenSaveTimeout and ScreenSaverIsSecure. It shows ``` HKEY_CURRENT_USER\Control Panel\Desktop ScreenSaveActive REG_SZ 1 ``` with ScreenSaveActive. The reason that there are no registry entries for ScreenSaveTimeout and ScreenSaverIsSecure is that they're set with GPOs (group policies), and the path is then "HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\Control Panel\Desktop" ### Option 3: Running this in Powershell: ``` Get-ItemProperty -Path "HKCU:\Control Panel\Desktop" ``` will show all the properties of the "HKCU:\Control Panel\Desktop", and adding `-Name ` will list only that (e.g. -Name ScreenSaveTimeOut). #### PC1 I can see ``` LockScreenAutoLockActive : 0 ScreenSaveTimeOut : 60 ScreenSaverIsSecure : 1 ``` in the output. The LockScreenAutoLockActive seems to be (based on quick Googling) some old entry, not related to any user setting. #### PC2 The ScreenSaveTimeout and ScreenSaverIsSecure were found in the another list (set by group policies): `Get-ItemProperty "HKCU:\Software\Policies\Microsoft\Windows\Control Panel\Desktop"` ``` ScreenSaveActive : 1 ScreenSaveTimeOut : 900 ScreenSaverIsSecure : 1 ``` ### Option 4: Using the SystemParametersInfoA from User32.dll The [SystemParametersInfoW](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow) function could be used to get screen saver related settings. - SPI_GETSCREENSAVEACTIVE -> Enable Screen Saver - SPI_GETSCREENSAVESECURE -> Password protect the screen saver - SPI_GETSCREENSAVETIMEOUT -> Get the screen saver idle timer timeout value (seconds) Some quick-and-dirty python code MWE ```python import ctypes # Determines whether the screen saver requires a password to display the Windows desktop. SPI_GETSCREENSAVESECURE = 0x0076 # Determines whether screen saving is enabled SPI_GETSCREENSAVEACTIVE = 0x0010 # Retrieves the screen saver time-out value, in seconds. SPI_GETSCREENSAVETIMEOUT = 0x000E # Same as ctypes.wintypes.BOOL(). # Got an error with Python 3.12.1 (from Microsoft Store) when trying to access wintypes, so not using that for bools. retval = ctypes.c_long() pvparam = ctypes.byref(retval) result = ctypes.windll.user32.SystemParametersInfoW(SPI_GETSCREENSAVEACTIVE, 0, pvparam, 0) print(retval.value) result = ctypes.windll.user32.SystemParametersInfoW(SPI_GETSCREENSAVESECURE, 0, pvparam, 0) print(retval.value) result = ctypes.windll.user32.SystemParametersInfoW(SPI_GETSCREENSAVETIMEOUT, 0, pvparam, 0) print(retval.value) ``` This prints on my PC1: ``` 1 1 60 ``` and PC2: ``` 1 1 900 ``` Therefore, the **SystemParametersInfoW** is an API which may be used to access *both*, the settings set by user in the Screen Saver Settings (saved in "HKCU:\Control Panel\Desktop"), and the values set by the group policies (in "HKCU:\Software\Policies\Microsoft\Windows\Control Panel\Desktop). This seems to be the simplest API.
What are the Windows default settings ## What are the Windows default settings - Some reddit comment from 2015 says, in [webcache.googleusercontent.com](https://webcache.googleusercontent.com/search?q=cache:p6i94OjXeXoJ:https://www.reddit.com/r/Surface/comments/3rkjqz/why_does_windows_ignore_the_password_timeout/&hl=en&gl=fi) > the screen saver has been disabled by default on Windows for years now. Most Windows 8/10 users don't use screen savers anymore, because the lock screen behavior and display power saving features makes it moot. - The windows on the Virtualbox also does not have sleep settings available (other than for the screen): ![image](https://github.com/fohrloop/wakepy/assets/17479683/e6d49585-b978-4eee-8bdc-361e8ad53b87) - I did try to run wakepy 0.7.2 keep.running (SetThreadExecutionState with system flag) for 18 minutes. Virtualbox was on full screen mode. The underlying Ubuntu did lock the screen at some point. After logging in to Ubuntu, I saw the test python script on the Win10 WM still running. There was no log-in required for Windows. I'm not sure if this this test helps at all and if things are different when running on a VM. ### Testing with Windows 10 Pro (VM on Virtualbox): - System: Windows 10 Pro, version 22H2, Build 19045.2965 The Lock Screen settings are not available if Windows is not Activated. ![image](https://github.com/fohrloop/wakepy/assets/17479683/d5dc55fb-fc2e-457b-bb98-2844e4212d81) The `Get-ItemProperty -Path "HKCU:\Control Panel\Desktop"` did not return ScreenSaveTimeout or ScreenSaverIsSecure, so I'm guessing Screen Saver is not somehow available..? Also the SystemParametersInfoW returns only zeroes: ``` >>> pvparam = ctypes.byref(retval) >>> result = ctypes.windll.user32.SystemParametersInfoW(SPI_GETSCREENSAVEACTIVE, 0, pvparam, 0) >>> print(retval.value) 0 >>> >>> result = ctypes.windll.user32.SystemParametersInfoW(SPI_GETSCREENSAVESECURE, 0, pvparam, 0) >>> print(retval.value) 0 >>> >>> result = ctypes.windll.user32.SystemParametersInfoW(SPI_GETSCREENSAVETIMEOUT, 0, pvparam, 0) >>> print(retval.value) 0 ``` ### Testing with the company laptop PC2 (Win 10 Enterprise) - PC2: company laptop (with company policies and restricted settings access, and "Windows/company default settings", Windows 10 Enterprise version 22H2 build 19045.3803). - Ran wakepy 0,7.2 `keep.running` mode, and left the computer on with the manual test script - After 34 minutes the computer still did not sleep. The test script was still printing with 2 second interval (as expected, as wakepy had set the thread executionstate) - The computer did blank the screen (it has 15 minute setting for it) - When moving the mouse, the **system asked to log in with a password!** Therefore, there is still some another setting, other than the found Screen Lock setting (ScreenSaverIsSecure from "HKCU:\Control Panel\Desktop") , which controls "Screen saver" type of behavior on enterprise /company owned Windows machines.
fohrloop commented 8 months ago

@ccrutchf I'm continuing the discussion here. I added my current understanding about the Windows sleep & lock states and the possible transitions in a diagram here. Not sure if it is perfect, but it can serve as basis for discussion.

When the SetThreadExecutionState is used with ES_SYSTEM_REQUIRED (keep.running) it is not possible to automatically get to the Sleep state which takes the upper right corner away (let's also forget manual sleep):

image

Result: Now the screen lock state will only be entered if there is a screen saver with password enabled. Not sure if this a problem of not.

Alternative approaches

1. only prevent suspend

Document well that wakepy keep.running mode only prevents suspend, and it does not guarantee that the system will lock automatically, if a screensaver with password (or a screen lock process) is not enabled.

2. prevent suspend + guarantee automatic screen lock

wakepy keep.running mode prevents suspend and guarantees that system will lock the screen automatically.

3. separate mode for guarantee automatic screen lock

wakepy keep.running mode which only prevents suspend. Add another context manager which guarantees automatic screen lock.

4. others?

fohrloop commented 8 months ago

@ccrutchf

PowerToys sets the precedent of not worrying about locking: https://github.com/microsoft/PowerToys/blob/c406a15099584a069374ab5531e1f711c5b53315/src/modules/awake/Awake/Core/Manager.cs

I interpreted your comment as "PowerToys does not lock the screen automatically but just uses SetThreadExecutionState to prevent automatic suspend. Because PowerToys is widely used, it can be used as an example, and people are used to that behavior. In other words, perhaps wakepy should not guarantee automatic screen lock". I agree with that :)

They even have an issue about this: https://github.com/microsoft/PowerToys/issues

Search 27178.

I checked it. It says

  1. Set Awake to keep awake indefinitely + keep screen on.

Let's look at the state transition diagram. Since ES_SYSTEM_REQUIRED and ES_DISPLAY_REQUIRED are both on, the automatic idle timer based transitions (ScreenSaveTimeout and suspend timer) are not possible:

image

  1. Put your computer to sleep.

This means that manual transition from Working to Sleep.

  1. Somehow awake the computer (i.e. via mouse) but do not unlock it (keep lock screen).

Now the system gets to the lock screen. This means that the user had "Require sign-in when PC wakes up from sleep" setting enabled. Now if you look at the situation, since ES_SYSTEM_REQUIRED and ES_DISPLAY_REQUIRED are both set, there are only two ways to get out of the lock screen: (4) unlocking with password (5) putting system manually back to sleep (with power button? not sure if possible otherwise)

image

NB: if Keep screen on is disabled PC will go to sleep again in 2 mins.

I would like to test if this is actually what happens. Note that is "keep screen" is disabled (there is no ES_DISPLAY_REQUIRED flag), the options look like this (additional option 6):

image

Commentary for PowerToys issue 27178

I would guess that most likely the route (6) occurs then automatically after the ScreenSaverTimeout. Then, as it is very common to have "blank" as the Screen Saver, the laptop screen will simply turn black. But the computer is not in sleep state, as it cannot enter sleep state automatically since ES_SYSTEM_REQUIRED prevents it. So without testing myself or seeing more proof, my guess is that what is actually happening is that the system ends up in the Screen saver state with "blank" screen saver.

PC should be able to go to sleep again in 2 mins (default value for Windows 10) if it hasn't been unlocked.

^-- If automatic sleep is prevented, the system will not automatically sleep. It will not matter if the starting state is the screen lock or the working state. I don't think that PowerToys (or wakepy) should disable itself when lock screen is turned on. Otherwise, when someone just starts a long running process with keep.running mode on, and locks the screen (for security), the mode would be terminated immediately and the process would not long more than few minutes.

Then other question is that if the mode should terminate itself if the system is put manually to sleep. I think it should not. The reasoning is that if something is running before (manual) sleep, it should be running after system resumes from sleep. That is how all programs work. Maybe there are some rare use cases where that kind of behavior is beneficial, but that should not be the automatic behaviour in wakepy, I think. And providing similar user experience cross-platform would probably be quite difficult. I guess that in a potential solution one somehow listen the system when it's going to sleep/resuming from sleep.

fohrloop commented 5 months ago

Labeled this as a bug although not sure if this can be thought of a bug in wakepy or as a feature of the Windows operating system.

fohrloop commented 4 months ago

A possible solution to this should consider also other systems, which may only lock screen automatically on certain situations. For example, on KDE Plasma on Kubuntu, screenlock is started when returning from sleep, or when it's enabled with a timer and the idle timer reaches the set value. It's possible that people have this automatic screensaver timer completely disabled. What should happen then? Should wakepy at least give a warning by default? See this comment on reddit.