diff --git a/README.md b/README.md index 1f79e45..7597fb7 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ curl -fsSL https://example.org/dokril/lemana-vpn/raw/branch/main/install.sh \ ```sh vpn --status -Modules: core=ok, bitwarden=on, touchid=on, dns=on, patches=active, keychain=password:yes/totp_seed:yes +Modules: core=ok, bitwarden=on, touchid=on, dns=on, app=on, autostart=on, patches=active, keychain=password:yes/totp_seed:yes VPN disconnected ``` @@ -111,6 +111,8 @@ VPN disconnected | `bitwarden=missing` | Модуль включён, но `bw` не найден | | `touchid=on/off/missing` | Состояние Touch ID helper | | `dns=on/missing` | Наличие DNS cleanup wrapper | +| `app=on/missing` | Установлен ли `~/Applications/LemanaVPN.app` | +| `autostart=on/off` | Есть ли LaunchAgent для запуска приложения при логине | | `patches=active/pending` | Применены ли runtime-патчи `openconnect-lite` | | `keychain=password:yes/totp_seed:yes` | Есть ли LDAP-пароль и TOTP seed в Keychain | @@ -131,12 +133,12 @@ Detected state: openconnect-lite: yes Bitwarden CLI: no Touch ID helper: no - Swift: yes - Menu Bar app: no - LaunchAgent: no DNS cleanup: no sudoers: no/no shell aliases: no + Swift: yes + Menu Bar app: no + LaunchAgent: no Keychain password: no Keychain TOTP seed: no ``` @@ -236,6 +238,8 @@ sh install.sh --without-touchid Приложение живёт в macOS status bar, запускает `~/bin/vpn-lemanapro.sh --json`, показывает состояние VPN, IP, оставшееся время сессии, health-check тоннеля и строку состояния модулей. +Если в меню видно `modules unavailable: update CLI`, значит запущенное приложение обращается к старому `~/bin/vpn-lemanapro.sh`, который ещё не умеет отдавать модульный статус. Повтори установку через `curl`; установщик обновит CLI и перезапустит уже запущенное `LemanaVPN.app`. + Для сборки нужен Swift 5.9+ из Xcode Command Line Tools: ```sh diff --git a/app/Sources/LemanaVPN/VPNManager.swift b/app/Sources/LemanaVPN/VPNManager.swift index 9a1a1b6..32a5a79 100644 --- a/app/Sources/LemanaVPN/VPNManager.swift +++ b/app/Sources/LemanaVPN/VPNManager.swift @@ -45,21 +45,29 @@ struct ModuleStatus: Decodable { var backup: Bool } + struct AppModule: Decodable { + var installed: Bool + var autostart: Bool + } + var core: Core var bitwarden: ToggleModule var touchid: ToggleModule var keychain: Keychain var dns_cleanup: DnsCleanup var patches: Patches + var app: AppModule? var summary: String { let coreState = core.openconnect && core.openconnect_lite && core.openconnect_lite_config ? "core ok" : "core missing" let bwState = bitwarden.enabled ? (bitwarden.installed ? "bw on" : "bw missing") : "bw off" let touchState = touchid.enabled ? (touchid.installed ? "touch on" : "touch missing") : "touch off" let dnsState = dns_cleanup.installed ? "dns on" : "dns missing" + let appState = app.map { $0.installed ? "app on" : "app missing" } ?? "app unknown" + let autostartState = app.map { $0.autostart ? "autostart on" : "autostart off" } ?? "autostart unknown" let patchState = patches.active ? "patches active" : "patches pending" let keychainState = "kc \(keychain.password ? "pass" : "-")/\(keychain.totp_seed ? "totp" : "-")" - return [coreState, bwState, touchState, dnsState, patchState, keychainState].joined(separator: " | ") + return [coreState, bwState, touchState, dnsState, appState, autostartState, patchState, keychainState].joined(separator: " | ") } } @@ -88,7 +96,7 @@ class VPNManager: ObservableObject { @Published var state: VPNState = .disconnected @Published var lastError: String? @Published var tunnelHealthy: Bool = true - @Published var moduleSummary: String = "modules unknown" + @Published var moduleSummary: String = "modules loading..." private var process: Process? private var outputPipe: Pipe? @@ -151,12 +159,34 @@ class VPNManager: ObservableObject { proc.terminationHandler = { [weak self] _ in let data = pipe.fileHandleForReading.readDataToEndOfFile() - guard let self = self, let text = String(data: data, encoding: .utf8) else { return } + guard let self = self else { return } + let text = String(data: data, encoding: .utf8) ?? "" let lastLine = text.split(separator: "\n").map(String.init).last ?? "" - guard let jsonData = lastLine.data(using: .utf8), - let response = try? JSONDecoder().decode(VPNStatusResponse.self, from: jsonData), - let modules = response.modules else { return } + Task { @MainActor in + guard !lastLine.isEmpty, let jsonData = lastLine.data(using: .utf8) else { + self.moduleSummary = "modules unavailable" + self.log("[modules] status refresh returned no JSON output") + return + } + + let response: VPNStatusResponse + do { + response = try JSONDecoder().decode(VPNStatusResponse.self, from: jsonData) + } catch { + self.moduleSummary = "modules unavailable" + let compact = text.replacingOccurrences(of: "\n", with: "\\n") + let preview = compact.count > 500 ? String(compact.prefix(500)) + "..." : compact + self.log("[modules] status decode failed: \(error.localizedDescription); output=\(preview)") + return + } + + guard let modules = response.modules else { + self.moduleSummary = "modules unavailable: update CLI" + self.log("[modules] status has no modules field; reinstall CLI with install.sh") + return + } + self.moduleSummary = modules.summary } } diff --git a/bin/vpn-lemanapro.sh b/bin/vpn-lemanapro.sh index d60dd66..7cdc795 100755 --- a/bin/vpn-lemanapro.sh +++ b/bin/vpn-lemanapro.sh @@ -19,6 +19,8 @@ USE_BITWARDEN="${LEMANA_VPN_USE_BITWARDEN:-1}" USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID:-1}" CACHE_BW_SESSION="${LEMANA_VPN_CACHE_BW_SESSION:-0}" DNS_CLEANUP="${LEMANA_VPN_DNS_CLEANUP:-/usr/local/sbin/lemana-vpn-dns-cleanup}" +APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}" +LAUNCH_AGENT="${LEMANA_VPN_LAUNCH_AGENT:-$HOME/Library/LaunchAgents/ru.dokops.LemanaVPN.plist}" BW_KC_SERVICE="${LEMANA_VPN_BW_KC_SERVICE:-vpn-lemanapro}" BW_KC_ACCOUNT_SESSION="${LEMANA_VPN_BW_KC_ACCOUNT_SESSION:-bw-session}" BW_KC_ACCOUNT_MASTER="${LEMANA_VPN_BW_KC_ACCOUNT_MASTER:-bw-master}" @@ -106,12 +108,15 @@ _keychain_has() { _module_status_json() { local openconnect_installed openconnect_lite_installed bitwarden_installed touchid_installed dns_cleanup_installed + local app_installed app_autostart local config_present oc_config_present patch_backup_present patches_active keychain_password keychain_totp_seed openconnect_installed="$(_module_bool command -v openconnect)" openconnect_lite_installed="$(_module_bool test -x "$OC_BIN")" bitwarden_installed="$(_module_bool command -v bw)" touchid_installed="$(_module_bool test -x "$KC_FP")" dns_cleanup_installed="$(_module_bool test -x "$DNS_CLEANUP")" + app_installed="$(_module_bool test -x "$APP_DIR/Contents/MacOS/LemanaVPN")" + app_autostart="$(_module_bool test -f "$LAUNCH_AGENT")" config_present="$(_module_bool test -f "$CONFIG_FILE")" oc_config_present="$(_module_bool test -f "$HOME/.config/openconnect-lite/config.toml")" patch_backup_present="$(_module_bool test -f "$PATCH_BACKUP_DIR/webengine_process.py.before-lemana-vpn")" @@ -119,7 +124,7 @@ _module_status_json() { keychain_password="$(_module_bool _keychain_has openconnect-lite "$KC_USERNAME")" keychain_totp_seed="$(_module_bool _keychain_has openconnect-lite "totp/$KC_USERNAME")" - printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s}}' \ + printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s},"app":{"installed":%s,"autostart":%s}}' \ "$openconnect_installed" \ "$openconnect_lite_installed" \ "$config_present" \ @@ -133,7 +138,9 @@ _module_status_json() { "$keychain_totp_seed" \ "$dns_cleanup_installed" \ "$patches_active" \ - "$patch_backup_present" + "$patch_backup_present" \ + "$app_installed" \ + "$app_autostart" } _module_human_part() { @@ -149,6 +156,7 @@ _module_human_part() { _module_status_human() { local core bitwarden_installed touchid_installed dns_cleanup_installed patches_active keychain_password keychain_totp_seed + local app_installed app_autostart if command -v openconnect >/dev/null 2>&1 && [[ -x "$OC_BIN" && -f "$HOME/.config/openconnect-lite/config.toml" ]]; then core="core=ok" else @@ -161,13 +169,17 @@ _module_status_human() { patches_active="$(_module_bool _patches_active)" keychain_password="$(_module_bool _keychain_has openconnect-lite "$KC_USERNAME")" keychain_totp_seed="$(_module_bool _keychain_has openconnect-lite "totp/$KC_USERNAME")" + app_installed="$(_module_bool test -x "$APP_DIR/Contents/MacOS/LemanaVPN")" + app_autostart="$(_module_bool test -f "$LAUNCH_AGENT")" printf 'Modules: %s, ' "$core" _module_human_part "bitwarden" "$USE_BITWARDEN" "$bitwarden_installed" printf ', ' _module_human_part "touchid" "$USE_TOUCHID" "$touchid_installed" - printf ', dns=%s, patches=%s, keychain=password:%s/totp_seed:%s\n' \ + printf ', dns=%s, app=%s, autostart=%s, patches=%s, keychain=password:%s/totp_seed:%s\n' \ "$([[ "$dns_cleanup_installed" == "true" ]] && printf on || printf missing)" \ + "$([[ "$app_installed" == "true" ]] && printf on || printf missing)" \ + "$([[ "$app_autostart" == "true" ]] && printf on || printf off)" \ "$([[ "$patches_active" == "true" ]] && printf active || printf pending)" \ "$([[ "$keychain_password" == "true" ]] && printf yes || printf no)" \ "$([[ "$keychain_totp_seed" == "true" ]] && printf yes || printf no)" diff --git a/install.sh b/install.sh index 7af8521..a95c4e8 100755 --- a/install.sh +++ b/install.sh @@ -566,6 +566,22 @@ install_launch_agent() { fi } +restart_running_menu_bar_app() { + [ "$INSTALL_APP" -eq 1 ] || return 0 + + if [ "$DRY_RUN" -eq 1 ]; then + printf '+ restart LemanaVPN.app if running\n' + return 0 + fi + + if pgrep -x LemanaVPN >/dev/null 2>&1; then + log "Restarting running LemanaVPN.app" + killall LemanaVPN >/dev/null 2>&1 || true + sleep 1 + open "$APP_DIR" >/dev/null 2>&1 || true + fi +} + install_shell_aliases() { [ "$INSTALL_ALIASES" -eq 1 ] || return 0 @@ -636,6 +652,7 @@ main() { install_touchid_helper "$tmp" install_menu_bar_app "$tmp" install_launch_agent "$tmp" + restart_running_menu_bar_app install_shell_aliases "$tmp" maybe_login_bitwarden diff --git a/tests/smoke.sh b/tests/smoke.sh index e9674b9..cceffce 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -19,5 +19,14 @@ printf '%s\n' "$output" | grep -q 'Modules: bitwarden=0 touchid=0 sudoers=1 shel printf '%s\n' "$output" | grep -q 'sudo install -d -m 755 -o root -g wheel /usr/local/sbin' printf '%s\n' "$output" | grep -q 'swift build -c release --package-path' printf '%s\n' "$output" | grep -q 'launchctl load' +printf '%s\n' "$output" | grep -q 'restart LemanaVPN.app if running' + +status_json="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)" +printf '%s\n' "$status_json" | grep -q '"modules":' +printf '%s\n' "$status_json" | grep -q '"app":' + +status_text="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status)" +printf '%s\n' "$status_text" | grep -q 'app=' +printf '%s\n' "$status_text" | grep -q 'autostart=' printf 'smoke ok\n'