Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ UserDefaults.

Please do not open a public issue for security problems.

1. Email **security@osmancagrigenc.dev** with a description, the affected version, steps to
1. Email **info@codifya.com** with a description, the affected version, steps to
reproduce and the impact you expect.
2. We aim to acknowledge reports within 48 hours and to agree on a fix timeline with you.

Expand Down
22 changes: 17 additions & 5 deletions Sources/MacOptimizer/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ public final class AppState: ObservableObject {
// Alert / Notification State
@Published public var activeAlertMessage: String?
@Published public var showAlert = false
@Published public var aiKillConfirmationPID: Int32?
/// PID awaiting the user's explicit confirmation before it is sent SIGTERM. Every UI path that
/// terminates a process except the Memory screen's own force-quit dialog goes through this.
@Published public var pendingTerminationPID: Int32?

public var unresolvedAlertsCount: Int {
autonomousAlerts.filter { !$0.isResolved }.count
Expand Down Expand Up @@ -414,14 +416,24 @@ public final class AppState: ObservableObject {
}
case .killProcess:
if let pid = action.targetPID {
aiKillConfirmationPID = pid
requestProcessTermination(pid: pid)
}
}
}

public func confirmAIProcessTermination() {
guard let pid = aiKillConfirmationPID else { return }
aiKillConfirmationPID = nil
/// Asks the user to confirm terminating `pid`; nothing is terminated until they confirm.
public func requestProcessTermination(pid: Int32) {
pendingTerminationPID = pid
}

public func cancelProcessTermination() {
pendingTerminationPID = nil
}

/// Sends SIGTERM (never SIGKILL) to the process the user just confirmed.
public func confirmProcessTermination() {
guard let pid = pendingTerminationPID else { return }
pendingTerminationPID = nil
killProcess(pid: pid, force: false)
}

Expand Down
3 changes: 2 additions & 1 deletion Sources/MacOptimizer/Core/Operations/CLICommandRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ public struct CLICommandRunner {
status Print current CPU, memory, thermal, swap, and disk telemetry.
clean Scan for junk files (default: --dry-run).
clean --execute --yes Print the plan and move approved items to Trash.
--include-trash Include Trash items in the plan (requires extra confirmation).
--include-trash Also list Trash items; with --execute --yes, permanently delete them
(no confirmation beyond --yes).
version Print version and license information.
help Show this help menu.

Expand Down
9 changes: 0 additions & 9 deletions Sources/MacOptimizer/Views/AI/AICopilotView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,6 @@ public struct AICopilotView: View {
.padding(20)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(NSColor.windowBackgroundColor).opacity(0.5))
.alert(L10n.string("Terminate this process?", table: .ai), isPresented: Binding(
get: { appState.aiKillConfirmationPID != nil },
set: { if !$0 { appState.aiKillConfirmationPID = nil } }
)) {
Button("Cancel", role: .cancel) { appState.aiKillConfirmationPID = nil }
Button("Terminate Normally", role: .destructive) { appState.confirmAIProcessTermination() }
} message: {
Text(L10n.string("PID: %lld. SIGTERM will be sent first.", table: .ai, Int64(appState.aiKillConfirmationPID ?? 0)))
}
.onAppear {
if appState.aiInsights.isEmpty {
appState.runAIHealthAnalysis()
Expand Down
2 changes: 1 addition & 1 deletion Sources/MacOptimizer/Views/Dashboard/DashboardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ public struct DashboardView: View {
LiveTopProcessesCard(
metrics: appState.metrics,
onKill: { pid in
appState.killProcess(pid: pid)
appState.requestProcessTermination(pid: pid)
},
onNavigateToMemory: {
appState.selectedTab = .memory
Expand Down
14 changes: 14 additions & 0 deletions Sources/MacOptimizer/Views/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ public struct MainView: View {
dismissButton: .default(Text(l10n: "Done"))
)
}
// Shared confirmation for the dashboard, AI Copilot and Autonomous Guard terminate buttons.
.alert(L10n.string("Terminate this process?", table: .ai), isPresented: Binding(
get: { appState.pendingTerminationPID != nil },
set: { if !$0 { appState.cancelProcessTermination() } }
)) {
Button(role: .cancel) { appState.cancelProcessTermination() } label: {
Text(l10n: "Cancel", table: .ai)
}
Button(role: .destructive) { appState.confirmProcessTermination() } label: {
Text(l10n: "Terminate Normally", table: .ai)
}
} message: {
Text(L10n.string("PID: %lld. SIGTERM will be sent first.", table: .ai, Int64(appState.pendingTerminationPID ?? 0)))
}
}

// MARK: - Sidebar
Expand Down
19 changes: 19 additions & 0 deletions Tests/MacOptimizerTests/MacOptimizerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,25 @@ final class MacOptimizerTests: XCTestCase {
XCTAssertFalse(CLICommandRunner.helpText.contains { "çğıİöşüÇĞÖŞÜ".contains($0) })
}

func testCLIHelpDoesNotPromiseExtraTrashConfirmation() {
XCTAssertFalse(CLICommandRunner.helpText.contains("extra confirmation"))
XCTAssertTrue(CLICommandRunner.helpText.contains("no confirmation beyond --yes"))
}

@MainActor
func testProcessTerminationWaitsForExplicitConfirmation() {
let state = AppState(startMonitoring: false)
state.requestProcessTermination(pid: 4242)
XCTAssertEqual(state.pendingTerminationPID, 4242)
state.cancelProcessTermination()
XCTAssertNil(state.pendingTerminationPID)

state.executeAIAction(AIAction(title: "Quit", type: .killProcess, targetPID: 4343))
XCTAssertEqual(state.pendingTerminationPID, 4343, "AI/watchdog kill actions must only request confirmation")
state.cancelProcessTermination()
XCTAssertNil(state.pendingTerminationPID)
}

func testBatteryHealthUsesAppleSiliconCapacityAndIntelCapacityShapes() {
let appleSilicon = ["AppleRawMaxCapacity": 4_600, "NominalChargeCapacity": 4_550, "DesignCapacity": 5_000]
let intel = ["MaxCapacity": 4_000, "DesignCapacity": 5_000]
Expand Down
Loading