Skip to content
This repository was archived by the owner on Jun 14, 2026. It is now read-only.

Commit 18c6ff2

Browse files
committed
more modification
1 parent d20aaa7 commit 18c6ff2

18 files changed

Lines changed: 523 additions & 80 deletions

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ After authentication, Apple returns a `pod` header:
118118
- Package ID is `wiki.qaq.unfaird`; launchd label is `wiki.qaq.unfaird`; default port is `8080`.
119119
- `backend-swift/Package.swift` depends on sibling `../../unfair` when built from this repository.
120120
- Root `make build` is the single production packaging entry: it builds the frontend, builds the Swift iOS backend, and emits the rootless deb.
121-
- Root `make install` depends on `make build` and installs the generated deb on `DEVICE_HOST`.
121+
- Root `make install` depends on `make build` and installs the generated deb on `DEVICE_HOST` or Theos device variables (`THEOS_DEVICE_IP`, `THEOS_DEVICE_USER`, `THEOS_DEVICE_PORT`).
122122

123123
### Backend Shared Utilities
124124

Makefile

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,27 @@ test:
1616
cd frontend && npm test
1717

1818
install: build
19-
@if [[ -z "$(DEVICE_HOST)" ]]; then echo "DEVICE_HOST is required" >&2; exit 1; fi
19+
@set -euo pipefail; \
2020
host="$(DEVICE_HOST)"; \
21+
device_ip="$(THEOS_DEVICE_IP)"; \
22+
device_user="$(THEOS_DEVICE_USER)"; \
23+
device_port="$(THEOS_DEVICE_PORT)"; \
24+
if [[ -z "$$device_user" ]]; then device_user="root"; fi; \
25+
if [[ -z "$$host" && -n "$$device_ip" ]]; then \
26+
if [[ "$$device_ip" == *@* ]]; then host="$$device_ip"; else host="$${device_user}@$$device_ip"; fi; \
27+
fi; \
28+
if [[ -z "$$host" ]]; then echo "DEVICE_HOST or THEOS_DEVICE_IP is required" >&2; exit 1; fi; \
29+
scp_args=(); \
30+
ssh_args=(); \
31+
if [[ -n "$$device_port" ]]; then scp_args=(-P "$$device_port"); ssh_args=(-p "$$device_port"); fi; \
2132
deb=$$(ls -t $(PACKAGE_GLOB) | head -n 1); \
2233
remote="/var/tmp/$${deb:t}"; \
23-
scp "$$deb" "$$host:$$remote"; \
24-
ssh "$$host" "apt install -y '$$remote'"; \
25-
curl -fsS "http://$${host#*@}:8080/health"
34+
scp "$${scp_args[@]}" "$$deb" "$$host:$$remote"; \
35+
ssh "$${ssh_args[@]}" "$$host" "apt install -y '$$remote'"; \
36+
health_host="$${host#*@}"; \
37+
if [[ "$$health_host" == \[*\]* ]]; then health_host="$${health_host#\[}"; health_host="$${health_host%\]}"; fi; \
38+
if [[ "$$health_host" == *:* ]]; then health_host="[$$health_host]"; fi; \
39+
curl -fsS "http://$$health_host:8080/health"
2640

