keep-awake 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- keep_awake-1.2.0/LICENSE +21 -0
- keep_awake-1.2.0/MANIFEST.in +17 -0
- keep_awake-1.2.0/PKG-INFO +138 -0
- keep_awake-1.2.0/README.md +118 -0
- keep_awake-1.2.0/native_code/include/pm.h +11 -0
- keep_awake-1.2.0/native_code/src/ext.c +27 -0
- keep_awake-1.2.0/native_code/src/pm_macos.c +47 -0
- keep_awake-1.2.0/native_code/src/pm_windows.c +99 -0
- keep_awake-1.2.0/pyproject.toml +34 -0
- keep_awake-1.2.0/setup.cfg +4 -0
- keep_awake-1.2.0/setup.py +37 -0
- keep_awake-1.2.0/src/keep_awake/__init__.py +109 -0
- keep_awake-1.2.0/src/keep_awake/_native_api.pyi +18 -0
- keep_awake-1.2.0/src/keep_awake/dbus_api.py +112 -0
- keep_awake-1.2.0/src/keep_awake/py.typed +2 -0
- keep_awake-1.2.0/src/keep_awake.egg-info/PKG-INFO +138 -0
- keep_awake-1.2.0/src/keep_awake.egg-info/SOURCES.txt +18 -0
- keep_awake-1.2.0/src/keep_awake.egg-info/dependency_links.txt +1 -0
- keep_awake-1.2.0/src/keep_awake.egg-info/requires.txt +3 -0
- keep_awake-1.2.0/src/keep_awake.egg-info/top_level.txt +1 -0
keep_awake-1.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Kevin Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include pyproject.toml
|
|
4
|
+
include setup.py
|
|
5
|
+
|
|
6
|
+
recursive-include native_code *.c *.h
|
|
7
|
+
|
|
8
|
+
exclude AGENTS.md
|
|
9
|
+
exclude .gitignore
|
|
10
|
+
prune .venv
|
|
11
|
+
prune .github
|
|
12
|
+
prune .claude
|
|
13
|
+
prune .vscode
|
|
14
|
+
prune tests
|
|
15
|
+
prune docs
|
|
16
|
+
prune build
|
|
17
|
+
prune dist
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keep_awake
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Prevent your computer from going to sleep.
|
|
5
|
+
Author-email: Kevin Chen <1354016594@qq.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Home-page, https://github.com/czf0613/keep_awake_py
|
|
8
|
+
Keywords: sleep,prevent,awake,idle
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
11
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Provides-Extra: linux
|
|
17
|
+
Requires-Dist: jeepney>=0.9.0; extra == "linux"
|
|
18
|
+
Dynamic: license
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Keep Awake
|
|
22
|
+
|
|
23
|
+
Prevent idle system sleep and keep the display awake while Python is doing work.
|
|
24
|
+
Supports **CPython 3.8+**, including free-threaded (no-GIL) CPython 3.13/3.14.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
# Windows / macOS
|
|
30
|
+
pip install keep_awake
|
|
31
|
+
|
|
32
|
+
# Linux desktop
|
|
33
|
+
pip install 'keep_awake[linux]'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Windows and macOS use a C extension. A matching wheel avoids a local compiler;
|
|
37
|
+
otherwise installation builds from source. Linux uses the pure Python
|
|
38
|
+
[Jeepney](https://jeepney.readthedocs.io/en/latest/) D-Bus client and requires no
|
|
39
|
+
project C extension or D-Bus development headers.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from keep_awake import prevent_sleep, allow_sleep
|
|
45
|
+
|
|
46
|
+
if not prevent_sleep():
|
|
47
|
+
raise RuntimeError("Could not acquire a sleep inhibitor")
|
|
48
|
+
try:
|
|
49
|
+
# Do long-running work here.
|
|
50
|
+
pass
|
|
51
|
+
finally:
|
|
52
|
+
allow_sleep()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For automatic cleanup:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from keep_awake import KeepAwakeGuard
|
|
59
|
+
|
|
60
|
+
with KeepAwakeGuard():
|
|
61
|
+
# Do long-running work here.
|
|
62
|
+
pass
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- Every successful `prevent_sleep()` adds one reference and returns `True`.
|
|
66
|
+
Failure returns `False` without adding a reference.
|
|
67
|
+
- Pair each successful acquisition with one `allow_sleep()`, which returns `None`.
|
|
68
|
+
The OS inhibitor is released only when the shared reference count reaches zero.
|
|
69
|
+
Calling `allow_sleep()` when the count is already zero does nothing.
|
|
70
|
+
- Calls are synchronized across threads, including no-GIL builds. All callers
|
|
71
|
+
share **one OS inhibitor with a reference count**. If thread A acquires and
|
|
72
|
+
thread B acquires then releases, the count changes `1 → 2 → 1`, so sleep remains
|
|
73
|
+
inhibited until A releases. A release may happen on a different thread.
|
|
74
|
+
- Use the public API only. Calling the private C extension directly bypasses
|
|
75
|
+
synchronization, reference counting and exit cleanup, and is unsupported.
|
|
76
|
+
- Nested and overlapping `KeepAwakeGuard` scopes are supported, including reuse
|
|
77
|
+
of the same guard. Each scope releases only its own successful acquisition.
|
|
78
|
+
Acquisition failure still does not raise; use the boolean-returning API when
|
|
79
|
+
success is required.
|
|
80
|
+
- On normal interpreter shutdown, one shared `atexit` handler releases the backend
|
|
81
|
+
even if several references remain. Acquisitions after this cleanup return `False`.
|
|
82
|
+
Forced termination, interpreter crashes and `os._exit()` bypass this handler;
|
|
83
|
+
use a guard or `try/finally` for cleanup during normal execution.
|
|
84
|
+
These requests do not override manual sleep, lid closure, or administrator power
|
|
85
|
+
policy.
|
|
86
|
+
|
|
87
|
+
## Platforms
|
|
88
|
+
|
|
89
|
+
| Platform | Implementation |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| macOS arm64 / x86_64 | IOKit display-idle assertion, serialized by the Python API |
|
|
92
|
+
| Windows x86_64 / ARM64 | `SetThreadExecutionState` on a dedicated, synchronized worker |
|
|
93
|
+
| Linux desktop | GNOME SessionManager, falling back to `org.freedesktop.ScreenSaver` |
|
|
94
|
+
|
|
95
|
+
On Linux, run inside a logged-in graphical session with
|
|
96
|
+
`DBUS_SESSION_BUS_ADDRESS` set and a supported inhibition service. GNOME requests
|
|
97
|
+
suspend and idle inhibition; the freedesktop fallback depends on the desktop's
|
|
98
|
+
implementation of idle inhibition. Headless servers and minimal compositors may
|
|
99
|
+
not provide either service. An unavailable bus/service returns `False`; omitting
|
|
100
|
+
the required `linux` extra produces a dependency import error.
|
|
101
|
+
|
|
102
|
+
The connection opens on first acquisition and closes on release, including when
|
|
103
|
+
release fails. If the desktop/session service restarts, release and reacquire the
|
|
104
|
+
inhibitor. Actual GNOME/KDE power behavior still requires desktop validation.
|
|
105
|
+
|
|
106
|
+
Native Windows ARM64 wheels are built for Python 3.11–3.14 and 3.13t/3.14t.
|
|
107
|
+
Python 3.8–3.10 remains supported on Windows x86_64. This matches the native ARM64
|
|
108
|
+
interpreters available through uv.
|
|
109
|
+
|
|
110
|
+
## Migrating from the idempotent API
|
|
111
|
+
|
|
112
|
+
Repeated successful calls now require matching releases. Code that previously
|
|
113
|
+
called `prevent_sleep()` many times but released only once must balance its calls
|
|
114
|
+
or use a guard around each unit of work. Do not manually release an acquisition
|
|
115
|
+
already owned by a guard, or release after a failed acquisition: the counter is
|
|
116
|
+
shared and cannot identify a mismatched manual release.
|
|
117
|
+
|
|
118
|
+
## Development and publishing
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
uv sync --frozen
|
|
122
|
+
uv run --frozen pytest -q
|
|
123
|
+
uv build
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
CI checks Python 3.8–3.14 and free-threaded 3.13t/3.14t on Linux, Windows x86_64 and
|
|
127
|
+
both macOS architectures, plus the six Windows ARM64 configurations above,
|
|
128
|
+
whenever changes reach `master` or a pull request targets it.
|
|
129
|
+
Publishing a GitHub Release runs the checks and builds before uploading the
|
|
130
|
+
sdist and native wheels to PyPI through Trusted Publishing.
|
|
131
|
+
|
|
132
|
+
See [development and release instructions](https://github.com/czf0613/keep_awake_py/blob/master/docs/DEVELOPMENT.md),
|
|
133
|
+
[compatibility findings and verification limits](https://github.com/czf0613/keep_awake_py/blob/master/docs/COMPATIBILITY.md), and
|
|
134
|
+
[Codex repository guidance](https://github.com/czf0613/keep_awake_py/blob/master/AGENTS.md).
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
[MIT](https://github.com/czf0613/keep_awake_py/blob/master/LICENSE)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Keep Awake
|
|
2
|
+
|
|
3
|
+
Prevent idle system sleep and keep the display awake while Python is doing work.
|
|
4
|
+
Supports **CPython 3.8+**, including free-threaded (no-GIL) CPython 3.13/3.14.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
# Windows / macOS
|
|
10
|
+
pip install keep_awake
|
|
11
|
+
|
|
12
|
+
# Linux desktop
|
|
13
|
+
pip install 'keep_awake[linux]'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Windows and macOS use a C extension. A matching wheel avoids a local compiler;
|
|
17
|
+
otherwise installation builds from source. Linux uses the pure Python
|
|
18
|
+
[Jeepney](https://jeepney.readthedocs.io/en/latest/) D-Bus client and requires no
|
|
19
|
+
project C extension or D-Bus development headers.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from keep_awake import prevent_sleep, allow_sleep
|
|
25
|
+
|
|
26
|
+
if not prevent_sleep():
|
|
27
|
+
raise RuntimeError("Could not acquire a sleep inhibitor")
|
|
28
|
+
try:
|
|
29
|
+
# Do long-running work here.
|
|
30
|
+
pass
|
|
31
|
+
finally:
|
|
32
|
+
allow_sleep()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For automatic cleanup:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from keep_awake import KeepAwakeGuard
|
|
39
|
+
|
|
40
|
+
with KeepAwakeGuard():
|
|
41
|
+
# Do long-running work here.
|
|
42
|
+
pass
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- Every successful `prevent_sleep()` adds one reference and returns `True`.
|
|
46
|
+
Failure returns `False` without adding a reference.
|
|
47
|
+
- Pair each successful acquisition with one `allow_sleep()`, which returns `None`.
|
|
48
|
+
The OS inhibitor is released only when the shared reference count reaches zero.
|
|
49
|
+
Calling `allow_sleep()` when the count is already zero does nothing.
|
|
50
|
+
- Calls are synchronized across threads, including no-GIL builds. All callers
|
|
51
|
+
share **one OS inhibitor with a reference count**. If thread A acquires and
|
|
52
|
+
thread B acquires then releases, the count changes `1 → 2 → 1`, so sleep remains
|
|
53
|
+
inhibited until A releases. A release may happen on a different thread.
|
|
54
|
+
- Use the public API only. Calling the private C extension directly bypasses
|
|
55
|
+
synchronization, reference counting and exit cleanup, and is unsupported.
|
|
56
|
+
- Nested and overlapping `KeepAwakeGuard` scopes are supported, including reuse
|
|
57
|
+
of the same guard. Each scope releases only its own successful acquisition.
|
|
58
|
+
Acquisition failure still does not raise; use the boolean-returning API when
|
|
59
|
+
success is required.
|
|
60
|
+
- On normal interpreter shutdown, one shared `atexit` handler releases the backend
|
|
61
|
+
even if several references remain. Acquisitions after this cleanup return `False`.
|
|
62
|
+
Forced termination, interpreter crashes and `os._exit()` bypass this handler;
|
|
63
|
+
use a guard or `try/finally` for cleanup during normal execution.
|
|
64
|
+
These requests do not override manual sleep, lid closure, or administrator power
|
|
65
|
+
policy.
|
|
66
|
+
|
|
67
|
+
## Platforms
|
|
68
|
+
|
|
69
|
+
| Platform | Implementation |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| macOS arm64 / x86_64 | IOKit display-idle assertion, serialized by the Python API |
|
|
72
|
+
| Windows x86_64 / ARM64 | `SetThreadExecutionState` on a dedicated, synchronized worker |
|
|
73
|
+
| Linux desktop | GNOME SessionManager, falling back to `org.freedesktop.ScreenSaver` |
|
|
74
|
+
|
|
75
|
+
On Linux, run inside a logged-in graphical session with
|
|
76
|
+
`DBUS_SESSION_BUS_ADDRESS` set and a supported inhibition service. GNOME requests
|
|
77
|
+
suspend and idle inhibition; the freedesktop fallback depends on the desktop's
|
|
78
|
+
implementation of idle inhibition. Headless servers and minimal compositors may
|
|
79
|
+
not provide either service. An unavailable bus/service returns `False`; omitting
|
|
80
|
+
the required `linux` extra produces a dependency import error.
|
|
81
|
+
|
|
82
|
+
The connection opens on first acquisition and closes on release, including when
|
|
83
|
+
release fails. If the desktop/session service restarts, release and reacquire the
|
|
84
|
+
inhibitor. Actual GNOME/KDE power behavior still requires desktop validation.
|
|
85
|
+
|
|
86
|
+
Native Windows ARM64 wheels are built for Python 3.11–3.14 and 3.13t/3.14t.
|
|
87
|
+
Python 3.8–3.10 remains supported on Windows x86_64. This matches the native ARM64
|
|
88
|
+
interpreters available through uv.
|
|
89
|
+
|
|
90
|
+
## Migrating from the idempotent API
|
|
91
|
+
|
|
92
|
+
Repeated successful calls now require matching releases. Code that previously
|
|
93
|
+
called `prevent_sleep()` many times but released only once must balance its calls
|
|
94
|
+
or use a guard around each unit of work. Do not manually release an acquisition
|
|
95
|
+
already owned by a guard, or release after a failed acquisition: the counter is
|
|
96
|
+
shared and cannot identify a mismatched manual release.
|
|
97
|
+
|
|
98
|
+
## Development and publishing
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
uv sync --frozen
|
|
102
|
+
uv run --frozen pytest -q
|
|
103
|
+
uv build
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
CI checks Python 3.8–3.14 and free-threaded 3.13t/3.14t on Linux, Windows x86_64 and
|
|
107
|
+
both macOS architectures, plus the six Windows ARM64 configurations above,
|
|
108
|
+
whenever changes reach `master` or a pull request targets it.
|
|
109
|
+
Publishing a GitHub Release runs the checks and builds before uploading the
|
|
110
|
+
sdist and native wheels to PyPI through Trusted Publishing.
|
|
111
|
+
|
|
112
|
+
See [development and release instructions](https://github.com/czf0613/keep_awake_py/blob/master/docs/DEVELOPMENT.md),
|
|
113
|
+
[compatibility findings and verification limits](https://github.com/czf0613/keep_awake_py/blob/master/docs/COMPATIBILITY.md), and
|
|
114
|
+
[Codex repository guidance](https://github.com/czf0613/keep_awake_py/blob/master/AGENTS.md).
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
[MIT](https://github.com/czf0613/keep_awake_py/blob/master/LICENSE)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#include <Python.h>
|
|
2
|
+
#include "pm.h"
|
|
3
|
+
|
|
4
|
+
static PyMethodDef ModMethods[] = {
|
|
5
|
+
{"_prevent_sleep", pm_prevent_sleep, METH_NOARGS, NULL},
|
|
6
|
+
{"_allow_sleep", pm_allow_sleep, METH_NOARGS, NULL},
|
|
7
|
+
{NULL, NULL, 0, NULL}};
|
|
8
|
+
|
|
9
|
+
static struct PyModuleDef module = {
|
|
10
|
+
PyModuleDef_HEAD_INIT,
|
|
11
|
+
"_native_api",
|
|
12
|
+
NULL,
|
|
13
|
+
-1,
|
|
14
|
+
ModMethods};
|
|
15
|
+
|
|
16
|
+
PyMODINIT_FUNC PyInit__native_api(void)
|
|
17
|
+
{
|
|
18
|
+
PyObject *m = PyModule_Create(&module);
|
|
19
|
+
if (m == NULL)
|
|
20
|
+
{
|
|
21
|
+
return NULL;
|
|
22
|
+
}
|
|
23
|
+
#ifdef Py_GIL_DISABLED
|
|
24
|
+
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
|
|
25
|
+
#endif
|
|
26
|
+
return m;
|
|
27
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#include "pm.h"
|
|
2
|
+
#include <IOKit/pwr_mgt/IOPMLib.h>
|
|
3
|
+
#include <CoreFoundation/CoreFoundation.h>
|
|
4
|
+
|
|
5
|
+
/* All entry points require the public Python API's lock. */
|
|
6
|
+
static IOPMAssertionID sleepAssertion = kIOPMNullAssertionID;
|
|
7
|
+
#define reasonForActive CFSTR("需要保持系统活动以确保后台任务执行")
|
|
8
|
+
|
|
9
|
+
static bool prevent_sleep(void)
|
|
10
|
+
{
|
|
11
|
+
bool success = true;
|
|
12
|
+
if (sleepAssertion == kIOPMNullAssertionID)
|
|
13
|
+
{
|
|
14
|
+
IOReturn status = IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn, reasonForActive, &sleepAssertion);
|
|
15
|
+
|
|
16
|
+
success &= (status == kIOReturnSuccess);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return success;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static void allow_sleep(void)
|
|
23
|
+
{
|
|
24
|
+
if (sleepAssertion != kIOPMNullAssertionID)
|
|
25
|
+
{
|
|
26
|
+
IOPMAssertionRelease(sleepAssertion);
|
|
27
|
+
sleepAssertion = kIOPMNullAssertionID;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
PyObject *pm_prevent_sleep(PyObject *self, PyObject *args)
|
|
32
|
+
{
|
|
33
|
+
if (prevent_sleep())
|
|
34
|
+
{
|
|
35
|
+
Py_RETURN_TRUE;
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
{
|
|
39
|
+
Py_RETURN_FALSE;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
PyObject *pm_allow_sleep(PyObject *self, PyObject *args)
|
|
44
|
+
{
|
|
45
|
+
allow_sleep();
|
|
46
|
+
Py_RETURN_NONE;
|
|
47
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#include "pm.h"
|
|
2
|
+
#include <windows.h>
|
|
3
|
+
|
|
4
|
+
/* All entry points require the public Python API's lock.
|
|
5
|
+
* The ready/stop events synchronize communication with the native worker. */
|
|
6
|
+
static HANDLE h_thread = NULL;
|
|
7
|
+
static HANDLE stop_event = NULL;
|
|
8
|
+
static HANDLE ready_event = NULL;
|
|
9
|
+
static bool started = false;
|
|
10
|
+
|
|
11
|
+
static DWORD WINAPI run_forever(LPVOID args)
|
|
12
|
+
{
|
|
13
|
+
started = SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED) != 0;
|
|
14
|
+
SetEvent(ready_event);
|
|
15
|
+
if (started)
|
|
16
|
+
{
|
|
17
|
+
WaitForSingleObject(stop_event, INFINITE);
|
|
18
|
+
SetThreadExecutionState(ES_CONTINUOUS);
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Called with the public Python lock held, after any worker has exited. */
|
|
24
|
+
static void close_handles(void)
|
|
25
|
+
{
|
|
26
|
+
if (h_thread != NULL)
|
|
27
|
+
{
|
|
28
|
+
CloseHandle(h_thread);
|
|
29
|
+
h_thread = NULL;
|
|
30
|
+
}
|
|
31
|
+
if (stop_event != NULL)
|
|
32
|
+
{
|
|
33
|
+
CloseHandle(stop_event);
|
|
34
|
+
stop_event = NULL;
|
|
35
|
+
}
|
|
36
|
+
if (ready_event != NULL)
|
|
37
|
+
{
|
|
38
|
+
CloseHandle(ready_event);
|
|
39
|
+
ready_event = NULL;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static bool prevent_sleep(void)
|
|
44
|
+
{
|
|
45
|
+
if (h_thread != NULL)
|
|
46
|
+
{
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
stop_event = CreateEventW(NULL, TRUE, FALSE, NULL);
|
|
50
|
+
ready_event = CreateEventW(NULL, TRUE, FALSE, NULL);
|
|
51
|
+
if (stop_event == NULL || ready_event == NULL)
|
|
52
|
+
{
|
|
53
|
+
close_handles();
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
started = false;
|
|
57
|
+
h_thread = CreateThread(NULL, 0, run_forever, NULL, 0, NULL);
|
|
58
|
+
if (h_thread == NULL)
|
|
59
|
+
{
|
|
60
|
+
close_handles();
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
WaitForSingleObject(ready_event, INFINITE);
|
|
64
|
+
bool success = started;
|
|
65
|
+
if (!success)
|
|
66
|
+
{
|
|
67
|
+
WaitForSingleObject(h_thread, INFINITE);
|
|
68
|
+
close_handles();
|
|
69
|
+
}
|
|
70
|
+
return success;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static void allow_sleep(void)
|
|
74
|
+
{
|
|
75
|
+
if (h_thread != NULL)
|
|
76
|
+
{
|
|
77
|
+
SetEvent(stop_event);
|
|
78
|
+
WaitForSingleObject(h_thread, INFINITE);
|
|
79
|
+
close_handles();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
PyObject *pm_prevent_sleep(PyObject *self, PyObject *args)
|
|
84
|
+
{
|
|
85
|
+
if (prevent_sleep())
|
|
86
|
+
{
|
|
87
|
+
Py_RETURN_TRUE;
|
|
88
|
+
}
|
|
89
|
+
else
|
|
90
|
+
{
|
|
91
|
+
Py_RETURN_FALSE;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
PyObject *pm_allow_sleep(PyObject *self, PyObject *args)
|
|
96
|
+
{
|
|
97
|
+
allow_sleep();
|
|
98
|
+
Py_RETURN_NONE;
|
|
99
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "keep_awake"
|
|
3
|
+
version = "1.2.0"
|
|
4
|
+
description = "Prevent your computer from going to sleep."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
keywords = ["sleep", "prevent", "awake", "idle"]
|
|
7
|
+
authors = [{ name = "Kevin Chen", email = "1354016594@qq.com" }]
|
|
8
|
+
dynamic = ["license"]
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Operating System :: Microsoft :: Windows",
|
|
13
|
+
"Operating System :: MacOS :: MacOS X",
|
|
14
|
+
"Operating System :: POSIX :: Linux",
|
|
15
|
+
]
|
|
16
|
+
dependencies = []
|
|
17
|
+
optional-dependencies = { linux = ["jeepney>=0.9.0"] }
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Home-page = "https://github.com/czf0613/keep_awake_py"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["setuptools>=64", "wheel"]
|
|
24
|
+
build-backend = "setuptools.build_meta"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest>=7.0.1",
|
|
29
|
+
"setuptools>=64",
|
|
30
|
+
"jeepney>=0.9.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
markers = ["desktop: requires a real Linux desktop session (opt in with KEEP_AWAKE_DESKTOP_TEST=1)"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from setuptools import setup, Extension
|
|
2
|
+
import sys
|
|
3
|
+
import sysconfig
|
|
4
|
+
|
|
5
|
+
c_modules = []
|
|
6
|
+
os_platform = sys.platform
|
|
7
|
+
macros = (
|
|
8
|
+
[("Py_GIL_DISABLED", "1")] if sysconfig.get_config_var("Py_GIL_DISABLED") else []
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
if os_platform == "win32":
|
|
12
|
+
c_modules.append(
|
|
13
|
+
Extension(
|
|
14
|
+
"keep_awake._native_api",
|
|
15
|
+
sources=["native_code/src/pm_windows.c", "native_code/src/ext.c"],
|
|
16
|
+
include_dirs=["native_code/include"],
|
|
17
|
+
extra_compile_args=["/utf-8"],
|
|
18
|
+
define_macros=macros,
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
elif os_platform == "darwin":
|
|
22
|
+
c_modules.append(
|
|
23
|
+
Extension(
|
|
24
|
+
"keep_awake._native_api",
|
|
25
|
+
sources=["native_code/src/pm_macos.c", "native_code/src/ext.c"],
|
|
26
|
+
include_dirs=["native_code/include"],
|
|
27
|
+
extra_link_args=["-framework", "CoreFoundation", "-framework", "IOKit"],
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
elif os_platform == "linux":
|
|
31
|
+
# nothing todo, native python implementation
|
|
32
|
+
pass
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError("Unsupported platform")
|
|
35
|
+
|
|
36
|
+
# SPDX project.license strings require setuptools versions unavailable on Python 3.8.
|
|
37
|
+
setup(ext_modules=c_modules, license="MIT")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import sys
|
|
3
|
+
from threading import Lock, local
|
|
4
|
+
from typing import final
|
|
5
|
+
|
|
6
|
+
__all__ = ["prevent_sleep", "allow_sleep", "KeepAwakeGuard"]
|
|
7
|
+
os_platform = sys.platform
|
|
8
|
+
_mutex = Lock()
|
|
9
|
+
_references = 0
|
|
10
|
+
_shutting_down = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def prevent_sleep() -> bool:
|
|
14
|
+
"""Acquire one sleep-prevention reference; failed requests are not counted."""
|
|
15
|
+
global _references
|
|
16
|
+
with _mutex:
|
|
17
|
+
if _shutting_down:
|
|
18
|
+
return False
|
|
19
|
+
if _references == 0 and not _acquire_backend():
|
|
20
|
+
return False
|
|
21
|
+
_references += 1
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def allow_sleep() -> None:
|
|
26
|
+
"""Release one reference, allowing sleep only after the last release."""
|
|
27
|
+
global _references
|
|
28
|
+
with _mutex:
|
|
29
|
+
if _references == 0:
|
|
30
|
+
return
|
|
31
|
+
if _references == 1:
|
|
32
|
+
try:
|
|
33
|
+
_release_backend()
|
|
34
|
+
finally:
|
|
35
|
+
# Backends clean up even if release is interrupted. Never cache
|
|
36
|
+
# success after the underlying inhibitor may have been removed.
|
|
37
|
+
_references = 0
|
|
38
|
+
else:
|
|
39
|
+
_references -= 1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _shutdown() -> None:
|
|
43
|
+
"""Release all outstanding references and prevent new work during exit."""
|
|
44
|
+
global _references, _shutting_down
|
|
45
|
+
with _mutex:
|
|
46
|
+
_shutting_down = True
|
|
47
|
+
try:
|
|
48
|
+
if _references:
|
|
49
|
+
_release_backend()
|
|
50
|
+
finally:
|
|
51
|
+
_references = 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _acquire_backend() -> bool:
|
|
55
|
+
|
|
56
|
+
# in macOS and windows, just call the native api
|
|
57
|
+
if os_platform in ["darwin", "win32"]:
|
|
58
|
+
from ._native_api import _prevent_sleep
|
|
59
|
+
|
|
60
|
+
return _prevent_sleep()
|
|
61
|
+
elif os_platform == "linux":
|
|
62
|
+
# in linux, use dbus to send a message to avoid sleep
|
|
63
|
+
from .dbus_api import session_on
|
|
64
|
+
|
|
65
|
+
return session_on()
|
|
66
|
+
else:
|
|
67
|
+
raise NotImplementedError(f"Platform '{os_platform}' is not supported.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _release_backend() -> None:
|
|
71
|
+
if os_platform in ["darwin", "win32"]:
|
|
72
|
+
from ._native_api import _allow_sleep
|
|
73
|
+
|
|
74
|
+
_allow_sleep()
|
|
75
|
+
elif os_platform == "linux":
|
|
76
|
+
from .dbus_api import session_off
|
|
77
|
+
|
|
78
|
+
session_off()
|
|
79
|
+
else:
|
|
80
|
+
raise NotImplementedError(f"Platform '{os_platform}' is not supported.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
atexit.register(_shutdown)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@final
|
|
87
|
+
class KeepAwakeGuard:
|
|
88
|
+
"""Own a reference for each successful entry, including nested scopes."""
|
|
89
|
+
|
|
90
|
+
def __init__(self):
|
|
91
|
+
# One guard may be nested or shared across threads. Failed entries must
|
|
92
|
+
# never consume another entry's successful acquisition.
|
|
93
|
+
self._state = local()
|
|
94
|
+
|
|
95
|
+
def __enter__(self):
|
|
96
|
+
if not hasattr(self._state, "entries"):
|
|
97
|
+
self._state.entries = []
|
|
98
|
+
entries = self._state.entries
|
|
99
|
+
entries.append(False)
|
|
100
|
+
try:
|
|
101
|
+
entries[-1] = prevent_sleep()
|
|
102
|
+
except BaseException:
|
|
103
|
+
entries.pop()
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
107
|
+
entries = getattr(self._state, "entries", ())
|
|
108
|
+
if entries and entries.pop():
|
|
109
|
+
allow_sleep()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Native API for keep_awake module
|
|
2
|
+
|
|
3
|
+
def _prevent_sleep() -> bool:
|
|
4
|
+
"""Prevent the system from sleeping. Returns True if successful, False otherwise.
|
|
5
|
+
Now the screen will not turn off and system will not go to sleep.
|
|
6
|
+
Requires the public Python API's lock, including on free-threaded CPython.
|
|
7
|
+
Direct calls to this private extension are unsupported and are not synchronized.
|
|
8
|
+
Repeated calls share one process-wide inhibitor; they are not reference counted.
|
|
9
|
+
The public Python API manages reference counting around this private backend.
|
|
10
|
+
"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
def _allow_sleep() -> None:
|
|
14
|
+
"""Release the inhibitor; requires the public Python API's lock.
|
|
15
|
+
|
|
16
|
+
Sequential repeated releases are safe. Direct calls are unsupported.
|
|
17
|
+
"""
|
|
18
|
+
pass
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Linux desktop inhibition over a private, lazily opened D-Bus connection."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from threading import Lock
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from jeepney import DBusAddress, DBusErrorResponse, MessageType, new_method_call
|
|
8
|
+
from jeepney.io.blocking import DBusConnection, open_dbus_connection
|
|
9
|
+
|
|
10
|
+
_logger = logging.getLogger(__name__)
|
|
11
|
+
_mutex = Lock()
|
|
12
|
+
_connection: Optional[DBusConnection] = None
|
|
13
|
+
_cookie: Optional[int] = None
|
|
14
|
+
_backend = None
|
|
15
|
+
_timeout = 5.0
|
|
16
|
+
_app_id = "org.python.keep_awake"
|
|
17
|
+
_reason = "Keep system and screen awake"
|
|
18
|
+
|
|
19
|
+
# GNOME flags: suspend (4) | idle (8). Method capitalization differs by API.
|
|
20
|
+
_backends = (
|
|
21
|
+
(
|
|
22
|
+
DBusAddress(
|
|
23
|
+
"/org/gnome/SessionManager",
|
|
24
|
+
"org.gnome.SessionManager",
|
|
25
|
+
"org.gnome.SessionManager",
|
|
26
|
+
),
|
|
27
|
+
"susu",
|
|
28
|
+
(_app_id, 0, _reason, 12),
|
|
29
|
+
"Uninhibit",
|
|
30
|
+
),
|
|
31
|
+
(
|
|
32
|
+
DBusAddress(
|
|
33
|
+
"/org/freedesktop/ScreenSaver",
|
|
34
|
+
"org.freedesktop.ScreenSaver",
|
|
35
|
+
"org.freedesktop.ScreenSaver",
|
|
36
|
+
),
|
|
37
|
+
"ss",
|
|
38
|
+
(_app_id, _reason),
|
|
39
|
+
"UnInhibit",
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _call(connection, address, method, signature, body):
|
|
45
|
+
message = new_method_call(address, method, signature, body)
|
|
46
|
+
reply = connection.send_and_get_reply(message, timeout=_timeout)
|
|
47
|
+
if reply.header.message_type == MessageType.error:
|
|
48
|
+
raise DBusErrorResponse(reply)
|
|
49
|
+
return reply.body
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _close(connection):
|
|
53
|
+
try:
|
|
54
|
+
connection.close()
|
|
55
|
+
except OSError:
|
|
56
|
+
_logger.debug("Failed to close the D-Bus connection", exc_info=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def session_on() -> bool:
|
|
60
|
+
"""Acquire one process-wide inhibitor, returning False if unavailable."""
|
|
61
|
+
global _connection, _cookie, _backend
|
|
62
|
+
with _mutex:
|
|
63
|
+
if _cookie is not None:
|
|
64
|
+
return True
|
|
65
|
+
connection = None
|
|
66
|
+
try:
|
|
67
|
+
connection = open_dbus_connection(bus="SESSION")
|
|
68
|
+
for backend in _backends:
|
|
69
|
+
address, signature, body, _ = backend
|
|
70
|
+
try:
|
|
71
|
+
result = _call(connection, address, "Inhibit", signature, body)
|
|
72
|
+
except DBusErrorResponse:
|
|
73
|
+
continue
|
|
74
|
+
if (
|
|
75
|
+
len(result) != 1
|
|
76
|
+
or type(result[0]) is not int
|
|
77
|
+
or not 0 <= result[0] <= 0xFFFFFFFF
|
|
78
|
+
):
|
|
79
|
+
raise ValueError("Inhibit must return a UInt32 cookie")
|
|
80
|
+
_connection, _cookie, _backend = connection, result[0], backend
|
|
81
|
+
return True
|
|
82
|
+
except (
|
|
83
|
+
DBusErrorResponse,
|
|
84
|
+
OSError,
|
|
85
|
+
EOFError,
|
|
86
|
+
KeyError,
|
|
87
|
+
ValueError,
|
|
88
|
+
RuntimeError,
|
|
89
|
+
StopIteration,
|
|
90
|
+
) as exc:
|
|
91
|
+
_logger.debug("Unable to inhibit the desktop session: %s", exc)
|
|
92
|
+
finally:
|
|
93
|
+
if connection is not None and connection is not _connection:
|
|
94
|
+
# Also releases an inhibitor whose reply was lost, malformed,
|
|
95
|
+
# or interrupted before ownership transferred to module state.
|
|
96
|
+
_close(connection)
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def session_off() -> None:
|
|
101
|
+
"""Release the inhibitor; disconnect even when the service cannot reply."""
|
|
102
|
+
global _connection, _cookie, _backend
|
|
103
|
+
with _mutex:
|
|
104
|
+
if _connection is None:
|
|
105
|
+
return
|
|
106
|
+
try:
|
|
107
|
+
_call(_connection, _backend[0], _backend[3], "u", (_cookie,))
|
|
108
|
+
except (DBusErrorResponse, OSError, EOFError, ValueError) as exc:
|
|
109
|
+
_logger.debug("Unable to release the desktop inhibitor: %s", exc)
|
|
110
|
+
finally:
|
|
111
|
+
_close(_connection)
|
|
112
|
+
_connection, _cookie, _backend = None, None, None
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keep_awake
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Prevent your computer from going to sleep.
|
|
5
|
+
Author-email: Kevin Chen <1354016594@qq.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Home-page, https://github.com/czf0613/keep_awake_py
|
|
8
|
+
Keywords: sleep,prevent,awake,idle
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
11
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Provides-Extra: linux
|
|
17
|
+
Requires-Dist: jeepney>=0.9.0; extra == "linux"
|
|
18
|
+
Dynamic: license
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Keep Awake
|
|
22
|
+
|
|
23
|
+
Prevent idle system sleep and keep the display awake while Python is doing work.
|
|
24
|
+
Supports **CPython 3.8+**, including free-threaded (no-GIL) CPython 3.13/3.14.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
# Windows / macOS
|
|
30
|
+
pip install keep_awake
|
|
31
|
+
|
|
32
|
+
# Linux desktop
|
|
33
|
+
pip install 'keep_awake[linux]'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Windows and macOS use a C extension. A matching wheel avoids a local compiler;
|
|
37
|
+
otherwise installation builds from source. Linux uses the pure Python
|
|
38
|
+
[Jeepney](https://jeepney.readthedocs.io/en/latest/) D-Bus client and requires no
|
|
39
|
+
project C extension or D-Bus development headers.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from keep_awake import prevent_sleep, allow_sleep
|
|
45
|
+
|
|
46
|
+
if not prevent_sleep():
|
|
47
|
+
raise RuntimeError("Could not acquire a sleep inhibitor")
|
|
48
|
+
try:
|
|
49
|
+
# Do long-running work here.
|
|
50
|
+
pass
|
|
51
|
+
finally:
|
|
52
|
+
allow_sleep()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For automatic cleanup:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from keep_awake import KeepAwakeGuard
|
|
59
|
+
|
|
60
|
+
with KeepAwakeGuard():
|
|
61
|
+
# Do long-running work here.
|
|
62
|
+
pass
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- Every successful `prevent_sleep()` adds one reference and returns `True`.
|
|
66
|
+
Failure returns `False` without adding a reference.
|
|
67
|
+
- Pair each successful acquisition with one `allow_sleep()`, which returns `None`.
|
|
68
|
+
The OS inhibitor is released only when the shared reference count reaches zero.
|
|
69
|
+
Calling `allow_sleep()` when the count is already zero does nothing.
|
|
70
|
+
- Calls are synchronized across threads, including no-GIL builds. All callers
|
|
71
|
+
share **one OS inhibitor with a reference count**. If thread A acquires and
|
|
72
|
+
thread B acquires then releases, the count changes `1 → 2 → 1`, so sleep remains
|
|
73
|
+
inhibited until A releases. A release may happen on a different thread.
|
|
74
|
+
- Use the public API only. Calling the private C extension directly bypasses
|
|
75
|
+
synchronization, reference counting and exit cleanup, and is unsupported.
|
|
76
|
+
- Nested and overlapping `KeepAwakeGuard` scopes are supported, including reuse
|
|
77
|
+
of the same guard. Each scope releases only its own successful acquisition.
|
|
78
|
+
Acquisition failure still does not raise; use the boolean-returning API when
|
|
79
|
+
success is required.
|
|
80
|
+
- On normal interpreter shutdown, one shared `atexit` handler releases the backend
|
|
81
|
+
even if several references remain. Acquisitions after this cleanup return `False`.
|
|
82
|
+
Forced termination, interpreter crashes and `os._exit()` bypass this handler;
|
|
83
|
+
use a guard or `try/finally` for cleanup during normal execution.
|
|
84
|
+
These requests do not override manual sleep, lid closure, or administrator power
|
|
85
|
+
policy.
|
|
86
|
+
|
|
87
|
+
## Platforms
|
|
88
|
+
|
|
89
|
+
| Platform | Implementation |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| macOS arm64 / x86_64 | IOKit display-idle assertion, serialized by the Python API |
|
|
92
|
+
| Windows x86_64 / ARM64 | `SetThreadExecutionState` on a dedicated, synchronized worker |
|
|
93
|
+
| Linux desktop | GNOME SessionManager, falling back to `org.freedesktop.ScreenSaver` |
|
|
94
|
+
|
|
95
|
+
On Linux, run inside a logged-in graphical session with
|
|
96
|
+
`DBUS_SESSION_BUS_ADDRESS` set and a supported inhibition service. GNOME requests
|
|
97
|
+
suspend and idle inhibition; the freedesktop fallback depends on the desktop's
|
|
98
|
+
implementation of idle inhibition. Headless servers and minimal compositors may
|
|
99
|
+
not provide either service. An unavailable bus/service returns `False`; omitting
|
|
100
|
+
the required `linux` extra produces a dependency import error.
|
|
101
|
+
|
|
102
|
+
The connection opens on first acquisition and closes on release, including when
|
|
103
|
+
release fails. If the desktop/session service restarts, release and reacquire the
|
|
104
|
+
inhibitor. Actual GNOME/KDE power behavior still requires desktop validation.
|
|
105
|
+
|
|
106
|
+
Native Windows ARM64 wheels are built for Python 3.11–3.14 and 3.13t/3.14t.
|
|
107
|
+
Python 3.8–3.10 remains supported on Windows x86_64. This matches the native ARM64
|
|
108
|
+
interpreters available through uv.
|
|
109
|
+
|
|
110
|
+
## Migrating from the idempotent API
|
|
111
|
+
|
|
112
|
+
Repeated successful calls now require matching releases. Code that previously
|
|
113
|
+
called `prevent_sleep()` many times but released only once must balance its calls
|
|
114
|
+
or use a guard around each unit of work. Do not manually release an acquisition
|
|
115
|
+
already owned by a guard, or release after a failed acquisition: the counter is
|
|
116
|
+
shared and cannot identify a mismatched manual release.
|
|
117
|
+
|
|
118
|
+
## Development and publishing
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
uv sync --frozen
|
|
122
|
+
uv run --frozen pytest -q
|
|
123
|
+
uv build
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
CI checks Python 3.8–3.14 and free-threaded 3.13t/3.14t on Linux, Windows x86_64 and
|
|
127
|
+
both macOS architectures, plus the six Windows ARM64 configurations above,
|
|
128
|
+
whenever changes reach `master` or a pull request targets it.
|
|
129
|
+
Publishing a GitHub Release runs the checks and builds before uploading the
|
|
130
|
+
sdist and native wheels to PyPI through Trusted Publishing.
|
|
131
|
+
|
|
132
|
+
See [development and release instructions](https://github.com/czf0613/keep_awake_py/blob/master/docs/DEVELOPMENT.md),
|
|
133
|
+
[compatibility findings and verification limits](https://github.com/czf0613/keep_awake_py/blob/master/docs/COMPATIBILITY.md), and
|
|
134
|
+
[Codex repository guidance](https://github.com/czf0613/keep_awake_py/blob/master/AGENTS.md).
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
[MIT](https://github.com/czf0613/keep_awake_py/blob/master/LICENSE)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.py
|
|
6
|
+
native_code/include/pm.h
|
|
7
|
+
native_code/src/ext.c
|
|
8
|
+
native_code/src/pm_macos.c
|
|
9
|
+
native_code/src/pm_windows.c
|
|
10
|
+
src/keep_awake/__init__.py
|
|
11
|
+
src/keep_awake/_native_api.pyi
|
|
12
|
+
src/keep_awake/dbus_api.py
|
|
13
|
+
src/keep_awake/py.typed
|
|
14
|
+
src/keep_awake.egg-info/PKG-INFO
|
|
15
|
+
src/keep_awake.egg-info/SOURCES.txt
|
|
16
|
+
src/keep_awake.egg-info/dependency_links.txt
|
|
17
|
+
src/keep_awake.egg-info/requires.txt
|
|
18
|
+
src/keep_awake.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
keep_awake
|