wolsocketproxy 0.2.0__tar.gz → 0.3.1__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 (19) hide show
  1. {wolsocketproxy-0.2.0/src/wolsocketproxy.egg-info → wolsocketproxy-0.3.1}/PKG-INFO +42 -4
  2. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/README.md +39 -2
  3. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/pyproject.toml +6 -2
  4. wolsocketproxy-0.3.1/src/wolsocketproxy/__init__.py +75 -0
  5. wolsocketproxy-0.3.1/src/wolsocketproxy/common.py +1 -0
  6. wolsocketproxy-0.3.1/src/wolsocketproxy/keepalive.py +152 -0
  7. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy/proxy.py +107 -7
  8. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1/src/wolsocketproxy.egg-info}/PKG-INFO +42 -4
  9. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy.egg-info/SOURCES.txt +2 -0
  10. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy.egg-info/requires.txt +2 -1
  11. wolsocketproxy-0.2.0/src/wolsocketproxy/__init__.py +0 -44
  12. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/LICENSE +0 -0
  13. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/setup.cfg +0 -0
  14. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy/__main__.py +0 -0
  15. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy/monitor.py +0 -0
  16. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy/utils.py +0 -0
  17. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy.egg-info/dependency_links.txt +0 -0
  18. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy.egg-info/entry_points.txt +0 -0
  19. {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/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.2.0
3
+ Version: 0.3.1
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
@@ -17,6 +17,7 @@ Requires-Dist: aiohttp==3.13.5
17
17
  Requires-Dist: dataclass-wizard==0.39.1
18
18
  Requires-Dist: icmplib==3.0.4
19
19
  Requires-Dist: redfish==3.3.5
20
+ Requires-Dist: setproctitle==1.3.7
20
21
  Requires-Dist: wakeonlan==3.1.0
21
22
  Provides-Extra: dev
22
23
  Requires-Dist: wolsocketproxy[lint]; extra == "dev"
@@ -25,7 +26,7 @@ Provides-Extra: lint
25
26
  Requires-Dist: ty; extra == "lint"
26
27
  Requires-Dist: ruff; extra == "lint"
27
28
  Provides-Extra: build
28
- Requires-Dist: build[virtualenv]==1.4.2; extra == "build"
29
+ Requires-Dist: build[virtualenv]==1.5.0; extra == "build"
29
30
  Dynamic: license-file
30
31
 
31
32
  # wol-socket-proxy
@@ -96,10 +97,13 @@ The `wolsocketproxy.conf` has the following structure:
96
97
  // "http" - You must specify "online_check_http_url"
97
98
  // "ping" - You must specify "ip_address"
98
99
  "online_check_http_url": "http://192.168.1.124/health", // The URL used to check if machine is online, optional
99
- "online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is 300
100
+ "online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is 60
100
101
  "online_check_http_expected_code": 200, // Expected HTTP status code when using "http" method, optional, default is 200
101
102
  "ipmi_force_reset_if_power_up_failed": true, // Use IPMI power reset if this machine is not online after timeout, optional, default is false
102
- "ipmi_max_reset_try_count": 3 // Max IPMI power reset retry count before giving-up, optional, default is 3
103
+ "ipmi_max_reset_try_count": 3, // Max IPMI power reset retry count before giving-up, optional, default is 3
104
+ "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
103
107
  },
104
108
  // ... more machines ...
105
109
  },
@@ -129,3 +133,37 @@ The `wolsocketproxy.conf` has the following structure:
129
133
  ]
130
134
  }
