Skip to content

Commit aa272d9

Browse files
author
nats1412019@gmail.com
committed
Show updater progress logs in tray apps
1 parent dd894ff commit aa272d9

3 files changed

Lines changed: 641 additions & 117 deletions

File tree

menubar/CodexBotMenu.swift

Lines changed: 276 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,97 @@ class AppDelegate: NSObject, NSApplicationDelegate {
484484
}
485485
}
486486

487+
private func showUpdateProgressWindow() -> (NSWindow, NSTextView) {
488+
let window = NSWindow(
489+
contentRect: NSRect(x: 0, y: 0, width: 720, height: 480),
490+
styleMask: [.titled, .closable],
491+
backing: .buffered,
492+
defer: false
493+
)
494+
window.title = L("Updating Codex Discord", "Codex Discord 업데이트 중")
495+
window.center()
496+
497+
let contentView = NSView(frame: window.contentView?.bounds ?? .zero)
498+
contentView.autoresizingMask = [.width, .height]
499+
window.contentView = contentView
500+
501+
let titleLabel = NSTextField(labelWithString: L("Update in progress...", "업데이트 진행 중..."))
502+
titleLabel.font = NSFont.boldSystemFont(ofSize: 15)
503+
titleLabel.frame = NSRect(x: 20, y: 438, width: 680, height: 22)
504+
contentView.addSubview(titleLabel)
505+
506+
let descLabel = NSTextField(labelWithString: L(
507+
"The log below shows each update step and command output.",
508+
"아래 로그에 업데이트 단계와 명령 출력이 표시됩니다."
509+
))
510+
descLabel.textColor = .secondaryLabelColor
511+
descLabel.frame = NSRect(x: 20, y: 416, width: 680, height: 18)
512+
contentView.addSubview(descLabel)
513+
514+
let scrollView = NSScrollView(frame: NSRect(x: 20, y: 20, width: 680, height: 388))
515+
scrollView.hasVerticalScroller = true
516+
scrollView.autohidesScrollers = true
517+
518+
let textView = NSTextView(frame: scrollView.bounds)
519+
textView.isEditable = false
520+
textView.isSelectable = true
521+
textView.font = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)
522+
textView.textContainerInset = NSSize(width: 8, height: 8)
523+
if #available(macOS 10.14, *) {
524+
textView.backgroundColor = .textBackgroundColor
525+
}
526+
scrollView.documentView = textView
527+
contentView.addSubview(scrollView)
528+
529+
window.makeKeyAndOrderFront(nil)
530+
NSApp.activate(ignoringOtherApps: true)
531+
return (window, textView)
532+
}
533+
534+
private func appendUpdateLog(_ textView: NSTextView, _ text: String) {
535+
let normalized = text.replacingOccurrences(of: "\r\n", with: "\n")
536+
DispatchQueue.main.async {
537+
let prefix = textView.string.hasSuffix("\n") || textView.string.isEmpty ? "" : "\n"
538+
textView.textStorage?.append(NSAttributedString(string: prefix + normalized))
539+
if !normalized.hasSuffix("\n") {
540+
textView.textStorage?.append(NSAttributedString(string: "\n"))
541+
}
542+
textView.scrollToEndOfDocument(nil)
543+
}
544+
}
545+
546+
@discardableResult
547+
private func runShellStreaming(_ command: String, onOutput: @escaping (String) -> Void) -> Int32 {
548+
let task = Process()
549+
task.launchPath = "/bin/bash"
550+
task.arguments = ["-c", command]
551+
552+
let pipe = Pipe()
553+
task.standardOutput = pipe
554+
task.standardError = pipe
555+
556+
let handle = pipe.fileHandleForReading
557+
handle.readabilityHandler = { fileHandle in
558+
let data = fileHandle.availableData
559+
if data.isEmpty { return }
560+
if let text = String(data: data, encoding: .utf8), !text.isEmpty {
561+
onOutput(text)
562+
}
563+
}
564+
565+
do {
566+
try task.run()
567+
task.waitUntilExit()
568+
} catch {
569+
onOutput(error.localizedDescription + "\n")
570+
handle.readabilityHandler = nil
571+
return -1
572+
}
573+
574+
handle.readabilityHandler = nil
575+
return task.terminationStatus
576+
}
577+
487578
@objc private func performUpdate() {
488579
NSApp.activate(ignoringOtherApps: true)
489580
let alert = NSAlert()
@@ -516,84 +607,207 @@ class AppDelegate: NSObject, NSApplicationDelegate {
516607
}
517608

518609
if alert.runModal() == .alertFirstButtonReturn {
519-
let wasRunning = isRunning()
520-
if wasRunning {
521-
runShell("launchctl unload '\(plistDst)' 2>/dev/null")
522-
}
610+
let (progressWindow, logView) = showUpdateProgressWindow()
611+
appendUpdateLog(logView, L("Starting update...", "업데이트를 시작합니다..."))
523612

524-
let hasLocalChanges = !runShell("cd '\(botDir)' && git status --porcelain 2>/dev/null")
525-
.trimmingCharacters(in: .whitespacesAndNewlines)
526-
.isEmpty
527-
if hasLocalChanges {
528-
_ = runShell("cd '\(botDir)' && git stash push -u -m codex-discord-auto-update >/dev/null 2>&1")
529-
}
530-
531-
runShell("cd '\(botDir)' && git fetch origin main --tags")
532-
let pullOutput = runShell("cd '\(botDir)' && git reset --hard origin/main 2>&1")
613+
DispatchQueue.global(qos: .userInitiated).async {
614+
let wasRunning = self.isRunning()
615+
if wasRunning {
616+
self.appendUpdateLog(logView, self.L("Stopping running bot...", "실행 중인 봇을 중지합니다..."))
617+
_ = self.runShellStreaming("launchctl unload '\(self.plistDst)' 2>/dev/null") {
618+
self.appendUpdateLog(logView, $0)
619+
}
620+
}
533621

534-
let afterPull = runShell("cd '\(botDir)' && git rev-parse HEAD").trimmingCharacters(in: .whitespacesAndNewlines)
535-
let remote = runShell("cd '\(botDir)' && git rev-parse origin/main").trimmingCharacters(in: .whitespacesAndNewlines)
536-
if !afterPull.isEmpty && !remote.isEmpty && afterPull != remote {
622+
let hasLocalChanges = !self.runShell("cd '\(self.botDir)' && git status --porcelain 2>/dev/null")
623+
.trimmingCharacters(in: .whitespacesAndNewlines)
624+
.isEmpty
537625
if hasLocalChanges {
538-
_ = runShell("cd '\(botDir)' && git stash pop >/dev/null 2>&1")
626+
self.appendUpdateLog(logView, self.L("Stashing local changes...", "로컬 변경사항을 stash 합니다..."))
627+
_ = self.runShellStreaming("cd '\(self.botDir)' && git stash push -u -m codex-discord-auto-update 2>&1") {
628+
self.appendUpdateLog(logView, $0)
629+
}
539630
}
540-
let errAlert = NSAlert()
541-
errAlert.messageText = L("Update Failed", "업데이트 실패")
542-
errAlert.informativeText = L("git pull failed:\n", "git pull 실패:\n") + pullOutput
543-
errAlert.alertStyle = .critical
544-
errAlert.runModal()
545-
if wasRunning {
546-
generatePlist()
547-
runShell("launchctl load '\(plistDst)'")
631+
632+
self.appendUpdateLog(logView, self.L("Fetching latest changes...", "최신 변경사항을 가져옵니다..."))
633+
let fetchStatus = self.runShellStreaming("cd '\(self.botDir)' && git fetch origin main --tags 2>&1") {
634+
self.appendUpdateLog(logView, $0)
548635
}
549-
return
550-
}
551636

552-
let output = runShell("cd '\(botDir)' && npm install && npm rebuild better-sqlite3 && npm run build 2>&1")
637+
self.appendUpdateLog(logView, self.L("Resetting to origin/main...", "origin/main 기준으로 맞춥니다..."))
638+
let resetStatus = self.runShellStreaming("cd '\(self.botDir)' && git reset --hard origin/main 2>&1") {
639+
self.appendUpdateLog(logView, $0)
640+
}
553641

554-
if hasLocalChanges {
555-
_ = runShell("cd '\(botDir)' && git stash pop >/dev/null 2>&1")
556-
}
642+
let afterPull = self.runShell("cd '\(self.botDir)' && git rev-parse HEAD").trimmingCharacters(in: .whitespacesAndNewlines)
643+
let remote = self.runShell("cd '\(self.botDir)' && git rev-parse origin/main").trimmingCharacters(in: .whitespacesAndNewlines)
644+
if fetchStatus != 0 || resetStatus != 0 || (!afterPull.isEmpty && !remote.isEmpty && afterPull != remote) {
645+
if hasLocalChanges {
646+
self.appendUpdateLog(logView, self.L("Restoring stashed changes...", "stash 변경사항을 복원합니다..."))
647+
_ = self.runShellStreaming("cd '\(self.botDir)' && git stash pop 2>&1") {
648+
self.appendUpdateLog(logView, $0)
649+
}
650+
}
651+
if wasRunning {
652+
self.generatePlist()
653+
_ = self.runShell("launchctl load '\(self.plistDst)'")
654+
}
655+
DispatchQueue.main.async {
656+
let errAlert = NSAlert()
657+
errAlert.messageText = self.L("Update Failed", "업데이트 실패")
658+
errAlert.informativeText = self.L(
659+
"git sync failed. Check the update log window for details.",
660+
"git 동기화가 실패했습니다. 자세한 내용은 업데이트 로그 창을 확인하세요."
661+
)
662+
errAlert.alertStyle = .critical
663+
errAlert.runModal()
664+
progressWindow.makeKeyAndOrderFront(nil)
665+
}
666+
return
667+
}
557668

558-
currentVersion = getVersion()
559-
updateAvailable = false
560-
561-
// Rebuild the menu bar app itself and restart
562-
let swiftSrc = "\(botDir)/menubar/CodexBotMenu.swift"
563-
let swiftBin = "\(botDir)/menubar/CodexBotMenu"
564-
if FileManager.default.fileExists(atPath: swiftSrc) {
565-
// Accept Xcode license if needed before compiling
566-
let xcrunCheck = runShell("xcrun --find swiftc 2>&1")
567-
if xcrunCheck.contains("license") || xcrunCheck.contains("error") {
568-
runShell("osascript -e 'do shell script \"xcodebuild -license accept\" with administrator privileges' 2>/dev/null")
669+
if hasLocalChanges {
670+
self.appendUpdateLog(logView, self.L("Restoring stashed changes...", "stash 변경사항을 복원합니다..."))
671+
_ = self.runShellStreaming("cd '\(self.botDir)' && git stash pop 2>&1") {
672+
self.appendUpdateLog(logView, $0)
673+
}
569674
}
570-
runShell("swiftc -o '\(swiftBin)' '\(swiftSrc)' -framework Cocoa 2>&1")
571675

572-
if wasRunning {
573-
generatePlist()
574-
runShell("launchctl load '\(plistDst)'")
676+
self.appendUpdateLog(logView, self.L("Installing npm dependencies...", "npm 의존성을 설치합니다..."))
677+
let installStatus = self.runShellStreaming("cd '\(self.botDir)' && npm install 2>&1") {
678+
self.appendUpdateLog(logView, $0)
679+
}
680+
if installStatus != 0 {
681+
if wasRunning {
682+
self.generatePlist()
683+
_ = self.runShell("launchctl load '\(self.plistDst)'")
684+
}
685+
DispatchQueue.main.async {
686+
let errAlert = NSAlert()
687+
errAlert.messageText = self.L("Update Failed", "업데이트 실패")
688+
errAlert.informativeText = self.L(
689+
"npm install failed. Check the update log window for details.",
690+
"npm install이 실패했습니다. 자세한 내용은 업데이트 로그 창을 확인하세요."
691+
)
692+
errAlert.alertStyle = .critical
693+
errAlert.runModal()
694+
progressWindow.makeKeyAndOrderFront(nil)
695+
}
696+
return
697+
}
698+
699+
self.appendUpdateLog(logView, self.L("Rebuilding better-sqlite3...", "better-sqlite3를 다시 빌드합니다..."))
700+
let rebuildStatus = self.runShellStreaming("cd '\(self.botDir)' && npm rebuild better-sqlite3 2>&1") {
701+
self.appendUpdateLog(logView, $0)
702+
}
703+
if rebuildStatus != 0 {
704+
if wasRunning {
705+
self.generatePlist()
706+
_ = self.runShell("launchctl load '\(self.plistDst)'")
707+
}
708+
DispatchQueue.main.async {
709+
let errAlert = NSAlert()
710+
errAlert.messageText = self.L("Update Failed", "업데이트 실패")
711+
errAlert.informativeText = self.L(
712+
"Native rebuild failed. Check the update log window for details.",
713+
"네이티브 재빌드가 실패했습니다. 자세한 내용은 업데이트 로그 창을 확인하세요."
714+
)
715+
errAlert.alertStyle = .critical
716+
errAlert.runModal()
717+
progressWindow.makeKeyAndOrderFront(nil)
718+
}
719+
return
575720
}
576721

577-
// Restart menu bar app (detached from terminal)
578-
runShell("nohup bash -c 'sleep 1 && \"\(swiftBin)\"' > /dev/null 2>&1 &")
722+
self.appendUpdateLog(logView, self.L("Building project...", "프로젝트를 빌드합니다..."))
723+
let buildStatus = self.runShellStreaming("cd '\(self.botDir)' && npm run build 2>&1") {
724+
self.appendUpdateLog(logView, $0)
725+
}
726+
if buildStatus != 0 {
727+
if wasRunning {
728+
self.generatePlist()
729+
_ = self.runShell("launchctl load '\(self.plistDst)'")
730+
}
731+
DispatchQueue.main.async {
732+
let errAlert = NSAlert()
733+
errAlert.messageText = self.L("Update Failed", "업데이트 실패")
734+
errAlert.informativeText = self.L(
735+
"Build failed. Check the update log window for details.",
736+
"빌드가 실패했습니다. 자세한 내용은 업데이트 로그 창을 확인하세요."
737+
)
738+
errAlert.alertStyle = .critical
739+
errAlert.runModal()
740+
progressWindow.makeKeyAndOrderFront(nil)
741+
}
742+
return
743+
}
579744

580-
NSApplication.shared.terminate(nil)
581-
return
582-
}
745+
self.currentVersion = self.getVersion()
746+
self.updateAvailable = false
747+
self.appendUpdateLog(logView, self.L("Updated to version: ", "업데이트된 버전: ") + self.currentVersion)
748+
749+
let swiftSrc = "\(self.botDir)/menubar/CodexBotMenu.swift"
750+
let swiftBin = "\(self.botDir)/menubar/CodexBotMenu"
751+
if FileManager.default.fileExists(atPath: swiftSrc) {
752+
self.appendUpdateLog(logView, self.L("Rebuilding menu bar app...", "메뉴바 앱을 다시 빌드합니다..."))
753+
let xcrunCheck = self.runShell("xcrun --find swiftc 2>&1")
754+
if xcrunCheck.contains("license") || xcrunCheck.contains("error") {
755+
self.appendUpdateLog(logView, self.L("Requesting Xcode license approval...", "Xcode 라이선스 승인을 요청합니다..."))
756+
_ = self.runShell("osascript -e 'do shell script \"xcodebuild -license accept\" with administrator privileges' 2>/dev/null")
757+
}
583758

584-
if wasRunning {
585-
generatePlist()
586-
runShell("launchctl load '\(plistDst)'")
587-
}
759+
let swiftStatus = self.runShellStreaming("swiftc -o '\(swiftBin)' '\(swiftSrc)' -framework Cocoa 2>&1") {
760+
self.appendUpdateLog(logView, $0)
761+
}
762+
if swiftStatus != 0 {
763+
if wasRunning {
764+
self.generatePlist()
765+
_ = self.runShell("launchctl load '\(self.plistDst)'")
766+
}
767+
DispatchQueue.main.async {
768+
let errAlert = NSAlert()
769+
errAlert.messageText = self.L("Update Failed", "업데이트 실패")
770+
errAlert.informativeText = self.L(
771+
"Menu bar app rebuild failed. Check the update log window for details.",
772+
"메뉴바 앱 재빌드가 실패했습니다. 자세한 내용은 업데이트 로그 창을 확인하세요."
773+
)
774+
errAlert.alertStyle = .critical
775+
errAlert.runModal()
776+
progressWindow.makeKeyAndOrderFront(nil)
777+
}
778+
return
779+
}
588780

589-
let doneAlert = NSAlert()
590-
doneAlert.messageText = L("Update Complete", "업데이트 완료")
591-
doneAlert.informativeText = L("Updated to version: ", "업데이트된 버전: ") + currentVersion + "\n\n" + output
592-
doneAlert.alertStyle = .informational
593-
doneAlert.runModal()
781+
if wasRunning {
782+
self.generatePlist()
783+
_ = self.runShell("launchctl load '\(self.plistDst)'")
784+
}
594785

595-
updateStatus()
596-
buildMenu()
786+
self.appendUpdateLog(logView, self.L("Restarting menu bar app...", "메뉴바 앱을 재시작합니다..."))
787+
_ = self.runShell("nohup bash -c 'sleep 1 && \"\(swiftBin)\"' > /dev/null 2>&1 &")
788+
DispatchQueue.main.async {
789+
NSApplication.shared.terminate(nil)
790+
}
791+
return
792+
}
793+
794+
if wasRunning {
795+
self.generatePlist()
796+
_ = self.runShell("launchctl load '\(self.plistDst)'")
797+
}
798+
799+
DispatchQueue.main.async {
800+
progressWindow.close()
801+
let doneAlert = NSAlert()
802+
doneAlert.messageText = self.L("Update Complete", "업데이트 완료")
803+
doneAlert.informativeText = self.L("Updated to version: ", "업데이트된 버전: ") + self.currentVersion
804+
doneAlert.alertStyle = .informational
805+
doneAlert.runModal()
806+
807+
self.updateStatus()
808+
self.buildMenu()
809+
}
810+
}
597811
}
598812
}
599813

0 commit comments

Comments
 (0)