@@ 19,6 19,13 @@
"action": {
"editable": true,
"path": "."
+ },
+ "pyxdg": {
+ "hashes": [
+ "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4",
+ "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"
+ ],
+ "version": "==0.28"
}
},
"develop": {}
@@ 8,6 8,7 @@ package_dir =
= src
install_requires =
+ pyxdg == 0.28
importlib-metadata; python_version < "3.8"
[options.packages.find]
@@ 1,7 1,14 @@
+import asyncio
+import socket
import argparse
+import os
import sys
+import json
+import datetime
-from . import package_version
+import xdg.BaseDirectory
+
+from . import package_name, package_version
from . import util
from .util import env
@@ 15,14 22,96 @@ def parse_args():
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {package_version}")
+ runtime_dir = xdg.BaseDirectory.get_runtime_dir(strict=False)
+ default_socket_root = os.path.join(runtime_dir, package_name)
+ parser.add_argument("--socket-root", metavar="DIR", default=default_socket_root)
+
parser.add_argument("--log", default=env("LOG_LEVEL", "WARN"), help="set log level")
parser.add_argument("--log-file", metavar="FILE", default=env("LOG_FILE"), help="redirect stdout and stderr to FILE")
+ subparsers = parser.add_subparsers(dest="cmd", required=True)
+
+ def add_action_arg(p):
+ p.add_argument("action", metavar="ACTION")
+ p.add_argument("--socket", metavar="SOCKET")
+
+ bind_cmd = subparsers.add_parser("bind")
+ add_action_arg(bind_cmd)
+
+ trigger_cmd = subparsers.add_parser("trigger")
+ add_action_arg(trigger_cmd)
+
return parser.parse_args()
+class Action:
+ def __init__(self, name, coro):
+ self.name = name
+ self.server = None
+ self.loop = None
+ self.coro = coro
+
+ async def bind(self, socket_path):
+ if self.server is not None:
+ raise RuntimeError("oops! already running")
+
+ logger.info(f"binding action {self.name}: {socket_path}")
+
+ os.makedirs(os.path.dirname(socket_path), exist_ok=True)
+
+ self.loop = asyncio.get_running_loop()
+ (_, self.action_server) = await self.loop.create_datagram_endpoint(
+ lambda: Action.ServerProtocol(self),
+ family = socket.AF_UNIX,
+ local_addr = socket_path,
+ )
+
+ def trigger(self):
+ self.loop.create_task(self.coro())
+
+ class ServerProtocol:
+ def __init__(self, action):
+ self.action = action
+
+ def connection_made(self, transport):
+ self.transport = transport
+
+ def connection_lost(self, exc):
+ pass
+
+ def datagram_received(self, data, addr):
+ j = json.loads(data.decode())
+ logger.info(f"action {self.action.name} triggered: {j}")
+ self.action.trigger()
+
def main():
args = parse_args()
if args.log_file is not None:
sys.stderr = sys.stdout = open(args.log_file, "a")
util.setup_logger(args.log)
logger.debug(f"args: {args}")
+
+ socket_path = args.socket
+ if socket_path is None:
+ socket_path = os.path.join(args.socket_root, args.action)
+
+ if args.cmd == "bind":
+ stop = asyncio.Event()
+ async def go():
+ stop.set()
+
+ action = Action(name=args.action, coro=go)
+
+ async def main():
+ await action.bind(socket_path)
+ await stop.wait()
+
+ asyncio.run(main())
+
+ elif args.cmd == "trigger":
+ msg = {
+ "action": args.action,
+ "timestamp": datetime.datetime.utcnow().astimezone().isoformat(timespec='seconds'),
+ }
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
+ s.sendto(json.dumps(msg).encode("UTF-8"), socket_path)
+ s.close()