wolsocketproxy 0.3.1__tar.gz → 0.4.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.
Files changed (18) hide show
  1. {wolsocketproxy-0.3.1/src/wolsocketproxy.egg-info → wolsocketproxy-0.4.0}/PKG-INFO +23 -3
  2. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/README.md +21 -2
  3. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/pyproject.toml +2 -1
  4. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/src/wolsocketproxy/proxy.py +153 -7
  5. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0/src/wolsocketproxy.egg-info}/PKG-INFO +23 -3
  6. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/src/wolsocketproxy.egg-info/requires.txt +1 -0
  7. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/LICENSE +0 -0
  8. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/setup.cfg +0 -0
  9. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/src/wolsocketproxy/__init__.py +0 -0
  10. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/src/wolsocketproxy/__main__.py +0 -0
  11. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/src/wolsocketproxy/common.py +0 -0
  12. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/src/wolsocketproxy/keepalive.py +0 -0
  13. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/src/wolsocketproxy/monitor.py +0 -0
  14. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/src/wolsocketproxy/utils.py +0 -0
  15. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/src/wolsocketproxy.egg-info/SOURCES.txt +0 -0
  16. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/src/wolsocketproxy.egg-info/dependency_links.txt +0 -0
  17. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/src/wolsocketproxy.egg-info/entry_points.txt +0 -0
  18. {wolsocketproxy-0.3.1 → wolsocketproxy-0.4.0}/src/wolsocketproxy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wolsocketproxy
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: A socket proxy with wake-on-lan feature.
5
5
  Author-email: Song Fuchang <song.fc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -14,6 +14,7 @@ Requires-Python: >=3.12
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
16
  Requires-Dist: aiohttp==3.13.5
17
+ Requires-Dist: croniter==6.0.0
17
18
  Requires-Dist: dataclass-wizard==0.39.1
18
19
  Requires-Dist: icmplib==3.0.4
19
20
  Requires-Dist: redfish==3.3.5
@@ -102,8 +103,14 @@ The `wolsocketproxy.conf` has the following structure:
102
103
  "ipmi_force_reset_if_power_up_failed": true, // Use IPMI power reset if this machine is not online after timeout, optional, default is false
103
104
  "ipmi_max_reset_try_count": 3, // Max IPMI power reset retry count before giving-up, optional, default is 3
104
105
  "keep_alive_mode": true, // Indicates whether this machine has a keep-alive daemon, optional, default is false
105
- // If you enable this, wolsocketproxy will send a request when there is any traffic
106
- "keep_alive_mode_base_url": "http://192.168.1.124:8080" // Keep-alive daemon provided URL, optional
106
+ // If you enable this, wolsocketproxy will send a request when there is any traffic
107
+ "keep_alive_mode_base_url": "http://192.168.1.124:8080", // Keep-alive daemon provided URL, optional
108
+ "scheduled_power_up_times": [ // Scheduled power-up times, optional
109
+ {
110
+ "cron": "0 7 * * 1-5", // Cron expression for auto power-up
111
+ "keep_alive_time": 7200 // Send keep-alive requests for N seconds after power-up
112
+ }
113
+ ]
107
114
  },
108
115
  // ... more machines ...
109
116
  },
@@ -167,3 +174,16 @@ or the special process will be killed.
167
174
  You can combine this mode with `circadian`'s `process_block` config to keep it from auto-suspending.
168
175
 
169
176
  If you enable `keep_alive_mode` in proxy mode's config, the proxy will send requests to `/watchdog/feed` of this machine whenever there is any traffic towards this machine through it.
