Files
terminalX/apps/TerminalX/iOS/TsnetProbe.swift
kid aa92d0e676 初始提交:terminalX 可运行态(M0/M1/M1.5 已真机验证)
- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连
- M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire
- M1.5: tmux -CC 原生 tab(MVP)
- 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/
- 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口)
- 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名
- 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:20:46 +08:00

90 lines
3.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import SwiftUI
import TsnetBridge
struct TsnetPeer: Codable, Identifiable {
let name: String
let ip: String
let online: Bool
let os: String
var id: String { name + ip }
}
/// M1 app tsnet tailnet IP + tailnet
@MainActor
final class TsnetProbe: ObservableObject {
@Published var status = "准备中…"
@Published var selfIP = ""
@Published var peers: [TsnetPeer] = []
@Published var done = false
func run(authKey: String) {
let base = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0]
let dir = base + "/tsnet-probe"
try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
status = "加入 tailnet 中…up 阻塞等待 Running"
Task.detached {
guard let node = TsnetbridgeNewNode(dir, "terminalx-ipad") else {
await MainActor.run { self.status = "NewNode 失败"; self.done = true }
return
}
do {
try node.up(withAuthKey: authKey, timeoutMs: 45000)
let ip = node.selfIP()
try? await Task.sleep(nanoseconds: 5_000_000_000) // netmap
let peersJSON = node.peersJSON()
let decoded = (try? JSONDecoder().decode([TsnetPeer].self,
from: Data(peersJSON.utf8))) ?? []
await MainActor.run {
self.selfIP = ip
self.peers = decoded.sorted { $0.online && !$1.online }
self.status = ip.isEmpty ? "已 Up 但未取到 IP" : "已加入 tailnet ✓"
self.done = true
}
} catch {
await MainActor.run {
self.status = "失败:\(error.localizedDescription)"
self.done = true
}
}
}
}
}
struct TsnetProbeView: View {
let authKey: String
@StateObject private var probe = TsnetProbe()
var body: some View {
VStack(spacing: 12) {
Text("terminalX · tsnet 自检 (M1)").font(.headline)
ProgressView().opacity(probe.done ? 0 : 1)
Text(probe.status).foregroundStyle(probe.selfIP.isEmpty ? Color.secondary : Color.green)
if !probe.selfIP.isEmpty {
Text("本机 Tailscale IP\(probe.selfIP)")
.font(.system(.body, design: .monospaced)).bold().foregroundStyle(.green)
}
if probe.done {
Text("tailnet 对端:\(probe.peers.count)").font(.subheadline).padding(.top, 8)
}
if !probe.peers.isEmpty {
ScrollView {
VStack(alignment: .leading, spacing: 4) {
ForEach(probe.peers) { p in
HStack(spacing: 8) {
Circle().fill(p.online ? .green : .gray).frame(width: 8, height: 8)
Text(p.ip).font(.system(.caption, design: .monospaced))
Text(p.name).font(.caption).foregroundStyle(.secondary).lineLimit(1)
Text(p.os).font(.caption2).foregroundStyle(.tertiary)
}
}
}.frame(maxWidth: .infinity, alignment: .leading)
}.frame(maxHeight: 500)
}
}
.padding(28)
.task { probe.run(authKey: authKey) }
}
}