2741
clean-package:
2842
rm -rf backend-swift/debs backend-swift/.theos backend-swift/.build/ios-release

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ with theos installed
1818
make install DEVICE_HOST=root@<device-host>
1919
```
2020

21+
The root install target also accepts Theos device variables:
22+
23+
```bash
24+
THEOS=/Users/libr/theos THEOS_DEVICE_IP=<device-host> make install
25+
```
26+
2127
### Use
2228

2329
visit http://{ip}:8080/ and there's one webui to use.

backend-swift/Sources/UnfairDaemonCore/PosixSpawn.swift

Lines changed: 158 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Darwin
2+
import Dispatch
23
import Foundation
34
import Vapor
45

@@ -17,15 +18,28 @@ struct PosixSpawnResult {
1718
}
1819

1920
enum PosixSpawn {
21+
enum OutputStream {
22+
case stdout
23+
case stderr
24+
}
25+
2026
static func run(
2127
executablePath: String,
2228
arguments: [String],
2329
workingDirectory: URL,
2430
sandboxProfileURL: URL? = nil,
25-
timeoutSeconds: Int? = nil
31+
timeoutSeconds: Int? = nil,
32+
onOutputLine: ((OutputStream, String) -> Void)? = nil
2633
) throws -> PosixSpawnResult {
27-
let stdoutURL = workingDirectory.appendingPathComponent("stdout.log")
28-
let stderrURL = workingDirectory.appendingPathComponent("stderr.log")
34+
var stdoutPipe = try makePipe(operation: "stdout pipe")
35+
var stderrPipe = try makePipe(operation: "stderr pipe")
36+
defer {
37+
closeIfOpen(&stdoutPipe.read)
38+
closeIfOpen(&stdoutPipe.write)
39+
closeIfOpen(&stderrPipe.read)
40+
closeIfOpen(&stderrPipe.write)
41+
}
42+
2943
let launch = launchCommand(
3044
executablePath: executablePath,
3145
arguments: arguments,
@@ -39,8 +53,12 @@ enum PosixSpawn {
3953
#if !os(iOS)
4054
try throwIfFailed(posix_spawn_file_actions_addchdir_np(&actions, workingDirectory.path), operation: "posix_spawn_file_actions_addchdir_np")
4155
#endif
42-
try throwIfFailed(posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, stdoutURL.path, O_WRONLY | O_CREAT | O_TRUNC, 0o644), operation: "stdout redirect")
43-
try throwIfFailed(posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, stderrURL.path, O_WRONLY | O_CREAT | O_TRUNC, 0o644), operation: "stderr redirect")
56+
try throwIfFailed(posix_spawn_file_actions_adddup2(&actions, stdoutPipe.write, STDOUT_FILENO), operation: "stdout redirect")
57+
try throwIfFailed(posix_spawn_file_actions_adddup2(&actions, stderrPipe.write, STDERR_FILENO), operation: "stderr redirect")
58+
try throwIfFailed(posix_spawn_file_actions_addclose(&actions, stdoutPipe.read), operation: "stdout read close")
59+
try throwIfFailed(posix_spawn_file_actions_addclose(&actions, stderrPipe.read), operation: "stderr read close")
60+
try throwIfFailed(posix_spawn_file_actions_addclose(&actions, stdoutPipe.write), operation: "stdout write close")
61+
try throwIfFailed(posix_spawn_file_actions_addclose(&actions, stderrPipe.write), operation: "stderr write close")
4462

4563
var attributes: posix_spawnattr_t?
4664
try throwIfFailed(posix_spawnattr_init(&attributes), operation: "posix_spawnattr_init")
@@ -72,10 +90,58 @@ enum PosixSpawn {
7290
try throwIfFailed(spawnStatus, operation: "posix_spawn \(launch.executablePath)")
7391
#endif
7492

75-
let waitStatus = try wait(for: pid, timeoutSeconds: timeoutSeconds)
93+
closeIfOpen(&stdoutPipe.write)
94+
closeIfOpen(&stderrPipe.write)
95+
96+
let group = DispatchGroup()
97+
let outputQueue = DispatchQueue.global(qos: .utility)
98+
var stdout = Data()
99+
var stderr = Data()
100+
var stdoutError: Error?
101+
var stderrError: Error?
102+
103+
group.enter()
104+
outputQueue.async {
105+
do {
106+
stdout = try readOutputPipe(stdoutPipe.read, stream: .stdout, onOutputLine: onOutputLine)
107+
} catch {
108+
stdoutError = error
109+
}
110+
group.leave()
111+
}
112+
113+
group.enter()
114+
outputQueue.async {
115+
do {
116+
stderr = try readOutputPipe(stderrPipe.read, stream: .stderr, onOutputLine: onOutputLine)
117+
} catch {
118+
stderrError = error
119+
}
120+
group.leave()
121+
}
76122

77-
let stdout = try Data(contentsOf: stdoutURL)
78-
let stderr = try Data(contentsOf: stderrURL)
123+
let waitStatus: Int32
124+
var waitError: Error?
125+
do {
126+
waitStatus = try wait(for: pid, timeoutSeconds: timeoutSeconds)
127+
} catch {
128+
waitError = error
129+
waitStatus = 0
130+
}
131+
132+
group.wait()
133+
closeIfOpen(&stdoutPipe.read)
134+
closeIfOpen(&stderrPipe.read)
135+
136+
if let waitError = waitError {
137+
throw waitError
138+
}
139+
if let stdoutError = stdoutError {
140+
throw stdoutError
141+
}
142+
if let stderrError = stderrError {
143+
throw stderrError
144+
}
79145
return PosixSpawnResult(exitCode: exitCode(from: waitStatus), stdout: stdout, stderr: stderr)
80146
}
81147

@@ -115,6 +181,90 @@ enum PosixSpawn {
115181
}
116182
}
117183

184+
private static func throwIfErrnoFailed(_ status: Int32, operation: String) throws {
185+
guard status == 0 else {
186+
throw Abort(.internalServerError, reason: "\(operation) failed: \(String(cString: strerror(errno)))")
187+
}
188+
}
189+
190+
private static func makePipe(operation: String) throws -> (read: Int32, write: Int32) {
191+
var fds = [Int32](repeating: -1, count: 2)
192+
let status = fds.withUnsafeMutableBufferPointer { buffer in
193+
pipe(buffer.baseAddress!)
194+
}
195+
try throwIfErrnoFailed(status, operation: operation)
196+
return (fds[0], fds[1])
197+
}
198+
199+
private static func closeIfOpen(_ fd: inout Int32) {
200+
guard fd >= 0 else {
201+
return
202+
}
203+
close(fd)
204+
fd = -1
205+
}
206+
207+
private static func readOutputPipe(
208+
_ fd: Int32,
209+
stream: OutputStream,
210+
onOutputLine: ((OutputStream, String) -> Void)?
211+
) throws -> Data {
212+
var output = Data()
213+
var pendingLine = ""
214+
var buffer = [UInt8](repeating: 0, count: 4096)
215+
216+
while true {
217+
let count = Darwin.read(fd, &buffer, buffer.count)
218+
if count > 0 {
219+
let chunk = Data(buffer[0..<count])
220+
output.append(chunk)
221+
emitCompleteLines(from: chunk, pendingLine: &pendingLine, stream: stream, onOutputLine: onOutputLine)
222+
continue
223+
}
224+
if count == 0 {
225+
emitPendingLine(pendingLine, stream: stream, onOutputLine: onOutputLine)
226+
return output
227+
}
228+
if errno == EINTR {
229+
continue
230+
}
231+
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
232+
}
233+
}
234+
235+
private static func emitCompleteLines(
236+
from chunk: Data,
237+
pendingLine: inout String,
238+
stream: OutputStream,
239+
onOutputLine: ((OutputStream, String) -> Void)?
240+
) {
241+
guard let onOutputLine = onOutputLine else {
242+
return
243+
}
244+
245+
pendingLine += String(decoding: chunk, as: UTF8.self)
246+
let parts = pendingLine.components(separatedBy: .newlines)
247+
let endedWithNewline = pendingLine.unicodeScalars.last.map { CharacterSet.newlines.contains($0) } ?? false
248+
let completeLines = endedWithNewline ? parts : Array(parts.dropLast())
249+
250+
for line in completeLines {
251+
emitPendingLine(line, stream: stream, onOutputLine: onOutputLine)
252+
}
253+
pendingLine = endedWithNewline ? "" : parts.last ?? ""
254+
}
255+
256+
private static func emitPendingLine(
257+
_ line: String,
258+
stream: OutputStream,
259+
onOutputLine: ((OutputStream, String) -> Void)?
260+
) {
261+
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
262+
guard trimmed.isEmpty == false else {
263+
return
264+
}
265+
onOutputLine?(stream, trimmed)
266+
}
267+
118268
private static func wait(for pid: pid_t, timeoutSeconds: Int?) throws -> Int32 {
119269
var waitStatus: Int32 = 0
120270
guard let timeoutSeconds = timeoutSeconds else {

backend-swift/Sources/UnfairDaemonCore/SimulatorIPABuilder.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ enum SimulatorIPABuilder {
4040
.appendingPathComponent("\(baseName).simulator.\(fileExtension)")
4141
}
4242

43-
static func ensureSimulatorIpa(sourceURL: URL) throws -> URL {
43+
static func ensureSimulatorIpa(sourceURL: URL, log: (String) -> Void = { _ in }) throws -> URL {
4444
let outputURL = simulatorIpaURL(for: sourceURL)
4545
if try isFresh(outputURL: outputURL, sourceURL: sourceURL) {
46+
log("using cached simulator IPA")
4647
return outputURL
4748
}
4849

49-
try buildSimulatorIpa(sourceURL: sourceURL, outputURL: outputURL)
50+
log("building simulator IPA")
51+
try buildSimulatorIpa(sourceURL: sourceURL, outputURL: outputURL, log: log)
5052
return outputURL
5153
}
5254

@@ -72,7 +74,7 @@ enum SimulatorIPABuilder {
7274
return outputDate >= sourceDate
7375
}
7476

75-
private static func buildSimulatorIpa(sourceURL: URL, outputURL: URL) throws {
77+
private static func buildSimulatorIpa(sourceURL: URL, outputURL: URL, log: (String) -> Void) throws {
7678
let temporaryRoot = FileManager.default.temporaryDirectory
7779
.appendingPathComponent("asspp-simulator-\(UUID().uuidString)", isDirectory: true)
7880
let extractionURL = temporaryRoot.appendingPathComponent("extracted", isDirectory: true)
@@ -86,21 +88,27 @@ enum SimulatorIPABuilder {
8688
try? FileManager.default.removeItem(at: outputTempURL)
8789
}
8890

91+
log("extracting IPA payload")
8992
try FileManager.default.unzipItem(at: sourceURL, to: extractionURL, skipCRC32: true)
9093

9194
let appURL = try singlePayloadAppDirectory(in: extractionURL)
95+
log("preparing app bundle \(appURL.lastPathComponent)")
9296
try prepareAppBundle(appURL)
9397

98+
log("patching Mach-O load commands")
9499
let patchedFiles = try patchMachOFiles(in: appURL)
95100
guard patchedFiles.isEmpty == false else {
96101
throw Abort(.internalServerError, reason: "No Mach-O files with LC_BUILD_VERSION or LC_VERSION_MIN_IPHONEOS found in IPA")
97102
}
103+
log("patched \(patchedFiles.count) Mach-O file\(patchedFiles.count == 1 ? "" : "s")")
98104

99105
let ldidPath = try resolveLdidPath()
100106
for fileURL in patchedFiles {
107+
log("signing \(fileURL.lastPathComponent)")
101108
try signMachOFile(fileURL, ldidPath: ldidPath, workingDirectory: temporaryRoot)
102109
}
103110

111+
log("packaging simulator IPA")
104112
try? FileManager.default.removeItem(at: outputTempURL)
105113
try FileManager.default.zipItem(
106114
at: extractionURL,
@@ -110,6 +118,7 @@ enum SimulatorIPABuilder {
110118
)
111119
try? FileManager.default.removeItem(at: outputURL)
112120
try FileManager.default.moveItem(at: outputTempURL, to: outputURL)
121+
log("simulator IPA packaged")
113122
}
114123

115124
private static func singlePayloadAppDirectory(in extractionURL: URL) throws -> URL {

0 commit comments

Comments
 (0)