177
+
178
+ ### Scheduled power-up
179
+
180
+ You can configure `scheduled_power_up_times` in each machine entry to automatically power up the machine at specified times using
181
+ a cron expression. After the machine comes online, the proxy sends keep-alive requests to the machine's keep-alive daemon
182
+ (for the duration specified by `keep_alive_time`) to prevent it from auto-suspending during the expected usage window.
183
+
184
+ Each entry in `scheduled_power_up_times` contains:
185
+ - `cron`: A standard 5-field cron expression (e.g. `"0 7 * * 1-5"` for weekdays at 7:00)
186
+ - `keep_alive_time`: Duration in seconds to send keep-alive requests after power-up (e.g. `7200` for 2 hours)
187
+
188
+ If multiple entries overlap in time, the keep-alive period is automatically extended to cover the longest window, and no
189
+ duplicate keep-alive requests are sent.
@@ -71,8 +71,14 @@ The `wolsocketproxy.conf` has the following structure:
71
71
  "ipmi_force_reset_if_power_up_failed": true, // Use IPMI power reset if this machine is not online after timeout, optional, default is false
72
72
  "ipmi_max_reset_try_count": 3, // Max IPMI power reset retry count before giving-up, optional, default is 3
73
73
  "keep_alive_mode": true, // Indicates whether this machine has a keep-alive daemon, optional, default is false
74
- // If you enable this, wolsocketproxy will send a request when there is any traffic
75
- "keep_alive_mode_base_url": "http://192.168.1.124:8080" // Keep-alive daemon provided URL, optional
74
+ // If you enable this, wolsocketproxy will send a request when there is any traffic
75
+ "keep_alive_mode_base_url": "http://192.168.1.124:8080", // Keep-alive daemon provided URL, optional
76
+ "scheduled_power_up_times": [ // Scheduled power-up times, optional
77
+ {
78
+ "cron": "0 7 * * 1-5", // Cron expression for auto power-up
79
+ "keep_alive_time": 7200 // Send keep-alive requests for N seconds after power-up
80
+ }
81
+ ]
76
82
  },
77
83
  // ... more machines ...
78
84
  },
@@ -136,3 +142,16 @@ or the special process will be killed.
136
142
  You can combine this mode with `circadian`'s `process_block` config to keep it from auto-suspending.
137
143
 
138
144
  If you enable `keep_alive_mode` in proxy mode's config, the proxy will send requests to `/watchdog/feed` of this machine whenever there is any traffic towards this machine through it.
145
+
146
+ ### Scheduled power-up
147
+
148
+ You can configure `scheduled_power_up_times` in each machine entry to automatically power up the machine at specified times using
149
+ a cron expression. After the machine comes online, the proxy sends keep-alive requests to the machine's keep-alive daemon
150
+ (for the duration specified by `keep_alive_time`) to prevent it from auto-suspending during the expected usage window.
151
+
152
+ Each entry in `scheduled_power_up_times` contains:
153
+ - `cron`: A standard 5-field cron expression (e.g. `"0 7 * * 1-5"` for weekdays at 7:00)
154
+ - `keep_alive_time`: Duration in seconds to send keep-alive requests after power-up (e.g. `7200` for 2 hours)
155
+
156
+ If multiple entries overlap in time, the keep-alive period is automatically extended to cover the longest window, and no
157
+ duplicate keep-alive requests are sent.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wolsocketproxy"
7
- version = "0.3.1"
7
+ version = "0.4.0"
8
8
  description = "A socket proxy with wake-on-lan feature."
