Skip to content

Commit 83995ec

Browse files
committed
fix: harden browser polling and IPC timeout handling (#72)
1 parent 8c7d0ec commit 83995ec

11 files changed

Lines changed: 277 additions & 80 deletions

SimplyTrack/Services/Browsers/ArcBrowser.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ class ArcBrowser: BaseBrowser {
3939
/// Checks if Arc is currently in incognito mode.
4040
/// Uses the 'incognito' property available in Arc's AppleScript interface.
4141
/// Note: Permissions are already verified by getCurrentURL() call, so no need to re-check.
42-
/// - Returns: true if incognito mode is detected, false otherwise
43-
override func isInPrivateBrowsingMode() -> Bool {
42+
/// - Returns: true if incognito mode is detected, false if regular browsing is detected, nil if detection failed
43+
override func isInPrivateBrowsingMode() -> Bool? {
4444
let script = """
4545
tell application "Arc"
4646
if (count of windows) > 0 then
@@ -56,7 +56,7 @@ class ArcBrowser: BaseBrowser {
5656
guard let result = scriptResult.result,
5757
let isIncognito = Bool(result.lowercased())
5858
else {
59-
return false
59+
return nil
6060
}
6161

6262
return isIncognito

SimplyTrack/Services/Browsers/BraveBrowser.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ class BraveBrowser: BaseBrowser {
2929
/// Checks if Brave is currently in incognito mode.
3030
/// Uses the 'mode' property available in Brave's AppleScript interface (same as Chrome).
3131
/// Note: Permissions are already verified by getCurrentURL() call, so no need to re-check.
32-
/// - Returns: true if incognito mode is detected, false otherwise
33-
override func isInPrivateBrowsingMode() -> Bool {
32+
/// - Returns: true if incognito mode is detected, false if regular browsing is detected, nil if detection failed
33+
override func isInPrivateBrowsingMode() -> Bool? {
3434
let script = """
3535
tell application "Brave Browser"
3636
if (count of windows) > 0 then
@@ -44,7 +44,7 @@ class BraveBrowser: BaseBrowser {
4444
guard let result = scriptResult.result,
4545
let isIncognito = Bool(result.lowercased())
4646
else {
47-
return false
47+
return nil
4848
}
4949

5050
return isIncognito

SimplyTrack/Services/Browsers/BrowserInterface.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ protocol BrowserInterface {
2929
func getCurrentURL() -> String?
3030

3131
/// Checks if the current active tab/window is in private browsing mode.
32-
/// - Returns: true if private browsing is detected, false otherwise
33-
func isInPrivateBrowsingMode() -> Bool
32+
/// - Returns: true if private browsing is detected, false if regular browsing is detected, nil if detection failed
33+
func isInPrivateBrowsingMode() -> Bool?
3434
}
3535

3636
/// Base implementation providing common AppleScript execution functionality.
3737
/// Browser-specific classes can inherit from this to get shared AppleScript execution logic.
3838
class BaseBrowser: BrowserInterface {
39+
private static let appleScriptTimeoutSeconds = 3
40+
3941
let bundleId: String
4042
let displayName: String
4143

@@ -60,6 +62,8 @@ class BaseBrowser: BrowserInterface {
6062
// Handle permission-related errors
6163
if scriptResult.errorCode == -1743 || scriptResult.errorCode == -1744 {
6264
PermissionManager.shared.handleBrowserPermissionResult(success: false)
65+
} else if scriptResult.errorCode == -1712 {
66+
logger.debug("Browser (\(self.displayName)) AppleScript timed out")
6367
} else if scriptResult.errorCode == -1719 {
6468
// Invalid index - transient race condition when tabs/windows change during polling.
6569
// Silently ignore; the next polling cycle will succeed.
@@ -85,7 +89,7 @@ class BaseBrowser: BrowserInterface {
8589
}
8690

8791
/// Default implementation - subclasses should override
88-
func isInPrivateBrowsingMode() -> Bool {
92+
func isInPrivateBrowsingMode() -> Bool? {
8993
fatalError("Subclasses must implement isInPrivateBrowsingMode()")
9094
}
9195

@@ -95,7 +99,12 @@ class BaseBrowser: BrowserInterface {
9599
/// - Returns: AppleScriptResult containing the result, error code, and error details
96100
internal func executeAppleScript(_ script: String) -> AppleScriptResult {
97101
var error: NSDictionary?
98-
let appleScript = NSAppleScript(source: script)
102+
let wrappedScript = """
103+
with timeout of \(Self.appleScriptTimeoutSeconds) seconds
104+
\(script)
105+
end timeout
106+
"""
107+
let appleScript = NSAppleScript(source: wrappedScript)
99108
let result = appleScript?.executeAndReturnError(&error)
100109

101110
let errorCode = error?["NSAppleScriptErrorNumber"] as? Int

SimplyTrack/Services/Browsers/ChromeBrowser.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ class ChromeBrowser: BaseBrowser {
3030
/// Checks if Chrome is currently in incognito mode.
3131
/// Uses the reliable 'mode' property available in Chrome's AppleScript interface.
3232
/// Note: Permissions are already verified by getCurrentURL() call, so no need to re-check.
33-
/// - Returns: true if incognito mode is detected, false otherwise
34-
override func isInPrivateBrowsingMode() -> Bool {
33+
/// - Returns: true if incognito mode is detected, false if regular browsing is detected, nil if detection failed
34+
override func isInPrivateBrowsingMode() -> Bool? {
3535
let script = """
3636
tell application "Google Chrome"
3737
if (count of windows) > 0 then
@@ -45,7 +45,7 @@ class ChromeBrowser: BaseBrowser {
4545
guard let result = scriptResult.result,
4646
let isIncognito = Bool(result.lowercased())
4747
else {
48-
return false
48+
return nil
4949
}
5050

5151
return isIncognito

SimplyTrack/Services/Browsers/EdgeBrowser.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ class EdgeBrowser: BaseBrowser {
3030
/// Checks if Edge is currently in InPrivate mode.
3131
/// Uses the 'mode' property similar to Chrome, since Edge is Chromium-based.
3232
/// Note: Permissions are already verified by getCurrentURL() call, so no need to re-check.
33-
/// - Returns: true if InPrivate mode is detected, false otherwise
34-
override func isInPrivateBrowsingMode() -> Bool {
33+
/// - Returns: true if InPrivate mode is detected, false if regular browsing is detected, nil if detection failed
34+
override func isInPrivateBrowsingMode() -> Bool? {
3535
let script = """
3636
tell application "Microsoft Edge"
3737
if (count of windows) > 0 then
@@ -45,7 +45,7 @@ class EdgeBrowser: BaseBrowser {
4545
guard let result = scriptResult.result,
4646
let isInPrivate = Bool(result.lowercased())
4747
else {
48-
return false
48+
return nil
4949
}
5050

5151
return isInPrivate

SimplyTrack/Services/Browsers/FirefoxBrowser.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ class FirefoxBrowser: BaseBrowser {
9191
PermissionManager.shared.handleBrowserError(
9292
"Firefox requires additional setup for website tracking: open about:config in Firefox and set accessibility.force_disabled to -1."
9393
)
94+
} else if scriptResult.errorCode == -1712 {
95+
logger.debug("Firefox Accessibility AppleScript timed out")
9496
} else if scriptResult.errorCode == -1743 || scriptResult.errorCode == -1744 {
9597
PermissionManager.shared.handleSystemEventsPermissionResult(success: false)
9698
} else if scriptResult.errorCode == -25211 {
@@ -128,16 +130,16 @@ class FirefoxBrowser: BaseBrowser {
128130

129131
/// Checks if Firefox is currently in private browsing mode.
130132
/// Firefox appends a localized private browsing indicator to the window title.
131-
/// - Returns: true if private browsing is detected, false otherwise
132-
override func isInPrivateBrowsingMode() -> Bool {
133+
/// - Returns: true if private browsing is detected, false if regular browsing is detected, nil if detection failed
134+
override func isInPrivateBrowsingMode() -> Bool? {
133135
let script = """
134136
tell application "Firefox" to return name of front window
135137
"""
136138

137139
let scriptResult = executeAppleScript(script)
138140

139141
guard let windowName = scriptResult.result else {
140-
return false
142+
return nil
141143
}
142144

143145
return windowName.hasSuffix(Self.privateBrowsingSuffix)

SimplyTrack/Services/Browsers/SafariBrowser.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ class SafariBrowser: BaseBrowser {
3232

3333
/// Checks if Safari is currently in private browsing mode.
3434
/// Uses System Events to check for private window menu item.
35-
/// - Returns: true if private browsing is detected, false otherwise
36-
override func isInPrivateBrowsingMode() -> Bool {
35+
/// - Returns: true if private browsing is detected, false if regular browsing is detected, nil if detection failed
36+
override func isInPrivateBrowsingMode() -> Bool? {
3737
let systemEventsScript = """
3838
tell application "System Events"
3939
tell process "Safari"
@@ -53,7 +53,10 @@ class SafariBrowser: BaseBrowser {
5353
// Invalid index - transient race condition when menu items change during polling.
5454
// Silently ignore; the next polling cycle will succeed.
5555
logger.debug("Safari System Events transient error (invalid index): \(error.description)")
56-
return false
56+
return nil
57+
} else if scriptResult.errorCode == -1712 {
58+
logger.debug("Safari System Events AppleScript timed out")
59+
return nil
5760
} else if scriptResult.errorCode == -1743 || scriptResult.errorCode == -1744 {
5861
// System Events permission errors
5962
PermissionManager.shared.handleSystemEventsPermissionResult(success: false)
@@ -63,7 +66,7 @@ class SafariBrowser: BaseBrowser {
6366
// Log non-permission System Events errors
6467
logger.error("Safari System Events AppleScript error: \(error.description)")
6568
}
66-
return false
69+
return nil
6770
}
6871

6972
// If we successfully executed System Events AppleScript, permissions are working
@@ -76,6 +79,6 @@ class SafariBrowser: BaseBrowser {
7679
return isPrivate
7780
}
7881

79-
return false
82+
return nil
8083
}
8184
}

SimplyTrack/Services/Browsers/VivaldiBrowser.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ class VivaldiBrowser: BaseBrowser {
2929
/// Checks if Vivaldi is currently in private mode.
3030
/// Uses the 'mode' property available in Chromium-based browser AppleScript interfaces.
3131
/// Note: Permissions are already verified by getCurrentURL() call, so no need to re-check.
32-
/// - Returns: true if private mode is detected, false otherwise
33-
override func isInPrivateBrowsingMode() -> Bool {
32+
/// - Returns: true if private mode is detected, false if regular browsing is detected, nil if detection failed
33+
override func isInPrivateBrowsingMode() -> Bool? {
3434
let script = """
3535
tell application id "com.vivaldi.Vivaldi"
3636
if (count of windows) > 0 then
@@ -44,7 +44,7 @@ class VivaldiBrowser: BaseBrowser {
4444
guard let result = scriptResult.result,
4545
let isPrivate = Bool(result.lowercased())
4646
else {
47-
return false
47+
return nil
4848
}
4949

5050
return isPrivate

0 commit comments

Comments
 (0)