Files
mudfish-decky/main.py
T
ysj7705andClaude Opus 5.5 3c28c9dac2 feat: install, update and remove Mudfish from the panel
- Install: download the latest package from mudfish.net, verify the
  Makeself MD5, unpack the bzip2 tar ourselves (no installer script is
  executed; path traversal and links rejected) into /opt/mudfish/<ver>.
- Update: checked at most daily; button-only; refused while the tunnel is
  up; carries the saved login (.conf, .conf.db) into the new version,
  restarts the daemon and keeps only the previous version.
- Remove: button with confirmation, and automatically when the plugin is
  uninstalled: VPN off, daemon stopped, mudfish-tap removed,
  /opt/mudfish deleted, plugin settings cleared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:58:32 +09:00

98 lines
3.8 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, 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 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)
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:
await asyncio.to_thread(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)
async def _unload(self):
await asyncio.to_thread(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.
await asyncio.to_thread(self._get_controller().uninstall)
decky.logger.info("Mudfish removed with the plugin")