9
9
  authors = [
10
10
  {name = "Song Fuchang", email = "song.fc@gmail.com"}
@@ -20,6 +20,7 @@ classifiers = [
20
20
  requires-python = ">=3.12"
21
21
  dependencies = [
22
22
  "aiohttp==3.13.5",
23
+ "croniter==6.0.0",
23
24
  "dataclass-wizard==0.39.1",
24
25
  "icmplib==3.0.4",
25
26
  "redfish==3.3.5",
@@ -1,13 +1,17 @@
1
1
  import asyncio
2
2
  import contextlib
3
3
  import logging
4
+ import time
5
+ from collections.abc import Callable, Coroutine
4
6
  from dataclasses import dataclass
7
+ from datetime import datetime
5
8
  from logging import Logger
6
- from threading import Thread
9
+ from threading import Event, Thread
7
10
  from typing import Any, Literal, override
8
11
 
9
12
  import aiohttp
10
13
  import wakeonlan
14
+ from croniter import croniter
11
15
  from redfish.rest.v1 import HttpClient, redfish_client
12
16
 
13
17
  from wolsocketproxy.common import URL_WATCHDOG_FEED
@@ -15,6 +19,12 @@ from wolsocketproxy.monitor import Monitor, MonitorConfig
15
19
  from wolsocketproxy.utils import perform_ipmi_action
16
20
 
17
21
 
22
+ @dataclass
23
+ class ScheduledPowerUpTime:
24
+ cron: str
25
+ keep_alive_time: int = 0
26
+
27
+
18
28
  @dataclass
19
29
  class MachineConfig:
20
30
  wake_up_method: Literal["ipmi", "wol"] = "wol"
@@ -36,6 +46,8 @@ class MachineConfig:
36
46
  keep_alive_mode_base_url: str | None = None
37
47
  keep_alive_min_interval: int = 1
38
48
 
49
+ scheduled_power_up_times: list[ScheduledPowerUpTime] | None = None
50
+
39
51
 
40
52
  @dataclass
41
53
  class ProxyRoute:
@@ -64,10 +76,13 @@ class ProxyConfig:
64
76
 
65
77
 
66
78
  class TargetKeepAliveSender:
79
+ _log: logging.Logger = logging.getLogger(__name__)
80
+
67
81
  _target_url: str
68
82
  _keep_alive_min_interval: int
69
83
  _loop: asyncio.AbstractEventLoop
70
84
  _queue: asyncio.Queue
85
+ _stop_event: Event
71
86
 
72
87
  def __init__(self, target_base_url: str, keep_alive_min_interval: int) -> None:
73
88
  self._target_url = target_base_url.removesuffix("/") + URL_WATCHDOG_FEED
@@ -75,21 +90,32 @@ class TargetKeepAliveSender:
75
90
 
76
91
  self._loop = asyncio.new_event_loop()
77
92
  self._queue = asyncio.Queue(1)
93
+ self._stop_event = Event()
78
94
 
79
- def _loop() -> None:
95
+ def _run_loop() -> None:
80
96
  asyncio.set_event_loop(self._loop)
81
97
  self._loop.run_until_complete(self._send_worker())
82
98
 
83
- Thread(target=_loop, daemon=True).start()
99
+ Thread(target=_run_loop, daemon=True).start()
84
100
 
85
101
  async def _send_worker(self) -> None:
86
- while True:
102
+ while not self._stop_event.is_set():
87
103
  await self._queue.get()
104
+ if self._stop_event.is_set():
105
+ break
106
+ await self._send()
107
+ await asyncio.sleep(self._keep_alive_min_interval)
88
108
 
89
- async with aiohttp.request("GET", self._target_url) as resp:
109
+ async def _send(self) -> None:
110
+ try:
111
+ async with aiohttp.request(
112
+ "GET",
113
+ self._target_url,
114
+ timeout=aiohttp.ClientTimeout(total=5),
115
+ ) as resp:
90
116
  await resp.json()
91
-
92
- await asyncio.sleep(self._keep_alive_min_interval)
117
+ except (aiohttp.ClientError, aiohttp.ClientResponseError):
118
+ self._log.warning("Failed to send target keep alive request to %s", self._target_url, exc_info=True)
93
119
 
94
120
  def schedule_send(self) -> None:
95
121
  def _no_exception_put() -> None:
@@ -98,6 +124,118 @@ class TargetKeepAliveSender:
98
124
 
99
125
  self._loop.call_soon_threadsafe(_no_exception_put)
100
126
 
127
+ def stop(self) -> None:
128
+ self._stop_event.set()
129
+ with contextlib.suppress(asyncio.QueueFull):
130
+ self._loop.call_soon_threadsafe(lambda: self._queue.put_nowait(None))
131
+
132
+
133
+ class ScheduledPowerUpManager:
134
+ _log: Logger = logging.getLogger(__name__)
135
+
136
+ _machines: dict[str, MachineConfig]
137
+ _wake_up_callback: Callable[[str], Coroutine[Any, Any, None]]
138
+ _loop: asyncio.AbstractEventLoop
139
+ _keep_alive_end_times: dict[str, float]
140
+
141
+ def __init__(
142
+ self,
143
+ machines: dict[str, MachineConfig],
144
+ wake_up_callback: Callable[[str], Coroutine[Any, Any, None]],
145
+ ) -> None:
146
+ self._machines = machines
147
+ self._wake_up_callback = wake_up_callback
148
+ self._keep_alive_end_times = {}
149
+
150
+ def start(self) -> None:
151
+ self._loop = asyncio.new_event_loop()
152
+
153
+ def _run_loop() -> None:
154
+ asyncio.set_event_loop(self._loop)
155
+ self._loop.run_forever()
156
+
157
+ Thread(target=_run_loop, daemon=True).start()
158
+
159
+ for machine_name, machine in self._machines.items():
160
+ schedules = machine.scheduled_power_up_times
161
+ if not schedules:
162
+ continue
163
+
164
+ if machine.keep_alive_mode_base_url is None:
165
+ self._log.warning(
166
+ "Machine %s has scheduled_power_up_times but no keep_alive_mode_base_url configured, "
167
+ "keep-alive after power-up will be skipped",
168
+ machine_name,
169
+ )
170
+
171
+ for schedule in schedules:
172
+ asyncio.run_coroutine_threadsafe(
173
+ self._process_schedule(machine_name, machine, schedule),
174
+ self._loop,
175
+ )
176
+
177
+ async def _process_schedule(
178
+ self,
179
+ machine_name: str,
180
+ machine: MachineConfig,
181
+ schedule: ScheduledPowerUpTime,
182
+ ) -> None:
183
+ while True:
184
+ now = datetime.now()
185
+ cron = croniter(schedule.cron, now)
186
+ next_time = cron.get_next(datetime)
187
+
188
+ delay = (next_time - datetime.now()).total_seconds()
189
+ if delay > 0:
190
+ await asyncio.sleep(delay)
191
+
192
+ self._log.info(
193
+ "Scheduled power-up triggered for %s (cron: %s)",
194
+ machine_name,
195
+ schedule.cron,
196
+ )
197
+
198
+ try:
199
+ await self._wake_up_callback(machine_name)
200
+ except ConnectionAbortedError:
201
+ self._log.error(
202
+ "Scheduled power-up failed for %s, will retry at next schedule time",
203
+ machine_name,
204
+ )
205
+ continue
206
+
207
+ if schedule.keep_alive_time > 0 and machine.keep_alive_mode_base_url is not None:
208
+ await self._run_keep_alive(machine_name, machine, schedule.keep_alive_time)
209
+
210
+
211
+ async def _run_keep_alive(
212
+ self,
213
+ machine_name: str,
214
+ machine: MachineConfig,
215
+ keep_alive_time: int,
216
+ ) -> None:
217
+ assert machine.keep_alive_mode_base_url is not None
218
+ new_end = time.monotonic() + keep_alive_time
219
+ current_end = self._keep_alive_end_times.get(machine_name, 0.0)
220
+
221
+ if new_end > current_end:
222
+ self._keep_alive_end_times[machine_name] = new_end
223
+
224
+ if current_end > time.monotonic():
225
+ return
226
+
227
+ sender = TargetKeepAliveSender(
228
+ machine.keep_alive_mode_base_url,
229
+ machine.keep_alive_min_interval,
230
+ )
231
+
232
+ try:
233
+ while time.monotonic() < self._keep_alive_end_times.get(machine_name, 0.0):
234
+ sender.schedule_send()
235
+ await asyncio.sleep(machine.keep_alive_min_interval)
236
+ finally:
237
+ sender.stop()
238
+
101
239
 
102
240
  class ProxyUdpProtocol(asyncio.DatagramProtocol):
103
241
  _proxy: "Proxy"
@@ -154,6 +292,7 @@ class Proxy:
154
292
  _machines: dict[str, MachineConfig]
155
293
  _routes: list[ProxyRoute]
156
294
  _ipmi_configs: dict[str, IPMIConfig]
295
+ _scheduled_power_up_manager: ScheduledPowerUpManager
157
296
 
158
297
  def __init__(self, config: ProxyConfig) -> None:
159
298
  self._config = config
@@ -193,6 +332,11 @@ class Proxy:
193
332
  }
194
333
  )
195
334
 
335
+ self._scheduled_power_up_manager = ScheduledPowerUpManager(
336
+ machines=self._machines,
337
+ wake_up_callback=self._wake_up_target,
338
+ )
339
+
196
340
  def start(self) -> None:
197
341
  self._monitor.start()
198
342
 
@@ -201,6 +345,8 @@ class Proxy:
201
345
 
202
346
  loop = asyncio.get_event_loop()
203
347
 
348
+ self._scheduled_power_up_manager.start()
349
+
204
350
  self._log.info("Proxy server started.")
205
351
 
206
352
  with contextlib.suppress(KeyboardInterrupt):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wolsocketproxy
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: A socket proxy with wake-on-lan feature.
5
5
  Author-email: Song Fuchang <song.fc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -14,6 +14,7 @@ Requires-Python: >=3.12
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
16
  Requires-Dist: aiohttp==3.13.5
17
+ Requires-Dist: croniter==6.0.0
17
18
  Requires-Dist: dataclass-wizard==0.39.1
18
19
  Requires-Dist: icmplib==3.0.4
19
20
  Requires-Dist: redfish==3.3.5
@@ -102,8 +103,14 @@ The `wolsocketproxy.conf` has the following structure:
102
103
  "ipmi_force_reset_if_power_up_failed": true, // Use IPMI power reset if this machine is not online after timeout, optional, default is false
103
104
  "ipmi_max_reset_try_count": 3, // Max IPMI power reset retry count before giving-up, optional, default is 3
104
105
  "keep_alive_mode": true, // Indicates whether this machine has a keep-alive daemon, optional, default is false
105
- // If you enable this, wolsocketproxy will send a request when there is any traffic
106
- "keep_alive_mode_base_url": "http://192.168.1.124:8080" // Keep-alive daemon provided URL, optional
106
+ // If you enable this, wolsocketproxy will send a request when there is any traffic
107
+ "keep_alive_mode_base_url": "http://192.168.1.124:8080", // Keep-alive daemon provided URL, optional
108
+ "scheduled_power_up_times": [ // Scheduled power-up times, optional
109
+ {
110
+ "cron": "0 7 * * 1-5", // Cron expression for auto power-up
111
+ "keep_alive_time": 7200 // Send keep-alive requests for N seconds after power-up
112
+ }
113
+ ]
107
114
  },
