BluetoothTracker/Sources/blts_tracker/Services/UpdaterService.swift

192 lines
8.2 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import UserNotifications
import AppKit
public final class UpdaterService: ObservableObject {
public static let shared = UpdaterService()
public static let didUpdateStateNotification = Notification.Name("UpdaterServiceDidUpdateStateNotification")
@Published public var isUpdating: Bool = false
@Published public var updateStatusMessage: String = ""
@Published public var progressText: String = ""
@Published public var progressVisual: String = "▰▱▱▱▱"
private var animationTimer: Timer?
private var animIndex = 0
private let animFrames = ["▰▱▱▱▱", "▰▰▱▱▱", "▰▰▰▱▱", "▰▰▰▰▱", "▰▰▰▰▰", "▱▱▱▱▱"]
private init() {}
private func startAnimation(initialStage: String) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.progressText = initialStage
self.progressVisual = self.animFrames[0]
self.animationTimer?.invalidate()
self.animIndex = 0
self.animationTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
guard let self = self, self.isUpdating else { return }
self.animIndex = (self.animIndex + 1) % self.animFrames.count
self.progressVisual = self.animFrames[self.animIndex]
NotificationCenter.default.post(name: UpdaterService.didUpdateStateNotification, object: self)
}
RunLoop.main.add(self.animationTimer!, forMode: .common)
NotificationCenter.default.post(name: UpdaterService.didUpdateStateNotification, object: self)
}
}
private func updateStage(stage: String, visual: String? = nil) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.progressText = stage
if let v = visual { self.progressVisual = v }
NotificationCenter.default.post(name: UpdaterService.didUpdateStateNotification, object: self)
}
}
private func stopAnimation() {
DispatchQueue.main.async { [weak self] in
self?.animationTimer?.invalidate()
self?.animationTimer = nil
self?.isUpdating = false
NotificationCenter.default.post(name: UpdaterService.didUpdateStateNotification, object: self)
}
}
public func checkForUpdatesAndApply() {
guard !isUpdating else { return }
isUpdating = true
updateStatusMessage = "Проверка обновлений на GitHub..."
startAnimation(initialStage: "Проверка")
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let projectDir = "/Users/autli/Documents/blts_tracker"
self?.updateStage(stage: "Связь с GitHub", visual: "▰▰▱▱▱")
let updateScript = """
cd "\(projectDir)"
git -c http.timeout=8 fetch origin main 2>&1
FETCH_STATUS=$?
if [ $FETCH_STATUS -ne 0 ]; then
echo "NETWORK_ERROR"
exit 0
fi
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse origin/main)
if [ "$LOCAL" != "$REMOTE" ] || [ -f "\(projectDir)/force_update" ]; then
echo "UPDATE_FOUND"
git pull origin main 2>&1
./build_app.sh 2>&1
USER_APPS="$HOME/Applications"
mkdir -p "$USER_APPS"
rm -rf "$USER_APPS/BLTS Tracker.app"
cp -R "\(projectDir)/BLTS Tracker.app" "$USER_APPS/"
xattr -cr "$USER_APPS/BLTS Tracker.app" 2>/dev/null || true
if [ -w "/Applications" ]; then
rm -rf "/Applications/BLTS Tracker.app"
cp -R "\(projectDir)/BLTS Tracker.app" "/Applications/"
xattr -cr "/Applications/BLTS Tracker.app" 2>/dev/null || true
fi
echo "SUCCESS"
else
echo "UP_TO_DATE"
fi
"""
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/bash")
process.arguments = ["-c", updateScript]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
do {
try process.run()
// Advance visual stages during execution
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
if self?.isUpdating == true {
self?.updateStage(stage: "Загрузка", visual: "▰▰▰▱▱")
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 2.5) {
if self?.isUpdating == true {
self?.updateStage(stage: "Сборка", visual: "▰▰▰▰▱")
}
}
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? ""
DispatchQueue.main.async {
self?.stopAnimation()
if output.contains("SUCCESS") {
self?.updateStage(stage: "Готово", visual: "▰▰▰▰▰")
self?.sendNotification(
title: "🎉 Обновление установлено",
body: "BLTS Tracker успешно обновлён с GitHub! Перезапуск..."
)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
let appPath = "\(projectDir)/BLTS Tracker.app"
let p = Process()
p.executableURL = URL(fileURLWithPath: "/usr/bin/open")
p.arguments = [appPath]
try? p.run()
exit(0)
}
} else if output.contains("NETWORK_ERROR") {
self?.sendNotification(
title: "⚠️ Ошибка сети",
body: "Не удалось связаться с GitHub. Проверьте интернет или отключите VPN."
)
} else if output.contains("UP_TO_DATE") {
self?.sendNotification(
title: "✅ У вас последняя версия",
body: "Обновлений на GitHub нет, установлена актуальная версия."
)
} else {
self?.sendNotification(
title: "ℹ️ Проверка завершена",
body: "Репозиторий проверен. Код актуален."
)
}
}
} catch {
DispatchQueue.main.async {
self?.stopAnimation()
self?.sendNotification(
title: "⚠️ Ошибка",
body: "Не удалось выполнить проверку: \(error.localizedDescription)"
)
}
}
}
}
private func sendNotification(title: String, body: String) {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
let request = UNNotificationRequest(
identifier: "updater_\(UUID().uuidString)",
content: content,
trigger: nil
)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
}