From ba559cd66c16508de27a6f9c34e4a1adb2915a6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 06:47:34 +0000 Subject: [PATCH 1/5] Initial plan From 3e2c6c750be0b49d296e210604ec76f6ec25768f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 06:49:47 +0000 Subject: [PATCH 2/5] Add System Launch event with boot detection Co-authored-by: liopoos <12404909+liopoos@users.noreply.github.com> --- HemuLock/AppDelegate.swift | 24 ++++++++ HemuLock/Base.lproj/Localizable.strings | 2 + HemuLock/Enums/Event.swift | 5 ++ HemuLock/Managers/SystemBootManager.swift | 71 ++++++++++++++++++++++ HemuLock/en.lproj/Localizable.strings | 2 + HemuLock/zh-Hans.lproj/Localizable.strings | 2 + 6 files changed, 106 insertions(+) create mode 100644 HemuLock/Managers/SystemBootManager.swift diff --git a/HemuLock/AppDelegate.swift b/HemuLock/AppDelegate.swift index 20b264f..9333db3 100644 --- a/HemuLock/AppDelegate.swift +++ b/HemuLock/AppDelegate.swift @@ -371,6 +371,30 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele } observer = EventObserver() + + // Check for system launch event - only fire once per system boot + if SystemBootManager.shared.isNewSystemBoot() { + // Only handle if System Launch event is enabled in config + if appState.appConfig.activeEvents.contains(Event.systemLaunch.tag) { + // Handle recording + if appState.appConfig.isRecordEvent { + observer?.recordEvent(event: .systemLaunch) + } + + // Send notification + if appState.appConfig.notifyType != Notify.none.tag { + observer?.sendNotify(event: .systemLaunch) + } + + // Execute script + if appState.appConfig.isExecScript { + observer?.runScript(Event.systemLaunch.name) + } + } + + // Mark this boot as notified to prevent duplicate notifications + SystemBootManager.shared.markBootNotified() + } } /** diff --git a/HemuLock/Base.lproj/Localizable.strings b/HemuLock/Base.lproj/Localizable.strings index 3e1ac4a..ae8dfeb 100644 --- a/HemuLock/Base.lproj/Localizable.strings +++ b/HemuLock/Base.lproj/Localizable.strings @@ -34,6 +34,7 @@ "SYSTEM_WAKE" = "System Wake"; "SYSTEM_LOCK" = "System Lock"; "SYSTEM_UNLOCK" = "System Unlock"; +"SYSTEM_LAUNCH" = "System Started"; "EVENT_SCREEN_SLEEP" = "Screen Sleep"; "EVENT_SCREEN_WAKE" = "Screen Wake"; @@ -41,6 +42,7 @@ "EVENT_SYSTEM_WAKE" = "System Wake"; "EVENT_SYSTEM_LOCK" = "System Lock"; "EVENT_SYSTEM_UNLOCK" = "System Unlock"; +"EVENT_SYSTEM_LAUNCH" = "System Launch"; "EMPTY_PUSHOVER" = "Pushover Configuration Parameters Error"; "EMPTY_BARK" = "Bark Configuration Parameters Error"; diff --git a/HemuLock/Enums/Event.swift b/HemuLock/Enums/Event.swift index af4d3d8..7d6268c 100644 --- a/HemuLock/Enums/Event.swift +++ b/HemuLock/Enums/Event.swift @@ -15,6 +15,7 @@ enum Event: String, CaseIterable { case systemWake = "SYSTEM_WAKE" case systemLock = "SYSTEM_LOCK" case systemUnLock = "SYSTEM_UNLOCK" + case systemLaunch = "SYSTEM_LAUNCH" var name: String { return rawValue @@ -34,6 +35,8 @@ enum Event: String, CaseIterable { return 130 case .systemUnLock: return 131 + case .systemLaunch: + return 140 } } @@ -51,6 +54,8 @@ enum Event: String, CaseIterable { return NSNotification.Name(rawValue: "com.apple.screenIsLocked") case .systemUnLock: return NSNotification.Name(rawValue: "com.apple.screenIsUnlocked") + case .systemLaunch: + return NSNotification.Name(rawValue: "com.hemulock.systemLaunch") } } } diff --git a/HemuLock/Managers/SystemBootManager.swift b/HemuLock/Managers/SystemBootManager.swift new file mode 100644 index 0000000..39d795a --- /dev/null +++ b/HemuLock/Managers/SystemBootManager.swift @@ -0,0 +1,71 @@ +// +// SystemBootManager.swift +// HemuLock +// +// Created by HemuLock on 2024/02/09. +// + +import Foundation + +/** + SystemBootManager handles detection of system boot events. + + This manager tracks system boot time and determines if a notification + for the current boot has already been sent. It uses sysctl to get the + system boot time and UserDefaults to persist the last notified boot time. + */ +class SystemBootManager { + static let shared = SystemBootManager() + + private let lastBootTimeKey = "last_notified_boot_time" + + private init() {} + + /** + Get the current system boot time using sysctl. + + - Returns: The date when the system was last booted + */ + func getSystemBootTime() -> Date { + var mib = [CTL_KERN, KERN_BOOTTIME] + var bootTime = timeval() + var size = MemoryLayout.stride + + if sysctl(&mib, UInt32(mib.count), &bootTime, &size, nil, 0) != -1 { + return Date(timeIntervalSince1970: TimeInterval(bootTime.tv_sec)) + } + return Date() + } + + /** + Check if this is a new system boot (not just app relaunch). + + Compares the current system boot time with the last boot time that was notified. + + - Returns: true if this is a new boot that hasn't been notified yet, false otherwise + */ + func isNewSystemBoot() -> Bool { + let currentBootTime = getSystemBootTime() + + // Get the last notified boot time from UserDefaults + if let lastBootTime = UserDefaults.standard.object(forKey: lastBootTimeKey) as? Date { + // If the boot times are different, it's a new boot + // Allow a small tolerance for timing differences + return abs(currentBootTime.timeIntervalSince(lastBootTime)) > 1.0 + } + + // If no previous boot time is stored, consider it a new boot + return true + } + + /** + Mark the current boot as notified. + + Stores the current system boot time in UserDefaults to prevent + duplicate notifications for the same boot. + */ + func markBootNotified() { + let currentBootTime = getSystemBootTime() + UserDefaults.standard.set(currentBootTime, forKey: lastBootTimeKey) + } +} diff --git a/HemuLock/en.lproj/Localizable.strings b/HemuLock/en.lproj/Localizable.strings index ceb9931..ed07b6f 100644 --- a/HemuLock/en.lproj/Localizable.strings +++ b/HemuLock/en.lproj/Localizable.strings @@ -34,6 +34,7 @@ "SYSTEM_WAKE" = "System Wake"; "SYSTEM_LOCK" = "System Lock"; "SYSTEM_UNLOCK" = "System Unlock"; +"SYSTEM_LAUNCH" = "System Started"; "EVENT_SCREEN_SLEEP" = "Screen Sleep"; "EVENT_SCREEN_WAKE" = "Screen Wake"; @@ -41,6 +42,7 @@ "EVENT_SYSTEM_WAKE" = "System Wake"; "EVENT_SYSTEM_LOCK" = "System Lock"; "EVENT_SYSTEM_UNLOCK" = "System Unlock"; +"EVENT_SYSTEM_LAUNCH" = "System Launch"; "EMPTY_PUSHOVER" = "Pushover Configuration Parameters Error"; "EMPTY_BARK" = "Bark Configuration Parameters Error"; diff --git a/HemuLock/zh-Hans.lproj/Localizable.strings b/HemuLock/zh-Hans.lproj/Localizable.strings index 8be8c16..98e6742 100644 --- a/HemuLock/zh-Hans.lproj/Localizable.strings +++ b/HemuLock/zh-Hans.lproj/Localizable.strings @@ -37,6 +37,7 @@ "SYSTEM_WAKE" = "系统被唤醒"; "SYSTEM_LOCK" = "系统已被锁定"; "SYSTEM_UNLOCK" = "系统已解锁"; +"SYSTEM_LAUNCH" = "系统已启动"; "EVENT_SCREEN_SLEEP" = "屏幕睡眠"; "EVENT_SCREEN_WAKE" = "屏幕唤醒"; @@ -44,6 +45,7 @@ "EVENT_SYSTEM_WAKE" = "系统唤醒"; "EVENT_SYSTEM_LOCK" = "锁定"; "EVENT_SYSTEM_UNLOCK" = "解锁"; +"EVENT_SYSTEM_LAUNCH" = "系统启动"; "EMPTY_PUSHOVER" = "Pushover配置参数错误"; "EMPTY_BARK" = "Bark配置参数错误"; From cb3d7f0f88667c3b0d3dd9107d94edcc5ad77dfd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 06:50:49 +0000 Subject: [PATCH 3/5] Fix boot notification marking order to prevent repeated checks Co-authored-by: liopoos <12404909+liopoos@users.noreply.github.com> --- HemuLock/AppDelegate.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HemuLock/AppDelegate.swift b/HemuLock/AppDelegate.swift index 9333db3..12d07f8 100644 --- a/HemuLock/AppDelegate.swift +++ b/HemuLock/AppDelegate.swift @@ -374,6 +374,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele // Check for system launch event - only fire once per system boot if SystemBootManager.shared.isNewSystemBoot() { + // Mark this boot as notified first to prevent duplicate checks + SystemBootManager.shared.markBootNotified() + // Only handle if System Launch event is enabled in config if appState.appConfig.activeEvents.contains(Event.systemLaunch.tag) { // Handle recording @@ -391,9 +394,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele observer?.runScript(Event.systemLaunch.name) } } - - // Mark this boot as notified to prevent duplicate notifications - SystemBootManager.shared.markBootNotified() } } From 29347cc4ad69768eb537b4323b92cfb71cae83a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 06:51:47 +0000 Subject: [PATCH 4/5] Address code review feedback: improve error handling and add clarifying comments Co-authored-by: liopoos <12404909+liopoos@users.noreply.github.com> --- HemuLock/AppDelegate.swift | 4 +++- HemuLock/Managers/SystemBootManager.swift | 15 ++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/HemuLock/AppDelegate.swift b/HemuLock/AppDelegate.swift index 12d07f8..b930866 100644 --- a/HemuLock/AppDelegate.swift +++ b/HemuLock/AppDelegate.swift @@ -374,7 +374,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele // Check for system launch event - only fire once per system boot if SystemBootManager.shared.isNewSystemBoot() { - // Mark this boot as notified first to prevent duplicate checks + // Mark this boot as notified first to prevent duplicate notifications. + // This ensures the event fires only once per boot, even if the user + // enables/disables the event or relaunches the app during the same boot session. SystemBootManager.shared.markBootNotified() // Only handle if System Launch event is enabled in config diff --git a/HemuLock/Managers/SystemBootManager.swift b/HemuLock/Managers/SystemBootManager.swift index 39d795a..c4a23cc 100644 --- a/HemuLock/Managers/SystemBootManager.swift +++ b/HemuLock/Managers/SystemBootManager.swift @@ -24,9 +24,9 @@ class SystemBootManager { /** Get the current system boot time using sysctl. - - Returns: The date when the system was last booted + - Returns: The date when the system was last booted, or nil if unable to determine */ - func getSystemBootTime() -> Date { + func getSystemBootTime() -> Date? { var mib = [CTL_KERN, KERN_BOOTTIME] var bootTime = timeval() var size = MemoryLayout.stride @@ -34,7 +34,7 @@ class SystemBootManager { if sysctl(&mib, UInt32(mib.count), &bootTime, &size, nil, 0) != -1 { return Date(timeIntervalSince1970: TimeInterval(bootTime.tv_sec)) } - return Date() + return nil } /** @@ -45,7 +45,10 @@ class SystemBootManager { - Returns: true if this is a new boot that hasn't been notified yet, false otherwise */ func isNewSystemBoot() -> Bool { - let currentBootTime = getSystemBootTime() + guard let currentBootTime = getSystemBootTime() else { + // If we can't determine boot time, don't trigger the event + return false + } // Get the last notified boot time from UserDefaults if let lastBootTime = UserDefaults.standard.object(forKey: lastBootTimeKey) as? Date { @@ -65,7 +68,9 @@ class SystemBootManager { duplicate notifications for the same boot. */ func markBootNotified() { - let currentBootTime = getSystemBootTime() + guard let currentBootTime = getSystemBootTime() else { + return + } UserDefaults.standard.set(currentBootTime, forKey: lastBootTimeKey) } } From 73128edc77bfc947021f10a1451bb00ea7af2c71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 06:53:04 +0000 Subject: [PATCH 5/5] Extract magic number to named constant with documentation Co-authored-by: liopoos <12404909+liopoos@users.noreply.github.com> --- HemuLock/Managers/SystemBootManager.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/HemuLock/Managers/SystemBootManager.swift b/HemuLock/Managers/SystemBootManager.swift index c4a23cc..7e4269f 100644 --- a/HemuLock/Managers/SystemBootManager.swift +++ b/HemuLock/Managers/SystemBootManager.swift @@ -19,6 +19,11 @@ class SystemBootManager { private let lastBootTimeKey = "last_notified_boot_time" + // Tolerance in seconds for comparing boot times. + // System boot times may have minor differences due to precision, + // so we use a 1-second tolerance to avoid false positives. + private let bootTimeToleranceSeconds: TimeInterval = 1.0 + private init() {} /** @@ -54,7 +59,7 @@ class SystemBootManager { if let lastBootTime = UserDefaults.standard.object(forKey: lastBootTimeKey) as? Date { // If the boot times are different, it's a new boot // Allow a small tolerance for timing differences - return abs(currentBootTime.timeIntervalSince(lastBootTime)) > 1.0 + return abs(currentBootTime.timeIntervalSince(lastBootTime)) > bootTimeToleranceSeconds } // If no previous boot time is stored, consider it a new boot