108
115
  // ... more machines ...
109
116
  },
@@ -167,3 +174,16 @@ or the special process will be killed.
167
174
  You can combine this mode with `circadian`'s `process_block` config to keep it from auto-suspending.
168
175
 
169
176
  If you enable `keep_alive_mode` in proxy mode's config, the proxy will send requests to `/watchdog/feed` of this machine whenever there is any traffic towards this machine through it.
177
+
178
+ ### Scheduled power-up
179
+
180
+ You can configure `scheduled_power_up_times` in each machine entry to automatically power up the machine at specified times using
181
+ a cron expression. After the machine comes online, the proxy sends keep-alive requests to the machine's keep-alive daemon
182
+ (for the duration specified by `keep_alive_time`) to prevent it from auto-suspending during the expected usage window.
183
+
184
+ Each entry in `scheduled_power_up_times` contains:
185
+ - `cron`: A standard 5-field cron expression (e.g. `"0 7 * * 1-5"` for weekdays at 7:00)
186
+ - `keep_alive_time`: Duration in seconds to send keep-alive requests after power-up (e.g. `7200` for 2 hours)
187
+
188
+ If multiple entries overlap in time, the keep-alive period is automatically extended to cover the longest window, and no
189
+ duplicate keep-alive requests are sent.
@@ -1,4 +1,5 @@
1
1
  aiohttp==3.13.5
2
+ croniter==6.0.0
2
3
  dataclass-wizard==0.39.1
3
4
  icmplib==3.0.4
4
5
  redfish==3.3.5
File without changes
File without changes