Add a native iOS client for relay-based file sharing
The SwiftUI app bundles the Go swarm engine through gomobile, so an iPhone is a real peer using the existing PAKE handshake, chunk verification, and relay fallback. XcodeGen builds the project and devicectl installs it on the paired phone.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// A headless-ish smoke test driven by launch arguments:
|
||||
///
|
||||
/// xcrun simctl launch <udid> dev.tzgyn.share \
|
||||
/// -shareSelfTest -shareSelfTestRelay 127.0.0.1:9009 \
|
||||
/// -shareSelfTestCode 1234-selftest-share-apple
|
||||
///
|
||||
/// The test starts a receive room, waits for one complete file, and writes
|
||||
/// `Documents/share-selftest.txt`. Console output is unreliable from a
|
||||
/// simulator and impossible to read from a device, so the report on disk is
|
||||
/// the source of truth.
|
||||
@MainActor
|
||||
enum SelfTest {
|
||||
private static var started = false
|
||||
|
||||
static func runIfRequested(model: AppModel) {
|
||||
let args = ProcessInfo.processInfo.arguments
|
||||
guard !started else { return }
|
||||
let relay = value(after: "-shareSelfTestRelay", in: args) ?? "127.0.0.1:9009"
|
||||
let code = value(after: "-shareSelfTestCode", in: args)
|
||||
?? (args.contains("-shareSelfTestSend")
|
||||
? "1234-selftest-send-apple"
|
||||
: "1234-selftest-share-apple")
|
||||
|
||||
if args.contains("-shareSelfTestSend") {
|
||||
started = true
|
||||
runSend(model: model, relay: relay, code: code)
|
||||
} else if args.contains("-shareSelfTest") {
|
||||
started = true
|
||||
runReceive(model: model, relay: relay, code: code)
|
||||
}
|
||||
}
|
||||
|
||||
private static func runReceive(model: AppModel, relay: String, code: String) {
|
||||
model.mode = .receive
|
||||
model.relay = relay
|
||||
model.code = code
|
||||
model.startReceive()
|
||||
|
||||
Task { @MainActor in
|
||||
let deadline = Date().addingTimeInterval(120)
|
||||
var lastStatus: RoomStatus?
|
||||
|
||||
while Date() < deadline {
|
||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||
if let status = model.status {
|
||||
lastStatus = status
|
||||
if status.allComplete {
|
||||
writeReport(result: "ok", model: model, status: status, error: nil)
|
||||
return
|
||||
}
|
||||
if !status.running, let error = status.error, !error.isEmpty {
|
||||
writeReport(result: "room-error", model: model, status: status, error: error)
|
||||
return
|
||||
}
|
||||
}
|
||||
if model.status == nil, let error = model.errorMessage, !error.isEmpty {
|
||||
writeReport(result: "start-error", model: model, status: nil, error: error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
writeReport(
|
||||
result: "timeout",
|
||||
model: model,
|
||||
status: lastStatus,
|
||||
error: "no file completed before the 120s deadline"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func runSend(model: AppModel, relay: String, code: String) {
|
||||
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let seedDir = documents.appendingPathComponent("Seeds/selftest-send", isDirectory: true)
|
||||
let seedURL = seedDir.appendingPathComponent("share-send-selftest.bin")
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: seedDir, withIntermediateDirectories: true)
|
||||
let data = Data(repeating: 0x5A, count: 2 * 1024 * 1024)
|
||||
try data.write(to: seedURL)
|
||||
} catch {
|
||||
writeReport(result: "seed-error", model: model, status: nil, error: error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
model.mode = .send
|
||||
model.relay = relay
|
||||
model.code = code
|
||||
model.pickedFiles = [
|
||||
PickedFile(url: seedURL, name: seedURL.lastPathComponent, size: 2 * 1024 * 1024)
|
||||
]
|
||||
model.startSend()
|
||||
|
||||
Task { @MainActor in
|
||||
let deadline = Date().addingTimeInterval(120)
|
||||
var lastStatus: RoomStatus?
|
||||
var sawPeer = false
|
||||
|
||||
while Date() < deadline {
|
||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||
if let status = model.status {
|
||||
lastStatus = status
|
||||
if status.peers > 0 { sawPeer = true }
|
||||
if sawPeer, status.allComplete {
|
||||
// Give the receiver a moment to finish the last chunks and
|
||||
// disconnect before recording the result.
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
writeReport(result: "ok-seeded", model: model, status: status, error: nil)
|
||||
return
|
||||
}
|
||||
if !status.running, let error = status.error, !error.isEmpty {
|
||||
writeReport(result: "room-error", model: model, status: status, error: error)
|
||||
return
|
||||
}
|
||||
}
|
||||
if model.status == nil, let error = model.errorMessage, !error.isEmpty {
|
||||
writeReport(result: "start-error", model: model, status: nil, error: error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
writeReport(
|
||||
result: "timeout",
|
||||
model: model,
|
||||
status: lastStatus,
|
||||
error: "no peer completed the seed before the 120s deadline"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func value(after flag: String, in args: [String]) -> String? {
|
||||
guard let index = args.firstIndex(of: flag), index + 1 < args.count else { return nil }
|
||||
return args[index + 1]
|
||||
}
|
||||
|
||||
private static func writeReport(
|
||||
result: String,
|
||||
model: AppModel,
|
||||
status: RoomStatus?,
|
||||
error: String?
|
||||
) {
|
||||
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let url = documents.appendingPathComponent("share-selftest.txt")
|
||||
|
||||
var lines = [
|
||||
"result=\(result)",
|
||||
"code=\(status?.code ?? model.code)",
|
||||
"relay=\(status?.relay ?? model.relay)",
|
||||
"running=\(status?.running ?? false)",
|
||||
"peers=\(status?.peers ?? 0)",
|
||||
"files=\(status?.files.count ?? 0)",
|
||||
]
|
||||
if let error, !error.isEmpty {
|
||||
lines.append("error=\(error.replacingOccurrences(of: "\n", with: " "))")
|
||||
}
|
||||
for file in status?.files ?? [] {
|
||||
let localURL: URL?
|
||||
if model.mode == .send {
|
||||
localURL = model.pickedFiles.first(where: { $0.name == file.name })?.url
|
||||
} else {
|
||||
localURL = model.receivedURL(for: file)
|
||||
}
|
||||
let exists = localURL.map { FileManager.default.fileExists(atPath: $0.path) } ?? false
|
||||
let size = localURL.flatMap {
|
||||
try? $0.resourceValues(forKeys: [.fileSizeKey]).fileSize
|
||||
} ?? 0
|
||||
lines.append(
|
||||
"file=\(file.name) size=\(file.size) chunks=\(file.haveChunks)/\(file.totalChunks) complete=\(file.complete) exists=\(exists) diskSize=\(size)"
|
||||
)
|
||||
}
|
||||
|
||||
try? lines.joined(separator: "\n").appending("\n").write(
|
||||
to: url,
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
print("Share self-test \(result): \(url.path)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user