feat(binder): MDMClient, EnrollView improvements + supervise flow_backup
- MDMClient: error handling verbessert - SuperviseRunner: robustere EOF-nach-Success Erkennung - EnrollView: Enrollment-Status-Polling, Retry-Logik - SuperviseView: UX-Verbesserungen - ConfigureView: minor cleanup - flow_backup.go: backup flow für supervise-magic Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
db0aa6d24e
commit
d65ba84eb1
@ -142,6 +142,17 @@ enum MDMClient {
|
||||
return try await enqueue(udid: udid, command: cmd).responseBody
|
||||
}
|
||||
|
||||
/// Entfernt eine App per MDM-Command (RemoveApplication). Funktioniert
|
||||
/// auf supervised-iPhones ohne User-Confirm. Returnt die EnqueueResult-
|
||||
/// commandUUID, damit Caller per readCommandResult auf Ack warten kann.
|
||||
static func removeApp(udid: String, bundleID: String = "org.rebreak.app") async throws -> EnqueueResult {
|
||||
let cmd: [String: Any] = [
|
||||
"RequestType": "RemoveApplication",
|
||||
"Identifier": bundleID,
|
||||
]
|
||||
return try await enqueue(udid: udid, command: cmd)
|
||||
}
|
||||
|
||||
/// Pusht ManagedApplicationList-Query an iPhone (welche Apps managed?).
|
||||
/// Returnt die generierte CommandUUID — Caller liest danach via
|
||||
/// `MDMStatus.readCommandResult(udid:, commandUUID:)` das Ergebnis.
|
||||
|
||||
@ -25,7 +25,7 @@ enum SuperviseRunner {
|
||||
guard let bin = Paths.firstExecutable(in: Paths.superviseMagicCandidates) else {
|
||||
throw RunnerError.binaryMissing
|
||||
}
|
||||
return try await ProcessRunner.stream(bin, arguments: ["-v", "check"], onLine: onLine)
|
||||
return try await ProcessRunner.stream(bin, arguments: ["check", "-v"], onLine: onLine)
|
||||
}
|
||||
|
||||
/// Schreibt CloudConfigurationDetails.plist auf das iPhone + reboot.
|
||||
@ -41,9 +41,11 @@ enum SuperviseRunner {
|
||||
throw RunnerError.binaryMissing
|
||||
}
|
||||
// -yes ist Pflicht: ohne TTY-Pipe hängt der Bestätigungs-Prompt sonst endlos.
|
||||
var args: [String] = verbose ? ["-v", "-yes"] : ["-yes"]
|
||||
// CLI-Konvention: <command> [flags] — command MUSS zuerst kommen.
|
||||
var args: [String] = ["supervise", "-yes"]
|
||||
if verbose { args.append("-v") }
|
||||
if force { args.append("-force") }
|
||||
args.append(contentsOf: ["-org", organizationName, "supervise"])
|
||||
args.append(contentsOf: ["-org", organizationName])
|
||||
let result = try await ProcessRunner.stream(bin, arguments: args, onLine: onLine)
|
||||
if result.exitCode != 0 {
|
||||
throw RunnerError.nonZeroExit(result.exitCode)
|
||||
@ -57,6 +59,6 @@ enum SuperviseRunner {
|
||||
guard let bin = Paths.firstExecutable(in: Paths.superviseMagicCandidates) else {
|
||||
throw RunnerError.binaryMissing
|
||||
}
|
||||
return try await ProcessRunner.stream(bin, arguments: ["-v", "-yes", "unsupervise"], onLine: onLine)
|
||||
return try await ProcessRunner.stream(bin, arguments: ["unsupervise", "-v", "-yes"], onLine: onLine)
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,15 +255,18 @@ struct ConfigureView: View {
|
||||
let pushStartTime = Date()
|
||||
|
||||
// Harte Variante fuer robuste Tests:
|
||||
// Wenn ReBreak-App schon da ist, zuerst löschen und dann frisch pushen.
|
||||
// Wenn ReBreak-App schon da ist, zuerst per MDM-Command
|
||||
// (RemoveApplication) entfernen — KEIN cfgutil mehr, weil
|
||||
// cfgutil remove-app auf iOS 18+ den 804-Fehler wirft.
|
||||
let appAlreadyInstalled = await DeviceDetector.installedAppBundleIDs().contains("org.rebreak.app")
|
||||
if appAlreadyInstalled {
|
||||
model.configureLog.append("→ Hard-Reinstall: vorhandene ReBreak-App wird entfernt …")
|
||||
try await DeviceDetector.removeApp(bundleID: "org.rebreak.app")
|
||||
model.configureLog.append("→ Hard-Reinstall: vorhandene ReBreak-App wird via MDM entfernt …")
|
||||
let removeResult = try await MDMClient.removeApp(udid: udid)
|
||||
model.configureLog.append("✓ enqueued RemoveApplication: \(removeResult.commandUUID.prefix(8))")
|
||||
let removed = await waitForAppInstalled(expectedInstalled: false)
|
||||
if !removed {
|
||||
throw NSError(domain: "Binder", code: 7, userInfo: [NSLocalizedDescriptionKey:
|
||||
"Vorhandene ReBreak-App konnte nicht sicher entfernt werden."])
|
||||
"MDM-RemoveApplication wurde gepusht aber iPhone hat App nicht entfernt. Bitte Step 4 (Enroll) wiederholen."])
|
||||
}
|
||||
model.configureLog.append("✓ Vorhandene ReBreak-App entfernt.")
|
||||
} else {
|
||||
|
||||
@ -79,6 +79,7 @@ struct EnrollView: View {
|
||||
@State private var pollTask: Task<Void, Never>?
|
||||
@State private var didAutoAdvance = false
|
||||
@State private var showUnlockModal = false
|
||||
@State private var enrollError: String?
|
||||
|
||||
private let enrollmentProfileID = "org.rebreak.mdm.enrollment"
|
||||
|
||||
@ -176,8 +177,24 @@ struct EnrollView: View {
|
||||
HStack {
|
||||
Button("Zurück") { model.goTo(.supervise) }
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(busy)
|
||||
Spacer()
|
||||
if enrollmentReady {
|
||||
if let err = enrollError {
|
||||
HStack(spacing: 8) {
|
||||
Text(err)
|
||||
.font(.callout)
|
||||
.foregroundStyle(.red)
|
||||
.lineLimit(2)
|
||||
Button("Erneut versuchen") {
|
||||
enrollError = nil
|
||||
downloadStatus = nil
|
||||
flowStatus = nil
|
||||
localPath = nil
|
||||
downloadProfile()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
} else if enrollmentReady {
|
||||
Text("Enrollment bestätigt. Weiterleitung läuft automatisch …")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
@ -190,13 +207,14 @@ struct EnrollView: View {
|
||||
}
|
||||
|
||||
private func startIfNeeded() {
|
||||
if localPath == nil && !busy && !enrollmentReady {
|
||||
if localPath == nil && !busy && !enrollmentReady && enrollError == nil {
|
||||
downloadProfile()
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadProfile() {
|
||||
let dest = "/tmp/rebreak-enrollment.mobileconfig"
|
||||
let udid = model.device?.udid
|
||||
busy = true
|
||||
downloadStatus = "Lade von mdm.rebreak.org …"
|
||||
Task {
|
||||
@ -204,14 +222,36 @@ struct EnrollView: View {
|
||||
guard let url = URL(string: "https://mdm.rebreak.org/enrollment/rebreak-enrollment.mobileconfig") else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
let (data, response) = try await URLSession.shared.data(from: url)
|
||||
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
|
||||
var request = URLRequest(url: url)
|
||||
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
|
||||
request.setValue("no-cache", forHTTPHeaderField: "Cache-Control")
|
||||
request.setValue("no-cache", forHTTPHeaderField: "Pragma")
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
guard let http = response as? HTTPURLResponse else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
try data.write(to: URL(fileURLWithPath: dest))
|
||||
guard http.statusCode == 200 else {
|
||||
let body = String(data: data, encoding: .utf8)?.prefix(200) ?? ""
|
||||
throw NSError(domain: "RebreakMDM", code: http.statusCode, userInfo: [
|
||||
NSLocalizedDescriptionKey: "MDM-Server hat Status \(http.statusCode) zurückgegeben. \(body)"
|
||||
])
|
||||
}
|
||||
// Subject-Substitution: iOS ersetzt %SerialNumber%/%UDID% nur bei
|
||||
// DEP-installed Profilen, NICHT bei Safari/AirDrop. Wir patchen
|
||||
// die Variablen lokal mit der echten UDID, damit jeder Device-Bind
|
||||
// einen eindeutigen DN beim SCEP-Server erzeugt.
|
||||
var profileData = data
|
||||
if let udid, var text = String(data: data, encoding: .utf8) {
|
||||
text = text.replacingOccurrences(of: "%SerialNumber%", with: udid)
|
||||
text = text.replacingOccurrences(of: "%UDID%", with: udid)
|
||||
if let patched = text.data(using: .utf8) {
|
||||
profileData = patched
|
||||
}
|
||||
}
|
||||
try profileData.write(to: URL(fileURLWithPath: dest))
|
||||
await MainActor.run {
|
||||
localPath = dest
|
||||
downloadStatus = "Geladen: \(dest) (\(data.count) Bytes)"
|
||||
downloadStatus = "Geladen: \(dest) (\(profileData.count) Bytes)"
|
||||
}
|
||||
await MainActor.run {
|
||||
runInstallFlow(path: dest)
|
||||
@ -220,6 +260,7 @@ struct EnrollView: View {
|
||||
await MainActor.run {
|
||||
busy = false
|
||||
downloadStatus = "Download fehlgeschlagen: \(error.localizedDescription)"
|
||||
enrollError = "MDM-Server nicht erreichbar (\(error.localizedDescription)). Bitte später erneut versuchen."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +113,14 @@ struct SuperviseView: View {
|
||||
}
|
||||
|
||||
private func startIfNeeded() {
|
||||
// Skip wenn iPad bereits von ReBreak supervised — direkt zum nächsten Step.
|
||||
if model.device?.isSupervised == true,
|
||||
(model.device?.supervisorOrgName ?? "") == "ReBreak" {
|
||||
model.supervisionLog = ["✓ Bereits von ReBreak supervised — überspringe."]
|
||||
model.supervisionError = nil
|
||||
model.supervisionRunning = false
|
||||
return
|
||||
}
|
||||
if model.supervisionLog.isEmpty && !model.supervisionRunning && model.supervisionError == nil {
|
||||
startSupervise()
|
||||
}
|
||||
@ -125,9 +133,12 @@ struct SuperviseView: View {
|
||||
task?.cancel()
|
||||
task = Task { @MainActor in
|
||||
do {
|
||||
// force=false: wenn Device schon supervised ist (z.B. nach
|
||||
// partial-success + Retry), exitet CLI mit 0 + Hinweis statt
|
||||
// den ganzen Backup-Restore-Flow nochmal durchzulaufen.
|
||||
_ = try await SuperviseRunner.supervise(
|
||||
organizationName: "ReBreak",
|
||||
force: true,
|
||||
force: false,
|
||||
verbose: model.showAdvancedLogs
|
||||
) { line in
|
||||
model.supervisionLog.append(line)
|
||||
|
||||
@ -325,6 +325,14 @@ func commitViaMCInstall(conn *device.Conn, certDER []byte, orgName string, logf
|
||||
})
|
||||
mc.Close()
|
||||
if err != nil {
|
||||
// 14002 "A cloud configuration is already present" = Backup-Sandwich
|
||||
// hat CloudConfig bereits geschrieben → MCInstall-Re-Apply redundant,
|
||||
// Supervise war erfolgreich.
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "14002") || strings.Contains(errStr, "already present") {
|
||||
logf(" ✓ MCInstall reports CloudConfig already present (attempt %d) — supervision committed via backup-sandwich", attempt)
|
||||
return nil
|
||||
}
|
||||
lastErr = err
|
||||
logf(" · attempt %d/%d: Supervise failed: %v", attempt, maxAttempts, err)
|
||||
time.Sleep(retryDelay)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user