~rootmos/action

7f90ac42cfb69d935738400b763eae1aaa87e870 — Gustav Behm 9 months ago 0c27aa8
Simple, quick and dirty bind/trigger system
3 files changed, 98 insertions(+), 1 deletions(-)

M Pipfile.lock
M setup.cfg
M src/action/cli.py
M Pipfile.lock => Pipfile.lock +7 -0
@@ 19,6 19,13 @@
        "action": {
            "editable": true,
            "path": "."
        },
        "pyxdg": {
            "hashes": [
                "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4",
                "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"
            ],
            "version": "==0.28"
        }
    },
    "develop": {}

M setup.cfg => setup.cfg +1 -0
@@ 8,6 8,7 @@ package_dir =
    = src

install_requires =
  pyxdg == 0.28
  importlib-metadata; python_version < "3.8"

[options.packages.find]

M src/action/cli.py => src/action/cli.py +90 -1
@@ 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()