ysj7705/mudfish-decky/pipeline/head This commit looks good
Advanced shows the installed plugin version and the latest Gitea release. '최신 플러그인 주소 복사' puts the release zip URL on the clipboard for Decky's Install Plugin from URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
108 lines
4.5 KiB
Python
108 lines
4.5 KiB
Python
import asyncio
|
|
import logging
|
|
import os
|
|
import sys
|
|
import urllib.parse
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "py_modules"))
|
|
|
|
import decky # noqa: E402
|
|
|
|
from mudfish_decky import installer, selfupdate, system # noqa: E402
|
|
from mudfish_decky.client import MudfishClient # noqa: E402
|
|
from mudfish_decky.controller import Controller # noqa: E402
|
|
from mudfish_decky.relay import LoginRelay # noqa: E402
|
|
from mudfish_decky.store import Store # noqa: E402
|
|
|
|
RESUME_POLL_SECONDS = 2
|
|
LATEST_POLL_TICKS = 1800 # every hour at 2s per tick
|
|
LOGIN_RELAY_SECONDS = 300
|
|
|
|
|
|
class Plugin:
|
|
controller = None
|
|
|
|
def _get_controller(self) -> Controller:
|
|
if self.controller is None:
|
|
installed = system.find_mudfish()
|
|
base = system.api_base(installed[1]) if installed else system.DEFAULT_API
|
|
store = Store(os.path.join(decky.DECKY_PLUGIN_SETTINGS_DIR, "settings.json"))
|
|
target = urllib.parse.urlsplit(base)
|
|
self.controller = Controller(
|
|
MudfishClient(base), system, store,
|
|
relay_factory=lambda: LoginRelay(target=(target.hostname, target.port or 80), ttl=LOGIN_RELAY_SECONDS),
|
|
installer=installer,
|
|
)
|
|
return self.controller
|
|
|
|
async def start_login(self) -> dict:
|
|
return await asyncio.to_thread(self._get_controller().start_login)
|
|
|
|
async def stop_login(self) -> dict:
|
|
return await asyncio.to_thread(self._get_controller().stop_login)
|
|
|
|
async def plugin_release(self) -> dict:
|
|
latest = await asyncio.to_thread(selfupdate.latest_plugin_release)
|
|
return {"current": decky.DECKY_PLUGIN_VERSION, "latest": latest}
|
|
|
|
async def install_mudfish(self) -> dict:
|
|
return await asyncio.to_thread(self._get_controller().install)
|
|
|
|
async def update_mudfish(self) -> dict:
|
|
return await asyncio.to_thread(self._get_controller().update)
|
|
|
|
async def uninstall_mudfish(self) -> dict:
|
|
return await asyncio.to_thread(self._get_controller().uninstall)
|
|
|
|
async def get_state(self) -> dict:
|
|
return await asyncio.to_thread(self._get_controller().state)
|
|
|
|
async def connect(self) -> dict:
|
|
return await asyncio.to_thread(self._get_controller().connect)
|
|
|
|
async def disconnect(self) -> dict:
|
|
return await asyncio.to_thread(self._get_controller().disconnect)
|
|
|
|
async def set_node(self, choice: str) -> dict:
|
|
return await asyncio.to_thread(self._get_controller().set_node, str(choice))
|
|
|
|
async def list_nodes(self) -> list:
|
|
return await asyncio.to_thread(self._get_controller().nodes)
|
|
|
|
async def _main(self):
|
|
log = logging.getLogger("mudfish_decky")
|
|
log.setLevel(logging.INFO)
|
|
for handler in decky.logger.handlers:
|
|
log.addHandler(handler)
|
|
controller = self._get_controller()
|
|
# Bring up the idle launcher and sign in so nodes are listed. Never starts the tunnel.
|
|
await asyncio.to_thread(controller.prepare)
|
|
# Clean up Mudfish if the plugin folder disappears (uninstall), even if Decky kills us first.
|
|
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
|
await asyncio.to_thread(system.ensure_cleanup_watcher, plugin_dir, decky.DECKY_PLUGIN_SETTINGS_DIR)
|
|
decky.logger.info("Mudfish VPN plugin loaded")
|
|
# Watch for sleep/resume even while the panel is closed; a resumed tunnel is turned off.
|
|
# Also look for a newer Mudfish release now and then (the controller limits it to once a day).
|
|
ticks = 0
|
|
while True:
|
|
try:
|
|
controller.check_resume(system.sleep_offset())
|
|
if ticks % LATEST_POLL_TICKS == 0:
|
|
await asyncio.to_thread(controller.refresh_latest)
|
|
except Exception as e:
|
|
decky.logger.warning("background check failed: %s", e)
|
|
ticks += 1
|
|
await asyncio.sleep(RESUME_POLL_SECONDS)
|
|
|
|
# _unload/_uninstall run synchronously on purpose: Decky closes the plugin socket right after
|
|
# asking it to stop, and its listener then spins on EOF and starves the event loop, so an
|
|
# awaited worker thread would never resume and the plugin is SIGKILLed after 5 seconds.
|
|
async def _unload(self):
|
|
self._get_controller().shutdown()
|
|
decky.logger.info("Mudfish VPN plugin unloaded")
|
|
|
|
async def _uninstall(self):
|
|
# Removing the plugin removes Mudfish too (user decision): VPN off, daemon stopped, /opt/mudfish gone.
|
|
self._get_controller().uninstall()
|
|
decky.logger.info("Mudfish removed with the plugin")
|