131
135
  ```
136
+
137
+ ## Additional modes
138
+
139
+ ### Keep-alive daemon mode
140
+
141
+ This mode is used for running on target machine with auto-suspend (e.g. running `circadian` or similar), to keep the target
142
+ machine from auto suspending when running tasks.
143
+
144
+ Use the following command on the target machine to enter this mode:
145
+
146
+ ```bash
147
+ wolsocketproxy -k
148
+ ```
149
+
150
+ The config file `wolsocketproxy.conf` should look like the following:
151
+
152
+
153
+ ```json5
154
+ // Keep-alive daemon mode
155
+ {
156
+ "listen_address": "0.0.0.0", // The address to listen for HTTP server, default is 127.0.0.1
157
+ "listen_port": 18080, // The port to listen for HTTP server, default is 18080
158
+ "watchdog_feed_interval": 60, // Watchdog feed interval, seconds, default is 600
159
+ "keep_alive_method": "special_process", // How to keep target machine alive, default is "special_process"
160
+ // "special_process" - Fork a child process with specified name
161
+ "special_process_name": "wsp-keepalive", // The display name of the forked child process, default is "wsp-keepalive"
162
+ }
163
+ ```
164
+
165
+ You need to periodically send an HTTP GET request to `/watchdog/feed` at the `listen_port` in `watchdog_feed_interval` time,
166
+ or the special process will be killed.
167
+ You can combine this mode with `circadian`'s `process_block` config to keep it from auto-suspending.
168
+
169
+ 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.
@@ -66,10 +66,13 @@ The `wolsocketproxy.conf` has the following structure:
66
66
  // "http" - You must specify "online_check_http_url"
67
67
  // "ping" - You must specify "ip_address"
68
68
  "online_check_http_url": "http://192.168.1.124/health", // The URL used to check if machine is online, optional
69
- "online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is 300
69
+ "online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is 60
70
70
  "online_check_http_expected_code": 200, // Expected HTTP status code when using "http" method, optional, default is 200
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
- "ipmi_max_reset_try_count": 3 // Max IPMI power reset retry count before giving-up, optional, default is 3
72
+ "ipmi_max_reset_try_count": 3, // Max IPMI power reset retry count before giving-up, optional, default is 3
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
73
76
  },
74
77
  // ... more machines ...
75
78
  },
@@ -99,3 +102,37 @@ The `wolsocketproxy.conf` has the following structure:
99
102
  ]
100
103
  }
101
104
  ```
105
+
106
+ ## Additional modes
107
+
108
+ ### Keep-alive daemon mode
109
+
110
+ This mode is used for running on target machine with auto-suspend (e.g. running `circadian` or similar), to keep the target
111
+ machine from auto suspending when running tasks.
112
+
113
+ Use the following command on the target machine to enter this mode:
114
+
115
+ ```bash
116
+ wolsocketproxy -k
117
+ ```
118
+
119
+ The config file `wolsocketproxy.conf` should look like the following:
120
+
121
+
122
+ ```json5
123
+ // Keep-alive daemon mode
124
+ {
125
+ "listen_address": "0.0.0.0", // The address to listen for HTTP server, default is 127.0.0.1
126
+ "listen_port": 18080, // The port to listen for HTTP server, default is 18080
127
+ "watchdog_feed_interval": 60, // Watchdog feed interval, seconds, default is 600
128
+ "keep_alive_method": "special_process", // How to keep target machine alive, default is "special_process"
129
+ // "special_process" - Fork a child process with specified name
130
+ "special_process_name": "wsp-keepalive", // The display name of the forked child process, default is "wsp-keepalive"
131
+ }
132
+ ```
133
+
134
+ You need to periodically send an HTTP GET request to `/watchdog/feed` at the `listen_port` in `watchdog_feed_interval` time,
135
+ or the special process will be killed.
136
+ You can combine this mode with `circadian`'s `process_block` config to keep it from auto-suspending.
137
+
138
+ 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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wolsocketproxy"
7
- version = "0.2.0"
7
+ version = "0.3.1"
8
8
  description = "A socket proxy with wake-on-lan feature."
