p2p-swift / Tests / IrdP2PSignalingTests / Helpers / LocalServers.swift
520 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
import Foundation
import Testing
@testable import IrdP2PSignaling

/// Runs the node's real Go `signal` and `mailbox` servers on loopback for
/// integration tests. Gated on `AP2P_REPO` (a checkout of the node repo) and a
/// `go` toolchain; the tests skip cleanly otherwise.
final class LocalServers: @unchecked Sendable {
    static let appID = "11111111-2222-4333-8444-555555555555"
    static let origin = "https://test.example"
    static let secret = "0123456789abcdef"

    let signalPort: Int
    let mailboxPort: Int
    private var signal: Process?
    private var mailbox: Process?
    private let dir: URL

    static var repo: String? {
        let env = ProcessInfo.processInfo.environment
        guard let r = env["AP2P_REPO"], !r.isEmpty, FileManager.default.fileExists(atPath: r + "/signal/main.go") else { return nil }
        return r
    }

    static var isAvailable: Bool { repo != nil && goPath != nil }

    static var goPath: String? {
        for p in ["/usr/local/go/bin/go", "/opt/homebrew/bin/go", "/usr/bin/go"] where FileManager.default.isExecutableFile(atPath: p) { return p }
        return nil
    }

    var signalBase: URL { URL(string: "http://127.0.0.1:\(signalPort)")! }
    var mailboxBase: URL { URL(string: "http://127.0.0.1:\(mailboxPort)/mailbox/v1")! }
    var wsURL: URL { URL(string: "ws://127.0.0.1:\(signalPort)/ws")! }

    init() throws {
        let repo = try #require(LocalServers.repo)
        let go = try #require(LocalServers.goPath)
        signalPort = Int.random(in: 20000..<40000)
        mailboxPort = signalPort + 1
        dir = FileManager.default.temporaryDirectory.appendingPathComponent("ird-swift-servers-\(UUID().uuidString)")
        try FileManager.default.createDirectory(at: dir.appendingPathComponent("data"), withIntermediateDirectories: true)

        let signalApps = """
        {"turn":{"secret":"\(LocalServers.secret)","realm":"ap2p.test","ttl":3600,"urls":["stun:127.0.0.1:3478","turn:127.0.0.1:3478?transport=udp"]},
         "limits":{"messages_per_sec":30,"message_burst":60,"max_frame_bytes":65536},
         "apps":{"\(LocalServers.appID)":{"hosts":["127.0.0.1","localhost"],"origins":["\(LocalServers.origin)"],
           "limits":{"max_rooms":10,"max_room_peers":4,"max_conns_per_ip":50,"max_bytes_per_sec":262144},
           "mailbox":{"ttl_hours":1,"max_mb":16,"max_message_kb":64,"mint_ip_per_min":600,"mint_app_per_min":600,"open_mint":true}}}}
        """
        let mailboxApps = """
        {"node":{"secret":"\(LocalServers.secret)","max_disk_bytes":67108864},
         "apps":{"\(LocalServers.appID)":{"hosts":["127.0.0.1","localhost"],"origins":["\(LocalServers.origin)"],"open_mint":true,
           "limits":{"mailbox_ttl_hours":1,"mailbox_max_mb":16,"mailbox_max_message_kb":64}}}}
        """
        let signalConf = dir.appendingPathComponent("signal-apps.json")
        let mailboxConf = dir.appendingPathComponent("mailbox-apps.json")
        try signalApps.write(to: signalConf, atomically: true, encoding: .utf8)
        try mailboxApps.write(to: mailboxConf, atomically: true, encoding: .utf8)

        // each server is its own Go module: run from inside its directory
        signal = try LocalServers.launch(go: go, cwd: repo + "/signal", args: ["run", ".", "-listen", "127.0.0.1:\(signalPort)", "-apps", signalConf.path])
        mailbox = try LocalServers.launch(go: go, cwd: repo + "/mailbox", args: ["run", ".", "-listen", "127.0.0.1:\(mailboxPort)", "-config", mailboxConf.path, "-data", dir.appendingPathComponent("data").path])
    }

    private static func launch(go: String, cwd: String, args: [String]) throws -> Process {
        let p = Process()
        p.executableURL = URL(fileURLWithPath: go)
        p.arguments = args
        p.currentDirectoryURL = URL(fileURLWithPath: cwd)
        p.standardOutput = FileHandle.nullDevice
        p.standardError = FileHandle.nullDevice
        try p.run()
        return p
    }

    /// Waits until both servers answer (`go run` compiles first, which can take a while).
    func waitUntilReady(timeout: Duration = .seconds(120)) async throws {
        let deadline = ContinuousClock.now + timeout
        let http = URLSessionHTTPTransport()
        var signalUp = false, mailboxUp = false
        while ContinuousClock.now < deadline, !(signalUp && mailboxUp) {
            if signal?.isRunning == false || mailbox?.isRunning == false { throw P2PError.config("a local server exited early") }
            if !signalUp, let r = try? await http.send(URLRequest(url: signalBase.appendingPathComponent("healthz"), timeoutInterval: 1)), r.status == 200 { signalUp = true }
            if !mailboxUp, let r = try? await http.send(URLRequest(url: mailboxBase.appendingPathComponent("rooms/probe/messages"), timeoutInterval: 1)), r.status == 401 { mailboxUp = true }
            if !(signalUp && mailboxUp) { try await Task.sleep(for: .milliseconds(200)) }
        }
        guard signalUp && mailboxUp else { throw P2PError.timeout }
    }

    func stop() {
        for p in [signal, mailbox] { p?.terminate() }
        // `go run` execs the compiled binary as a child; make sure the listeners go away too
        for port in [signalPort, mailboxPort] {
            let k = Process()
            k.executableURL = URL(fileURLWithPath: "/bin/sh")
            // only the LISTENING process: our own test runner holds client sockets on this port
            k.arguments = ["-c", "lsof -ti tcp:\(port) -sTCP:LISTEN | xargs kill 2>/dev/null"]
            try? k.run()
            k.waitUntilExit()
        }
        try? FileManager.default.removeItem(at: dir)
    }
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/p2p-swift.git