- macOS plugin: Swift bridge + universal xcframework (arm64 + x86_64) - macOS podspec with direct force_load (no script phase needed — single slice) - Make TailscaleStart() idempotent — return success if already started - Document macOS entitlements (network.client + network.server) - Build script: ./build_go.sh [ios|macos|all] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
163 lines
6.0 KiB
Swift
163 lines
6.0 KiB
Swift
import Cocoa
|
|
import FlutterMacOS
|
|
|
|
public class TsnetFlutterPlugin: NSObject, FlutterPlugin {
|
|
|
|
private lazy var stateDir: String = {
|
|
let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
|
let dir = appSupport.appendingPathComponent("tailscale_state", isDirectory: true)
|
|
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
return dir.path
|
|
}()
|
|
|
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
|
let channel = FlutterMethodChannel(name: "tsnet_flutter", binaryMessenger: registrar.messenger)
|
|
let instance = TsnetFlutterPlugin()
|
|
registrar.addMethodCallDelegate(instance, channel: channel)
|
|
}
|
|
|
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
switch call.method {
|
|
case "start":
|
|
handleStart(call, result: result)
|
|
case "startProxy":
|
|
handleStartProxy(call, result: result)
|
|
case "stopProxy":
|
|
handleStopProxy(result: result)
|
|
case "stop":
|
|
handleStop(result: result)
|
|
case "status":
|
|
handleStatus(result: result)
|
|
case "tailscaleIP":
|
|
handleTailscaleIP(result: result)
|
|
default:
|
|
result(FlutterMethodNotImplemented)
|
|
}
|
|
}
|
|
|
|
private func consumeCString(_ ptr: UnsafeMutablePointer<CChar>?) -> String? {
|
|
guard let ptr = ptr else { return nil }
|
|
let str = String(cString: ptr)
|
|
TailscaleFreeString(ptr)
|
|
return str
|
|
}
|
|
|
|
private func handleStart(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
guard let args = call.arguments as? [String: Any],
|
|
let authKey = args["authKey"] as? String,
|
|
let hostname = args["hostname"] as? String else {
|
|
result(FlutterError(code: "INVALID_ARGS", message: "Missing authKey or hostname", details: nil))
|
|
return
|
|
}
|
|
|
|
let dir = self.stateDir
|
|
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
|
let errPtr = dir.withCString { dirC in
|
|
authKey.withCString { keyC in
|
|
hostname.withCString { hostC in
|
|
TailscaleStart(
|
|
UnsafeMutablePointer(mutating: dirC),
|
|
UnsafeMutablePointer(mutating: keyC),
|
|
UnsafeMutablePointer(mutating: hostC)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
let err = self.consumeCString(errPtr)
|
|
DispatchQueue.main.async {
|
|
if let err = err {
|
|
result(FlutterError(code: "START_FAILED", message: err, details: nil))
|
|
} else {
|
|
result(nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleStartProxy(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
guard let args = call.arguments as? [String: Any],
|
|
let ip = args["ip"] as? String,
|
|
let port = args["port"] as? Int else {
|
|
result(FlutterError(code: "INVALID_ARGS", message: "Missing ip or port", details: nil))
|
|
return
|
|
}
|
|
|
|
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
|
var localPort: CInt = 0
|
|
let errPtr = ip.withCString { ipC in
|
|
TailscaleStartProxy(
|
|
UnsafeMutablePointer(mutating: ipC),
|
|
CInt(port),
|
|
&localPort
|
|
)
|
|
}
|
|
let err = self.consumeCString(errPtr)
|
|
DispatchQueue.main.async {
|
|
if let err = err {
|
|
result(FlutterError(code: "PROXY_FAILED", message: err, details: nil))
|
|
} else {
|
|
result(Int(localPort))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleStopProxy(result: @escaping FlutterResult) {
|
|
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
|
let err = self.consumeCString(TailscaleStopProxy())
|
|
DispatchQueue.main.async {
|
|
if let err = err {
|
|
result(FlutterError(code: "STOP_PROXY_FAILED", message: err, details: nil))
|
|
} else {
|
|
result(nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleStop(result: @escaping FlutterResult) {
|
|
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
|
let err = self.consumeCString(TailscaleStop())
|
|
DispatchQueue.main.async {
|
|
if let err = err {
|
|
result(FlutterError(code: "STOP_FAILED", message: err, details: nil))
|
|
} else {
|
|
result(nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleStatus(result: @escaping FlutterResult) {
|
|
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
|
var errPtr: UnsafeMutablePointer<CChar>? = nil
|
|
let jsonPtr = TailscaleStatus(&errPtr)
|
|
let err = self.consumeCString(errPtr)
|
|
let json = self.consumeCString(jsonPtr)
|
|
DispatchQueue.main.async {
|
|
if let err = err {
|
|
result(FlutterError(code: "STATUS_FAILED", message: err, details: nil))
|
|
} else {
|
|
result(json)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleTailscaleIP(result: @escaping FlutterResult) {
|
|
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
|
var errPtr: UnsafeMutablePointer<CChar>? = nil
|
|
let ipPtr = TailscaleIP(&errPtr)
|
|
let err = self.consumeCString(errPtr)
|
|
let ip = self.consumeCString(ipPtr)
|
|
DispatchQueue.main.async {
|
|
if let err = err {
|
|
result(FlutterError(code: "IP_FAILED", message: err, details: nil))
|
|
} else {
|
|
result(ip)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|