9
9
  authors = [
10
10
  {name = "Song Fuchang", email = "song.fc@gmail.com"}
@@ -23,6 +23,7 @@ dependencies = [
23
23
  "dataclass-wizard==0.39.1",
24
24
  "icmplib==3.0.4",
25
25
  "redfish==3.3.5",
26
+ "setproctitle==1.3.7",
26
27
  "wakeonlan==3.1.0",
27
28
  ]
28
29
 
@@ -36,7 +37,7 @@ lint = [
36
37
  "ruff",
37
38
  ]
38
39
  build = [
39
- "build[virtualenv]==1.4.2",
40
+ "build[virtualenv]==1.5.0",
40
41
  ]
41
42
 
42
43
  [project.urls]
@@ -52,6 +53,7 @@ src = ["src"]
52
53
  target-version = "py312"
53
54
  lint.select = ["ALL"]
54
55
  lint.ignore = [
56
+ "ASYNC110",
55
57
  "COM812",
56
58
  "D100",
57
59
  "D101",
@@ -62,6 +64,7 @@ lint.ignore = [
62
64
  "D203",
63
65
  "D211",
64
66
  "D213",
67
+ "DTZ005",
65
68
  "S101",
66
69
  "EM102",
67
70
  "FBT001",
@@ -69,4 +72,5 @@ lint.ignore = [
69
72
  "ISC001",
70
73
  "TRY003",
71
74
  "TRY201",
75
+ "PLR0913",
72
76
  ]
@@ -0,0 +1,75 @@
1
+ import json
2
+ import logging
3
+ from argparse import ArgumentParser
4
+ from pathlib import Path
5
+ from sys import stdout
6
+
7
+ import dataclass_wizard
8
+
9
+ from wolsocketproxy.keepalive import KeepAliveConfig, KeepAliveDaemon
10
+ from wolsocketproxy.proxy import Proxy, ProxyConfig
11
+
12
+
13
+ def main() -> None:
14
+ logging.basicConfig(
15
+ format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
16
+ level=logging.INFO,
17
+ datefmt="%Y-%m-%d %H:%M:%S",
18
+ handlers=[
19
+ logging.StreamHandler(
20
+ stream=stdout,
21
+ ),
22
+ ],
23
+ force=True,
24
+ )
25
+
26
+ log = logging.getLogger()
27
+
28
+ parser = ArgumentParser(prog="wolsocketproxy", description="A socket proxy with wake-on-lan feature.")
29
+
30
+ parser.add_argument(
31
+ "-k",
32
+ "--keep-alive",
33
+ dest="keep_alive_mode",
34
+ default=False,
35
+ action="store_true",
36
+ help="Start in keep-alive daemon mode",
37
+ )
38
+
39
+ parser.add_argument(
40
+ "-c",
41
+ "--config",
42
+ help="The config file to use, default lookup order: /etc/wolsocketproxy.conf, ./wolsocketproxy.conf",
43
+ )
44
+
45
+ args = parser.parse_args()
46
+ config_path: Path
47
+
48
+ if "config" in args and args.config is not None:
49
+ config_path = Path(args.config)
50
+ else:
51
+ config_path = Path("/etc/wolsocketproxy.conf")
52
+
53
+ if not config_path.exists():
54
+ config_path = Path("./wolsocketproxy.conf")
55
+
56
+ if not config_path.exists():
57
+ log.error("Config file path %s does not exist!", config_path)
58
+ return
59
+
60
+ config_data = json.loads(config_path.read_text())
61
+
62
+ if args.keep_alive_mode:
63
+ config = dataclass_wizard.fromdict(KeepAliveConfig, config_data)
64
+
65
+ log.info("Loaded keep-alive mode config from %s", config_path)
66
+
67
+ keep_alive_daemon = KeepAliveDaemon(config)
68
+ keep_alive_daemon.start()
69
+ else:
70
+ config = dataclass_wizard.fromdict(ProxyConfig, config_data)
71
+
72
+ log.info("Loaded proxy mode config from %s", config_path)
73
+
74
+ proxy = Proxy(config)
75
+ proxy.start()
@@ -0,0 +1 @@
1
+ URL_WATCHDOG_FEED = "/watchdog/feed"
@@ -0,0 +1,152 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import signal
5
+ import time
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from threading import Thread
9
+ from typing import Literal
10
+
11
+ from aiohttp import web
12
+ from aiohttp.web import Request, Response
13
+ from setproctitle import setproctitle
14
+
15
+ from wolsocketproxy.common import URL_WATCHDOG_FEED
16
+
17
+
18
+ @dataclass
19
+ class KeepAliveConfig:
20
+ listen_address: str = "127.0.0.1"
21
+ listen_port: int = 18080
22
+
23
+ watchdog_feed_interval: int = 600
24
+
25
+ keep_alive_method: Literal["special_process"] = "special_process"
26
+
27
+ special_process_name: str | None = "wsp-keepalive"
28
+
29
+
30
+ class KeepAliveDaemon:
31
+ _log: logging.Logger = logging.getLogger(__name__)
32
+
33
+ _config: KeepAliveConfig
34
+ _web_app: web.Application
35
+
36
+ _watchdog_last_feed_time: datetime
37
+ _watchdog_is_hungry: bool = False
38
+
39
+ _special_process_id: int = -1
40
+
41
+ def __init__(self, config: KeepAliveConfig) -> None:
42
+ self._config = config
43
+
44
+ self._web_app = web.Application()
45
+
46
+ self._web_app.add_routes([web.get(URL_WATCHDOG_FEED, self._handle_watchdog_feed)])
47
+
48
+ async def _watchdog_timer(self) -> None:
49
+ while True:
50
+ previous_is_hungry = self._watchdog_is_hungry
51
+ last_feed_interval = datetime.now() - self._watchdog_last_feed_time
52
+
53
+ if last_feed_interval.total_seconds() > self._config.watchdog_feed_interval:
54
+ self._watchdog_is_hungry = True
55
+ else:
56
+ self._watchdog_is_hungry = False
57
+
58
+ if not previous_is_hungry and self._watchdog_is_hungry:
59
+ Thread(target=self._on_watchdog_hungry, daemon=True).start()
60
+ elif previous_is_hungry and not self._watchdog_is_hungry:
61
+ Thread(target=self._on_watchdog_feed, daemon=True).start()
62
+
63
+ await asyncio.sleep(1)
64
+
65
+ def _on_watchdog_hungry(self) -> None:
66
+ self._log.warning("Watchdog is hungry!")
67
+
68
+ if self._special_process_id > 0:
69
+ os.kill(self._special_process_id, signal.SIGKILL)
70
+ os.waitpid(self._special_process_id, 0)
71
+ self._log.warning("Killed special process PID %d", self._special_process_id)
72
+ self._special_process_id = -1
73
+
74
+ def _on_watchdog_feed(self) -> None:
75
+ self._log.info("Watchdog is feed.")
76
+
77
+ if self._config.keep_alive_method == "special_process" and self._special_process_id < 0:
78
+ self._start_special_process()
79
+
80
+ def _special_process(self) -> None:
81
+ assert self._config.special_process_name is not None
82
+ setproctitle(self._config.special_process_name)
83
+
84
+ while True:
85
+ time.sleep(60)
86
+
87
+ if os.getppid() == 1:
88
+ os._exit(0)
89
+
90
+ def _start_special_process(self) -> bool:
91
+ self._special_process_id = os.fork()
92
+
93
+ if self._special_process_id == 0:
94
+ self._special_process()
95
+ return True
96
+
97
+ self._log.info(
98
+ "Started special process with name %s, PID %d", self._config.special_process_name, self._special_process_id
99
+ )
100
+
101
+ return False
102
+
103
+ def start(self) -> None:
104
+ self._watchdog_last_feed_time = datetime.now()
105
+ self._watchdog_is_hungry = False
106
+
107
+ if self._config.keep_alive_method == "special_process": # noqa: SIM102
108
+ if self._start_special_process():
109
+ return
110
+
111
+ loop = asyncio.new_event_loop()
112
+
113
+ def _start_loop() -> None:
114
+ asyncio.set_event_loop(loop)
115
+ loop.run_forever()
116
+
117
+ Thread(target=_start_loop, daemon=True).start()
118
+
119
+ watchdog_task = asyncio.run_coroutine_threadsafe(self._watchdog_timer(), loop)
120
+
121
+ self._log.info(
122
+ "Keep-alive daemon started at %s:%d, watchdog feed interval %ds.",
123
+ self._config.listen_address,
124
+ self._config.listen_port,
125
+ self._config.watchdog_feed_interval,
126
+ )
127
+
128
+ web.run_app(self._web_app, host=self._config.listen_address, port=self._config.listen_port)
129
+
130
+ self._log.info("Stopping...")
131
+
132
+ watchdog_task.cancel()
133
+ loop.stop()
134
+
135
+ if self._special_process_id > 0:
136
+ os.kill(self._special_process_id, signal.SIGKILL)
137
+ os.waitpid(self._special_process_id, 0)
138
+
139
+ self._log.info("Stopped.")
140
+
141
+ async def _handle_watchdog_feed(self, _: Request) -> Response:
142
+ last_feed = self._watchdog_last_feed_time
143
+ self._watchdog_last_feed_time = datetime.now()
144
+ interval = (self._watchdog_last_feed_time - last_feed).total_seconds()
145
+
146
+ return web.json_response(
147
+ {
148
+ "status": "ok",
149
+ "last_feed": last_feed.strftime("%Y-%m-%d %H:%M:%S"),
150
+ "interval": interval,
151
+ }
152
+ )
@@ -3,11 +3,14 @@ import contextlib
3
3
  import logging
4
4
  from dataclasses import dataclass
5
5
  from logging import Logger
6
+ from threading import Thread
6
7
  from typing import Any, Literal, override
7
8
 
9
+ import aiohttp
8
10
  import wakeonlan
9
11
  from redfish.rest.v1 import HttpClient, redfish_client
10
12
 
13
+ from wolsocketproxy.common import URL_WATCHDOG_FEED
11
14
  from wolsocketproxy.monitor import Monitor, MonitorConfig
12
15
  from wolsocketproxy.utils import perform_ipmi_action
13
16
 
@@ -29,6 +32,10 @@ class MachineConfig:
29
32
  online_check_http_expected_code: int = 200
30
33
  online_check_timeout: int = 60
31
34
 
35
+ keep_alive_mode: bool = False
36
+ keep_alive_mode_base_url: str | None = None
37
+ keep_alive_min_interval: int = 1
38
+
32
39
 
33
40
  @dataclass
34
41
  class ProxyRoute:
@@ -56,6 +63,42 @@ class ProxyConfig:
56
63
  ipmi_configs: list[IPMIConfig] | None = None
57
64
 
58
65
 
66
+ class TargetKeepAliveSender:
67
+ _target_url: str
68
+ _keep_alive_min_interval: int
69
+ _loop: asyncio.AbstractEventLoop
70
+ _queue: asyncio.Queue
71
+
72
+ def __init__(self, target_base_url: str, keep_alive_min_interval: int) -> None:
73
+ self._target_url = target_base_url.removesuffix("/") + URL_WATCHDOG_FEED
74
+ self._keep_alive_min_interval = keep_alive_min_interval
75
+
76
+ self._loop = asyncio.new_event_loop()
77
+ self._queue = asyncio.Queue(1)
78
+
79
+ def _loop() -> None:
80
+ asyncio.set_event_loop(self._loop)
81
+ self._loop.run_until_complete(self._send_worker())
82
+
83
+ Thread(target=_loop, daemon=True).start()
84
+
85
+ async def _send_worker(self) -> None:
86
+ while True:
87
+ await self._queue.get()
88
+
89
+ async with aiohttp.request("GET", self._target_url) as resp:
90
+ await resp.json()
91
+
92
+ await asyncio.sleep(self._keep_alive_min_interval)
93
+
94
+ def schedule_send(self) -> None:
95
+ def _no_exception_put() -> None:
96
+ with contextlib.suppress(asyncio.QueueFull):
97
+ self._queue.put_nowait(1)
98
+
99
+ self._loop.call_soon_threadsafe(_no_exception_put)
100
+
101
+
59
102
  class ProxyUdpProtocol(asyncio.DatagramProtocol):
60
103
  _proxy: "Proxy"
61
104
  _monitor: Monitor
@@ -63,16 +106,24 @@ class ProxyUdpProtocol(asyncio.DatagramProtocol):
63
106
  _target_machine_name: str
64
107
  _target_address: str
65
108
  _target_port: int
109
+ _target_keep_alive_sender: TargetKeepAliveSender | None = None
66
110
  _target_pair: tuple[str, int]
67
111
 
68
112
  def __init__(
69
- self, proxy: "Proxy", monitor: Monitor, target_machine_name: str, target_address: str, target_port: int
113
+ self,
114
+ proxy: "Proxy",
115
+ monitor: Monitor,
116
+ target_machine_name: str,
117
+ target_address: str,
118
+ target_port: int,
119
+ target_keep_alive_sender: TargetKeepAliveSender | None = None,
70
120
  ) -> None:
71
121
  self._proxy = proxy
72
122
  self._monitor = monitor
73
123
  self._target_machine_name = target_machine_name
74
124
  self._target_address = target_address
75
125
  self._target_port = target_port
126
+ self._target_keep_alive_sender = target_keep_alive_sender
76
127
  self._target_pair = (target_address, target_port)
77
128
 
78
129
  @override
@@ -92,6 +143,9 @@ class ProxyUdpProtocol(asyncio.DatagramProtocol):
92
143
 
93
144
  self._transport.sendto(data, self._target_pair)
94
145
 
146
+ if self._target_keep_alive_sender is not None:
147
+ self._target_keep_alive_sender.schedule_send()
148
+
95
149
 
96
150
  class Proxy:
97
151
  _log: Logger = logging.getLogger()
@@ -180,9 +234,23 @@ class Proxy:
180
234
 
181
235
  def __create_tcp_route(self, route: ProxyRoute) -> None:
182
236
  assert route.target_machine_name is not None
237
+ machine_config = self._machines[route.target_machine_name]
238
+ target_keep_alive_sender = None
239
+
240
+ if machine_config.keep_alive_mode:
241
+ assert machine_config.keep_alive_mode_base_url is not None
242
+
243
+ target_keep_alive_sender = TargetKeepAliveSender(
244
+ machine_config.keep_alive_mode_base_url, machine_config.keep_alive_min_interval
245
+ )
183
246
 
184
247
  cr = asyncio.start_server(
185
- self.__make_tcp_route_handler(route.target_machine_name, route.target_address, route.target_port),
248
+ self.__make_tcp_route_handler(
249
+ route.target_machine_name,
250
+ route.target_address,
251
+ route.target_port,
252
+ target_keep_alive_sender,
253
+ ),
186
254
  route.local_address,
187
255
  route.local_port,
188
256
  )
@@ -190,16 +258,31 @@ class Proxy:
190
258
  loop = asyncio.get_event_loop()
191
259
  loop.run_until_complete(cr)
192
260
 
193
- async def __pipe(self, target_address: str, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
261
+ async def __pipe(
262
+ self,
263
+ target_address: str,
264
+ reader: asyncio.StreamReader,
265
+ writer: asyncio.StreamWriter,
266
+ target_keep_alive_sender: TargetKeepAliveSender | None = None,
267
+ ) -> None:
194
268
  try:
195
269
  while not reader.at_eof():
196
270
  writer.write(await reader.read(2048))
271
+
272
+ if target_keep_alive_sender is not None:
273
+ target_keep_alive_sender.schedule_send()
197
274
  except ConnectionResetError:
198
275
  self._log.warning("Connection reset by target %s", target_address)
199
276
  finally:
200
277
  writer.close()
201
278
 
202
- def __make_tcp_route_handler(self, target_machine_name: str, target_address: str, target_port: int) -> Any: # noqa: ANN401
279
+ def __make_tcp_route_handler(
280
+ self,
281
+ target_machine_name: str,
282
+ target_address: str,
283
+ target_port: int,
284
+ target_keep_alive_sender: TargetKeepAliveSender | None = None,
285
+ ) -> Any: # noqa: ANN401
203
286
  async def handler(local_reader: asyncio.StreamReader, local_writer: asyncio.StreamWriter) -> None:
204
287
  if not self._monitor.is_available(target_machine_name):
205
288
  await self._wake_up_target(target_machine_name)
@@ -211,8 +294,8 @@ class Proxy:
211
294
  self._log.error("Unable to open connection to %s:%d", target_address, target_port)
212
295
  raise e
213
296
 
214
- send_pipe = self.__pipe(target_address, local_reader, target_writer)
215
- recv_pipe = self.__pipe(target_address, target_reader, local_writer)
297
+ send_pipe = self.__pipe(target_address, local_reader, target_writer, target_keep_alive_sender)
298
+ recv_pipe = self.__pipe(target_address, target_reader, local_writer, target_keep_alive_sender)
216
299
  await asyncio.gather(send_pipe, recv_pipe)
217
300
 
218
301
  return handler
@@ -221,10 +304,27 @@ class Proxy:
221
304
  target_machine_name = route.target_machine_name
222
305
  assert target_machine_name is not None
223
306
 
307
+ machine_config = self._machines[target_machine_name]
308
+ target_keep_alive_sender = None
309
+
310
+ if machine_config.keep_alive_mode:
311
+ assert machine_config.keep_alive_mode_base_url is not None
312
+
313
+ target_keep_alive_sender = TargetKeepAliveSender(
314
+ machine_config.keep_alive_mode_base_url, machine_config.keep_alive_min_interval
315
+ )
316
+
224
317
  loop = asyncio.get_event_loop()
225
318
 
226
319
  cr = loop.create_datagram_endpoint(
227
- lambda: ProxyUdpProtocol(self, self._monitor, target_machine_name, route.target_address, route.target_port),
320
+ lambda: ProxyUdpProtocol(
321
+ self,
322
+ self._monitor,
323
+ target_machine_name,
324
+ route.target_address,
325
+ route.target_port,
326
+ target_keep_alive_sender,
327
+ ),
228
328
  local_addr=(route.local_address, route.local_port),
229
329
  )
230
330
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wolsocketproxy
3
- Version: 0.2.0
3
+ Version: 0.3.1
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
@@ -17,6 +17,7 @@ Requires-Dist: aiohttp==3.13.5
17
17
  Requires-Dist: dataclass-wizard==0.39.1
18
18
  Requires-Dist: icmplib==3.0.4
19
19
  Requires-Dist: redfish==3.3.5
20
+ Requires-Dist: setproctitle==1.3.7
20
21
  Requires-Dist: wakeonlan==3.1.0
21
22
  Provides-Extra: dev
22
23
  Requires-Dist: wolsocketproxy[lint]; extra == "dev"
@@ -25,7 +26,7 @@ Provides-Extra: lint
25
26
  Requires-Dist: ty; extra == "lint"
26
27
  Requires-Dist: ruff; extra == "lint"
27
28
  Provides-Extra: build
28
- Requires-Dist: build[virtualenv]==1.4.2; extra == "build"
29
+ Requires-Dist: build[virtualenv]==1.5.0; extra == "build"
29
30
  Dynamic: license-file
30
31
 
31
32
  # wol-socket-proxy
@@ -96,10 +97,13 @@ The `wolsocketproxy.conf` has the following structure:
96
97
  // "http" - You must specify "online_check_http_url"
97
98
  // "ping" - You must specify "ip_address"
98
99
  "online_check_http_url": "http://192.168.1.124/health", // The URL used to check if machine is online, optional
99
- "online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is 300
100
+ "online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is 60
100
101
  "online_check_http_expected_code": 200, // Expected HTTP status code when using "http" method, optional, default is 200
101
102
  "ipmi_force_reset_if_power_up_failed": true, // Use IPMI power reset if this machine is not online after timeout, optional, default is false
102
- "ipmi_max_reset_try_count": 3 // Max IPMI power reset retry count before giving-up, optional, default is 3
103
+ "ipmi_max_reset_try_count": 3, // Max IPMI power reset retry count before giving-up, optional, default is 3
104
+ "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
103
107
  },
104
108
  // ... more machines ...
105
109
  },
@@ -129,3 +133,37 @@ The `wolsocketproxy.conf` has the following structure:
129
133
  ]
130
134
  }
