Skip to content

Commit 5ff65ef

Browse files
committed
Support keep-alive mode in proxy.
1 parent 19124de commit 5ff65ef

6 files changed

Lines changed: 126 additions & 18 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ The `wolsocketproxy.conf` has the following structure:
6969
"online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is 60
7070
"online_check_http_expected_code": 200, // Expected HTTP status code when using "http" method, optional, default is 200
7171
"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
7376
},
7477
// ... more machines ...
7578
},
@@ -131,3 +134,5 @@ The config file `wolsocketproxy.conf` should look like the following:
131134
You need to periodically send an HTTP GET request to `/watchdog/feed` at the `listen_port` in `watchdog_feed_interval` time,
132135
or the special process will be killed.
133136
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.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "wolsocketproxy"
7-
version = "0.3.0"
7+
version = "0.3.1"
88
description = "A socket proxy with wake-on-lan feature."
99
authors = [
1010
{name = "Song Fuchang", email = "song.fc@gmail.com"}
@@ -72,4 +72,5 @@ lint.ignore = [
7272
"ISC001",
7373
"TRY003",
7474
"TRY201",
75+
"PLR0913",
7576
]

src/wolsocketproxy/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
URL_WATCHDOG_FEED = "/watchdog/feed"

src/wolsocketproxy/keepalive.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from aiohttp.web import Request, Response
1313
from setproctitle import setproctitle
1414

15+
from wolsocketproxy.common import URL_WATCHDOG_FEED
16+
1517

1618
@dataclass
1719
class KeepAliveConfig:
@@ -41,11 +43,7 @@ def __init__(self, config: KeepAliveConfig) -> None:
4143

4244
self._web_app = web.Application()
4345

44-
self._web_app.add_routes(
45-
[
46-
web.get("/watchdog/feed", self._handle_watchdog_feed)
47-
]
48-
)
46+
self._web_app.add_routes([web.get(URL_WATCHDOG_FEED, self._handle_watchdog_feed)])
4947

5048
async def _watchdog_timer(self) -> None:
5149
while True:
@@ -97,8 +95,7 @@ def _start_special_process(self) -> bool:
9795
return True
9896

9997
self._log.info(
100-
"Started special process with name %s, PID %d",
101-
self._config.special_process_name, self._special_process_id
98+
"Started special process with name %s, PID %d", self._config.special_process_name, self._special_process_id
10299
)
103100

104101
return False
@@ -123,7 +120,9 @@ def _start_loop() -> None:
123120

124121
self._log.info(
125122
"Keep-alive daemon started at %s:%d, watchdog feed interval %ds.",
126-
self._config.listen_address, self._config.listen_port, self._config.watchdog_feed_interval
123+
self._config.listen_address,
124+
self._config.listen_port,
125+
self._config.watchdog_feed_interval,
127126
)
128127

129128
web.run_app(self._web_app, host=self._config.listen_address, port=self._config.listen_port)

src/wolsocketproxy/proxy.py

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
import logging
44
from dataclasses import dataclass
55
from logging import Logger
6+
from threading import Thread
67
from typing import Any, Literal, override
78

9+
import aiohttp
810
import wakeonlan
911
from redfish.rest.v1 import HttpClient, redfish_client
1012

13+
from wolsocketproxy.common import URL_WATCHDOG_FEED
1114
from wolsocketproxy.monitor import Monitor, MonitorConfig
1215
from wolsocketproxy.utils import perform_ipmi_action
1316

@@ -29,6 +32,10 @@ class MachineConfig:
2932
online_check_http_expected_code: int = 200
3033
online_check_timeout: int = 60
3134

35+
keep_alive_mode: bool = False
36+
keep_alive_mode_base_url: str | None = None
37+
keep_alive_min_interval: int = 1
38+
3239

3340
@dataclass
3441
class ProxyRoute:
@@ -56,23 +63,67 @@ class ProxyConfig:
5663
ipmi_configs: list[IPMIConfig] | None = None
5764

5865

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+
59102
class ProxyUdpProtocol(asyncio.DatagramProtocol):
60103
_proxy: "Proxy"
61104
_monitor: Monitor
62105
_transport: asyncio.transports.DatagramTransport
63106
_target_machine_name: str
64107
_target_address: str
65108
_target_port: int
109+
_target_keep_alive_sender: TargetKeepAliveSender | None = None
66110
_target_pair: tuple[str, int]
67111

68112
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,
70120
) -> None:
71121
self._proxy = proxy
72122
self._monitor = monitor
73123
self._target_machine_name = target_machine_name
74124
self._target_address = target_address
75125
self._target_port = target_port
126+
self._target_keep_alive_sender = target_keep_alive_sender
76127
self._target_pair = (target_address, target_port)
77128

78129
@override
@@ -92,6 +143,9 @@ async def handle_datagram(self, data: bytes, addr: tuple[str | Any, int]) -> Non
92143

93144
self._transport.sendto(data, self._target_pair)
94145

146+
if self._target_keep_alive_sender is not None:
147+
self._target_keep_alive_sender.schedule_send()
148+
95149

96150
class Proxy:
97151
_log: Logger = logging.getLogger()
@@ -180,26 +234,55 @@ def __create_route(self, route: ProxyRoute) -> None:
180234

181235
def __create_tcp_route(self, route: ProxyRoute) -> None:
182236
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+
)
183246

184247
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+
),
186254
route.local_address,
187255
route.local_port,
188256
)
189257

190258
loop = asyncio.get_event_loop()
191259
loop.run_until_complete(cr)
192260

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:
194268
try:
195269
while not reader.at_eof():
196270
writer.write(await reader.read(2048))
271+
272+
if target_keep_alive_sender is not None:
273+
target_keep_alive_sender.schedule_send()
197274
except ConnectionResetError:
198275
self._log.warning("Connection reset by target %s", target_address)
199276
finally:
200277
writer.close()
201278

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
203286
async def handler(local_reader: asyncio.StreamReader, local_writer: asyncio.StreamWriter) -> None:
204287
if not self._monitor.is_available(target_machine_name):
205288
await self._wake_up_target(target_machine_name)
@@ -211,8 +294,8 @@ async def handler(local_reader: asyncio.StreamReader, local_writer: asyncio.Stre
211294
self._log.error("Unable to open connection to %s:%d", target_address, target_port)
212295
raise e
213296

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)
216299
await asyncio.gather(send_pipe, recv_pipe)
217300

218301
return handler
@@ -221,10 +304,27 @@ def __create_udp_route(self, route: ProxyRoute) -> None:
221304
target_machine_name = route.target_machine_name
222305
assert target_machine_name is not None
223306

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+
224317
loop = asyncio.get_event_loop()
225318

226319
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+
),
228328
local_addr=(route.local_address, route.local_port),
229329
)
230330

wolsocketproxy.conf.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"online_check_timeout": 300,
1010
"online_check_http_expected_code": 200,
1111
"ipmi_force_reset_if_power_up_failed": true,
12-
"ipmi_max_reset_try_count": 3
12+
"ipmi_max_reset_try_count": 3,
13+
"keep_alive_mode": true,
14+
"keep_alive_mode_base_url": "http://192.168.1.124:8080"
1315
}
1416
},
1517
"routes": [

0 commit comments

Comments
 (0)