Removed ArchUpdateService

This commit is contained in:
LemmyCook 2025-09-12 21:16:50 -04:00
parent 7d2eaa46e6
commit 5079fc78d3

View file

@ -1,704 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
Singleton {
id: updateService
// ============================================================================
// CORE PROPERTIES
// ============================================================================
// Package data
property var repoPackages: []
property var aurPackages: []
property var selectedPackages: []
property int selectedPackagesCount: 0
property string allUpdatesOutput: ""
// Update state
property bool updateInProgress: false
property bool updateFailed: false
property string lastUpdateError: ""
property bool checkFailed: false
property string lastCheckError: ""
// Monitoring state
property string capturedErrorText: ""
property string capturedSuccessText: ""
// Computed properties
readonly property bool aurBusy: checkAurUpdatesProcess.running || checkAurOnlyProcess.running
readonly property int updates: repoPackages.length
readonly property int aurUpdates: aurPackages.length
readonly property int totalUpdates: updates + aurUpdates
// Terminal validation
readonly property bool terminalAvailable: Quickshell.env("TERMINAL") !== ""
readonly property string terminalError: "TERMINAL environment variable not set"
// AUR helper validation
readonly property bool aurHelperAvailable: cachedAurHelper !== ""
readonly property string aurHelperError: "No AUR helper found (yay or paru not installed)"
// Polling cooldown (prevent excessive polling)
property int lastPollTime: 0
readonly property int pollCooldownMs: 5 * 60 * 1000 // 5 minutes
readonly property bool canPoll: (Date.now() - lastPollTime) > pollCooldownMs
// ============================================================================
// TIMERS
// ============================================================================
// Refresh timer for post-update polling
Timer {
id: refreshTimer
interval: 5000
repeat: false
onTriggered: {
doPoll()
}
}
// Timer to mark update as complete - with error handling
Timer {
id: updateCompleteTimer
interval: 30000 // Increased to 30 seconds to allow more time
repeat: false
onTriggered: {
checkForUpdateFailures()
}
}
// Timer to check if update processes are still running
Timer {
id: updateMonitorTimer
interval: 2000
repeat: true
running: updateInProgress
onTriggered: {
// Check if any update-related processes might still be running
checkUpdateStatus()
}
}
// ============================================================================
// MONITORING PROCESSES
// ============================================================================
// Process to monitor update completion
Process {
id: updateStatusProcess
command: ["pgrep", "-f", "(yay|paru).*(-S|-Syu)"]
onExited: function (exitCode) {
if (exitCode !== 0 && updateInProgress) {
// No update processes found, update likely completed
updateInProgress = false
updateMonitorTimer.stop()
errorCheckTimer.stop()
successCheckTimer.stop()
// Don't stop the complete timer - let it handle failures
// If the update actually failed, the timer will trigger and set updateFailed = true
// Refresh package lists after a short delay
Qt.callLater(() => {
doPoll()
}, 2000)
}
}
}
// Process to check for errors in log file (only when update is in progress)
Process {
id: errorCheckProcess
command: ["sh", "-c", "if [ -f /tmp/archupdater_output.log ]; then grep -i 'failed to build\\|could not resolve\\|unable to satisfy\\|failed to install\\|failed to upgrade\\|error:' /tmp/archupdater_output.log | grep -v 'ERROR_DETECTED' | tail -1; fi"]
onExited: function (exitCode) {
if (exitCode === 0 && updateInProgress && capturedErrorText.trim() !== "") {
// Error found in log
updateInProgress = false
updateFailed = true
updateCompleteTimer.stop()
updateMonitorTimer.stop()
errorCheckTimer.stop()
successCheckTimer.stop()
lastUpdateError = "Build or update error detected"
// Refresh to check actual state
Qt.callLater(() => {
doPoll()
}, 1000)
}
}
stdout: StdioCollector {
onStreamFinished: {
capturedErrorText = text || ""
}
}
}
// Process to check for successful completion
Process {
id: successCheckProcess
command: ["sh", "-c", "if [ -f /tmp/archupdater_output.log ]; then grep -i 'Update complete!\\|:: Running post-transaction hooks\\|:: Processing package changes\\|upgrading.*\\.\\.\\.\\|installing.*\\.\\.\\.\\|removing.*\\.\\.\\.' /tmp/archupdater_output.log | tail -1; fi"]
onExited: function (exitCode) {
if (exitCode === 0 && updateInProgress && capturedSuccessText.trim() !== "") {
// Success indicators found
updateInProgress = false
updateFailed = false
updateCompleteTimer.stop()
updateMonitorTimer.stop()
errorCheckTimer.stop()
successCheckTimer.stop()
lastUpdateError = ""
// Refresh to check actual state
Qt.callLater(() => {
doPoll()
}, 1000)
}
}
stdout: StdioCollector {
onStreamFinished: {
capturedSuccessText = text || ""
}
}
}
// Timer to check for success more frequently when update is in progress
Timer {
id: successCheckTimer
interval: 5000 // Check every 5 seconds
repeat: true
running: updateInProgress
onTriggered: {
if (updateInProgress && !successCheckProcess.running) {
successCheckProcess.running = true
}
}
}
// Timer to check for errors more frequently when update is in progress
Timer {
id: errorCheckTimer
interval: 5000 // Check every 5 seconds
repeat: true
running: updateInProgress
onTriggered: {
if (updateInProgress && !errorCheckProcess.running) {
errorCheckProcess.running = true
}
}
}
// ============================================================================
// MONITORING FUNCTIONS
// ============================================================================
function checkUpdateStatus() {
if (updateInProgress && !updateStatusProcess.running) {
updateStatusProcess.running = true
}
}
function checkForUpdateFailures() {
updateInProgress = false
updateFailed = true
updateCompleteTimer.stop()
updateMonitorTimer.stop()
// Refresh to check actual state after a delay
Qt.callLater(() => {
doPoll()
}, 2000)
}
// Initial check
Component.onCompleted: {
// Start AUR helper detection
getAurHelper()
// Set up a fallback timer in case detection takes too long
Qt.callLater(() => {
if (cachedAurHelper === "" && !yayCheckProcess.running && !paruCheckProcess.running) {
// No AUR helper found after reasonable time and processes have finished
checkFailed = true
lastCheckError = "No AUR helper found (yay or paru not installed)"
Logger.warn("ArchUpdater", "No AUR helper found (yay or paru)")
}
}, 5000) // 5 second fallback
}
// ============================================================================
// PACKAGE CHECKING PROCESSES
// ============================================================================
// Process for checking all updates with AUR helper (repo + AUR)
Process {
id: checkAurUpdatesProcess
command: []
onExited: function (exitCode) {
// For both yay and paru: exit code 0 = updates available, exit code 1 = no updates
if (exitCode !== 0 && exitCode !== 1) {
Logger.warn("ArchUpdater", "AUR helper check failed (code:", exitCode, ")")
checkFailed = true
lastCheckError = "Failed to check for updates (exit code: " + exitCode + ")"
aurPackages = []
repoPackages = []
}
// Don't clear checkFailed here - wait for the second process to complete
}
stdout: StdioCollector {
onStreamFinished: {
allUpdatesOutput = text
// Now get AUR-only updates to compare
checkAurOnlyProcess.running = true
}
}
}
// Process for checking AUR-only updates (to separate from repo updates)
Process {
id: checkAurOnlyProcess
command: []
onExited: function (exitCode) {
// For both yay and paru: exit code 0 = updates available, exit code 1 = no updates
if (exitCode !== 0 && exitCode !== 1) {
Logger.warn("ArchUpdater", "AUR helper AUR-only check failed (code:", exitCode, ")")
checkFailed = true
lastCheckError = "Failed to check AUR updates (exit code: " + exitCode + ")"
aurPackages = []
repoPackages = []
} else {
// Only clear checkFailed if both processes succeeded
// Check if the first process also succeeded (no error was set)
if (!checkFailed) {
checkFailed = false
lastCheckError = ""
}
}
}
stdout: StdioCollector {
onStreamFinished: {
parseAllUpdatesOutput(allUpdatesOutput, text)
Logger.log("ArchUpdater", "found", repoPackages.length, "repo package(s) and", aurPackages.length, "AUR package(s) to upgrade")
}
}
}
// ============================================================================
// PARSING FUNCTIONS
// ============================================================================
// Generic package parsing function
function parsePackageOutput(output, source) {
const lines = output.trim().split('\n').filter(line => line.trim())
const packages = []
for (const line of lines) {
const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
if (m) {
packages.push({
"name": m[1],
"oldVersion": m[2],
"newVersion": m[3],
"description": `${m[1]} ${m[2]} -> ${m[3]}`,
"source": source
})
}
}
// Only update if we have new data or if this is a fresh check
if (packages.length > 0 || output.trim() === "") {
if (source === "repo") {
repoPackages = packages
} else {
aurPackages = packages
}
}
}
// Parse all updates output (repo + AUR packages)
function parseAllUpdatesOutput(allOutput, aurOnlyOutput) {
const allLines = allOutput.trim().split('\n').filter(line => line.trim())
const aurOnlyLines = aurOnlyOutput.trim().split('\n').filter(line => line.trim())
// Create a set of AUR package names for quick lookup
const aurPackageNames = new Set()
for (const line of aurOnlyLines) {
const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
if (m) {
aurPackageNames.add(m[1])
}
}
const repoPackages = []
const aurPackages = []
for (const line of allLines) {
const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
if (m) {
const packageInfo = {
"name": m[1],
"oldVersion": m[2],
"newVersion": m[3],
"description": `${m[1]} ${m[2]} -> ${m[3]}`
}
// Check if this package is in the AUR-only list
if (aurPackageNames.has(m[1])) {
packageInfo.source = "aur"
aurPackages.push(packageInfo)
} else {
packageInfo.source = "repo"
repoPackages.push(packageInfo)
}
}
}
// Update the package lists
if (repoPackages.length > 0 || aurPackages.length > 0 || allOutput.trim() === "") {
updateService.repoPackages = repoPackages
updateService.aurPackages = aurPackages
}
}
function doPoll() {
// Prevent excessive polling
if (aurBusy || !canPoll) {
return
}
// Check if we have a cached AUR helper
if (cachedAurHelper !== "") {
// Clear error state when helper is available
if (checkFailed && lastCheckError.includes("No AUR helper found")) {
checkFailed = false
lastCheckError = ""
}
checkAurUpdatesProcess.command = [cachedAurHelper, "-Qu"]
checkAurOnlyProcess.command = [cachedAurHelper, getAurOnlyFlag()]
// Start AUR updates check (includes both repo and AUR packages)
checkAurUpdatesProcess.running = true
lastPollTime = Date.now()
} else {
// AUR helper detection is still in progress or failed
// Try to detect again if not already in progress
if (!yayCheckProcess.running && !paruCheckProcess.running) {
getAurHelper()
}
Logger.warn("ArchUpdater", "AUR helper detection in progress or failed")
}
}
// ============================================================================
// UPDATE FUNCTIONS
// ============================================================================
// Helper function to generate update command with error detection
function generateUpdateCommand(baseCommand) {
return baseCommand + " 2>&1 | tee /tmp/archupdater_output.log; if [ $? -ne 0 ]; then echo 'ERROR_DETECTED'; fi; echo 'Update complete! Press Enter to close...'; read -p 'Press Enter to continue...'"
}
// Update all packages (repo + AUR)
function runUpdate() {
if (totalUpdates === 0) {
doPoll()
return
}
// Reset any previous error states
updateFailed = false
lastUpdateError = ""
updateInProgress = true
capturedErrorText = ""
capturedSuccessText = ""
const terminal = Quickshell.env("TERMINAL")
if (!terminal) {
updateInProgress = false
updateFailed = true
lastUpdateError = "TERMINAL environment variable not set"
ToastService.showWarning("ArchUpdater", "TERMINAL environment variable not set")
return
}
// Check if we have an AUR helper for full system update
if (cachedAurHelper !== "" && (aurUpdates > 0 || updates > 0)) {
// Use AUR helper for full system update (handles both repo and AUR)
const command = generateUpdateCommand(cachedAurHelper + " -Syu")
Quickshell.execDetached([terminal, "-e", "bash", "-c", command])
} else if (cachedAurHelper === "") {
// No AUR helper found
updateInProgress = false
updateFailed = true
lastUpdateError = "No AUR helper found (yay or paru not installed)"
Logger.warn("ArchUpdater", "No AUR helper found for update")
}
// Start monitoring and timeout timers
refreshTimer.start()
updateCompleteTimer.start()
updateMonitorTimer.start()
}
// Update selected packages
function runSelectiveUpdate() {
if (selectedPackages.length === 0)
return
// Reset any previous error states
updateFailed = false
lastUpdateError = ""
updateInProgress = true
capturedErrorText = ""
capturedSuccessText = ""
const terminal = Quickshell.env("TERMINAL")
if (!terminal) {
updateInProgress = false
updateFailed = true
lastUpdateError = "TERMINAL environment variable not set"
ToastService.showWarning("ArchUpdater", "TERMINAL environment variable not set")
return
}
// Update all packages with AUR helper (handles both repo and AUR)
if (selectedPackages.length > 0) {
if (cachedAurHelper !== "") {
const packageList = selectedPackages.join(" ")
// Handle ghostty terminal differently due to command parsing issues
if (terminal.includes("ghostty")) {
const simpleCommand = cachedAurHelper + " -S " + packageList
Quickshell.execDetached([terminal, "-e", simpleCommand])
} else {
const command = generateUpdateCommand(cachedAurHelper + " -S " + packageList)
Quickshell.execDetached([terminal, "-e", "bash", "-c", command])
}
} else {
updateInProgress = false
updateFailed = true
lastUpdateError = "No AUR helper found (yay or paru not installed)"
Logger.warn("ArchUpdater", "No AUR helper found for packages:", selectedPackages.join(", "))
}
}
// Start monitoring and timeout timers
refreshTimer.start()
updateCompleteTimer.start()
updateMonitorTimer.start()
}
// Reset update state (useful for manual recovery)
function resetUpdateState() {
// Clear all update states
updateInProgress = false
updateFailed = false
lastUpdateError = ""
checkFailed = false
lastCheckError = ""
updateCompleteTimer.stop()
updateMonitorTimer.stop()
refreshTimer.stop()
errorCheckTimer.stop()
successCheckTimer.stop()
// Refresh to get current state
doPoll()
}
// Manual refresh function (bypasses cooldown)
function forceRefresh() {
// Prevent multiple simultaneous refreshes
if (aurBusy) {
return
}
// Clear error states when refreshing
updateFailed = false
lastUpdateError = ""
checkFailed = false
lastCheckError = ""
// Check if we have a cached AUR helper
if (cachedAurHelper !== "") {
// Clear error state when helper is available
if (checkFailed && lastCheckError.includes("No AUR helper found")) {
checkFailed = false
lastCheckError = ""
}
checkAurUpdatesProcess.command = [cachedAurHelper, "-Qu"]
checkAurOnlyProcess.command = [cachedAurHelper, getAurOnlyFlag()]
// Force refresh by bypassing cooldown
checkAurUpdatesProcess.running = true
lastPollTime = Date.now()
} else {
// AUR helper detection is still in progress or failed
// Try to detect again if not already in progress
if (!yayCheckProcess.running && !paruCheckProcess.running) {
getAurHelper()
}
Logger.warn("ArchUpdater", "AUR helper detection in progress or failed")
}
}
// ============================================================================
// UTILITY PROCESSES
// ============================================================================
// Process for checking yay availability
Process {
id: yayCheckProcess
command: ["which", "yay"]
onExited: function (exitCode) {
if (exitCode === 0) {
cachedAurHelper = "yay"
Logger.log("ArchUpdater", "Found yay AUR helper (preferred)")
// Clear error state when helper is found
if (checkFailed && lastCheckError.includes("No AUR helper found")) {
checkFailed = false
lastCheckError = ""
}
// Trigger initial check when helper is found
triggerInitialCheck()
}
}
}
// Process for checking paru availability
Process {
id: paruCheckProcess
command: ["which", "paru"]
onExited: function (exitCode) {
if (exitCode === 0) {
// Only use paru if yay wasn't found (yay is preferred)
if (cachedAurHelper === "") {
cachedAurHelper = "paru"
Logger.log("ArchUpdater", "Found paru AUR helper")
// Clear error state when helper is found
if (checkFailed && lastCheckError.includes("No AUR helper found")) {
checkFailed = false
lastCheckError = ""
}
// Trigger initial check when helper is found
triggerInitialCheck()
} else {
Logger.log("ArchUpdater", "Found paru but using", cachedAurHelper, "(preferred)")
}
}
}
}
// Cached AUR helper detection
property string cachedAurHelper: ""
// Helper function to detect AUR helper
function getAurHelper() {
// Return cached result if available
if (cachedAurHelper !== "") {
return cachedAurHelper
}
// Check for AUR helpers using Process objects
Logger.log("ArchUpdater", "Detecting AUR helper...")
// Start the detection processes
yayCheckProcess.running = true
paruCheckProcess.running = true
// Return empty string to indicate no helper found yet
// The processes will update cachedAurHelper when they complete
return ""
}
// Helper function to get the correct AUR-only flag for the detected helper
function getAurOnlyFlag() {
if (cachedAurHelper === "yay") {
return "-Qua"
} else if (cachedAurHelper === "paru") {
return "-Qua" // paru uses the same flag but different exit code behavior
}
return "-Qua" // fallback
}
// Helper function to trigger the initial package check
function triggerInitialCheck() {
// Only trigger if this is the first time (no packages have been checked yet)
if (repoPackages.length === 0 && aurPackages.length === 0 && !aurBusy) {
// Clear any previous error state
checkFailed = false
lastCheckError = ""
// Wait a bit for the system to be ready before the first check
Qt.callLater(() => {
checkAurUpdatesProcess.command = [cachedAurHelper, "-Qu"]
checkAurOnlyProcess.command = [cachedAurHelper, getAurOnlyFlag()]
checkAurUpdatesProcess.running = true
lastPollTime = Date.now()
}, 1000)
}
}
// ============================================================================
// PACKAGE SELECTION FUNCTIONS
// ============================================================================
function togglePackageSelection(packageName) {
const index = selectedPackages.indexOf(packageName)
if (index > -1) {
selectedPackages.splice(index, 1)
} else {
selectedPackages.push(packageName)
}
selectedPackagesCount = selectedPackages.length
}
function selectAllPackages() {
selectedPackages = [...repoPackages.map(pkg => pkg.name), ...aurPackages.map(pkg => pkg.name)]
selectedPackagesCount = selectedPackages.length
}
function deselectAllPackages() {
selectedPackages = []
selectedPackagesCount = 0
}
function isPackageSelected(packageName) {
return selectedPackages.indexOf(packageName) > -1
}
// ============================================================================
// REFRESH FUNCTIONS
// ============================================================================
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
// Notification helper
function notify(title, body) {
Quickshell.execDetached(["notify-send", "-a", "UpdateService", "-i", "system-software-update", title, body])
}
// ============================================================================
// AUTO-POLL TIMER
// ============================================================================
// Auto-poll every 15 minutes (respects cooldown)
Timer {
interval: 15 * 60 * 1000 // 15 minutes
repeat: true
running: true
onTriggered: {
if (!updateInProgress && canPoll) {
doPoll()
}
}
}
}