131
135
  ```
136
+
137
+ ## Additional modes
138
+
139
+ ### Keep-alive daemon mode
140
+
141
+ This mode is used for running on target machine with auto-suspend (e.g. running `circadian` or similar), to keep the target
142
+ machine from auto suspending when running tasks.
143
+
144
+ Use the following command on the target machine to enter this mode:
145
+
146
+ ```bash
147
+ wolsocketproxy -k
148
+ ```
149
+
150
+ The config file `wolsocketproxy.conf` should look like the following:
151
+
152
+
153
+ ```json5
154
+ // Keep-alive daemon mode
155
+ {
156
+ "listen_address": "0.0.0.0", // The address to listen for HTTP server, default is 127.0.0.1
157
+ "listen_port": 18080, // The port to listen for HTTP server, default is 18080
158
+ "watchdog_feed_interval": 60, // Watchdog feed interval, seconds, default is 600
159
+ "keep_alive_method": "special_process", // How to keep target machine alive, default is "special_process"
160
+ // "special_process" - Fork a child process with specified name
161
+ "special_process_name": "wsp-keepalive", // The display name of the forked child process, default is "wsp-keepalive"
162
+ }
163
+ ```
164
+
165
+ You need to periodically send an HTTP GET request to `/watchdog/feed` at the `listen_port` in `watchdog_feed_interval` time,
166
+ or the special process will be killed.
167
+ You can combine this mode with `circadian`'s `process_block` config to keep it from auto-suspending.
168
+
169
+ 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.
@@ -3,6 +3,8 @@ README.md
3
3
  pyproject.toml
4
4
  src/wolsocketproxy/__init__.py
5
5
  src/wolsocketproxy/__main__.py
6
+ src/wolsocketproxy/common.py
7
+ src/wolsocketproxy/keepalive.py
6
8
  src/wolsocketproxy/monitor.py
7
9
  src/wolsocketproxy/proxy.py
8
10
  src/wolsocketproxy/utils.py
@@ -2,10 +2,11 @@ aiohttp==3.13.5
2
2
  dataclass-wizard==0.39.1
3
3
  icmplib==3.0.4
4
4
  redfish==3.3.5
5
+ setproctitle==1.3.7
5
6
  wakeonlan==3.1.0
6
7
 
7
8
  [build]
8
- build[virtualenv]==1.4.2
9
+ build[virtualenv]==1.5.0
9
10
 
10
11
  [dev]
11
12
  wolsocketproxy[lint]
@@ -1,44 +0,0 @@
1
- import json
2
- import logging
3
- from argparse import ArgumentParser
4
- from pathlib import Path
5
-
6
- import dataclass_wizard
7
-
8
- from wolsocketproxy.proxy import Proxy, ProxyConfig
9
-
10
-
11
- def main() -> None:
12
- logging.basicConfig(level=logging.INFO)
13
- log = logging.getLogger()
14
-
15
- parser = ArgumentParser(prog="wolsocketproxy", description="A socket proxy with wake-on-lan feature.")
16
-
17
- parser.add_argument(
18
- "-c",
19
- "--config",
20
- help="The config file to use, default lookup order: /etc/wolsocketproxy.conf, ./wolsocketproxy.conf",
21
- )
22
-
23
- args = parser.parse_args()
24
- config_path: Path
25
-
26
- if "config" in args and args.config is not None:
27
- config_path = Path(args.config)
28
- else:
29
- config_path = Path("/etc/wolsocketproxy.conf")
30
-
31
- if not config_path.exists():
32
- config_path = Path("./wolsocketproxy.conf")
33
-
34
- if not config_path.exists():
35
- log.error("Config file path %s does not exist!", config_path)
36
- return
37
-
38
- config_data = json.loads(config_path.read_text())
39
- config = dataclass_wizard.fromdict(ProxyConfig, config_data)
40
-
41
- log.info("Loaded config from %s", config_path)
42
-
43
- proxy = Proxy(config)
44
- proxy.start()
File without changes
File without changes