Skip to content
Open
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
22 changes: 18 additions & 4 deletions SwiftBar/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,30 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUStandardUserDriverDelegat
var softwareUpdater: SPUUpdater!
#endif

/// True when SwiftBar is hosting an XCTest / Swift Testing bundle.
/// Set by the test runner via the `XCTestConfigurationFilePath` env var.
static var isRunningTests: Bool {
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil ||
NSClassFromString("XCTestCase") != nil
}

func applicationDidFinishLaunching(_: Notification) {
// Wire up the plugin manager so tests that reference
// `delegate.pluginManager` have a usable instance, then bail out before
// touching anything that keeps the runloop alive (Sparkle, the
// missing-folder modal, file-system observers). Without this the test
// host never reaches application termination and the test process hangs
// indefinitely after the last @Test finishes.
pluginManager = PluginManager.shared
if Self.isRunningTests { return }

preferencesWindowController.window?.delegate = self
setupToolbar()

// Clean up any corrupted NSStatusItem visibility states from UserDefaults
// This fixes issues where menubar items disappear after initial setup
cleanupStatusItemVisibility()

let hostBundle = Bundle.main
#if !MAC_APP_STORE
let updateDriver = SPUStandardUserDriver(hostBundle: hostBundle, delegate: self)
Expand All @@ -109,8 +125,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUStandardUserDriverDelegat
prefs.pluginDirectoryPath = nil
}

// Instance of Plugin Manager must be created after app launch
pluginManager = PluginManager.shared
pluginManager.loadPlugins()
pluginManager.persistLatestSystemReport(reason: "application-did-finish-launching")

Expand Down
6 changes: 5 additions & 1 deletion SwiftBar/Plugin/PackagedPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,14 @@ class PackagedPlugin: TimerArmingPlugin {
return nil
}

// contentsOfDirectory(at:) does not follow a directory-typed URL that is
// a symlink (it returns ENOTDIR), so resolve the link before enumerating.
// The `.swiftbar` extension check above already used the unresolved URL.
let fileManager = FileManager.default
let enumerationURL = directory.resolvingSymlinksInPath()
let contents: [URL]
do {
contents = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil)
contents = try fileManager.contentsOfDirectory(at: enumerationURL, includingPropertiesForKeys: nil)
} catch {
os_log("Failed to read packaged plugin directory %{public}@: %{public}@",
log: Log.plugin, type: .error, directory.path, error.localizedDescription)
Expand Down
6 changes: 5 additions & 1 deletion SwiftBar/Plugin/PluginManger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ func pluginSyncPath(for plugin: Plugin) -> String {
}

private func packagedPluginFileState(for packageURL: URL, fileManager: FileManager = .default) -> PluginFileState? {
// `findMainExecutable` requires the `.swiftbar` extension, which only the
// unresolved URL is guaranteed to have when the package is reached through
// a symlink. The enumerator still needs the resolved path so we can read
// file attributes off the real directory.
let resolvedPackageURL = packageURL.resolvingSymlinksInPath()
guard PackagedPlugin.findMainExecutable(in: resolvedPackageURL) != nil,
guard PackagedPlugin.findMainExecutable(in: packageURL) != nil,
let enumerator = fileManager.enumerator(at: resolvedPackageURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
else {
return nil
Expand Down