Processes & Concurrency

RSS for tag

Discover how the operating system manages multiple applications and processes simultaneously, ensuring smooth multitasking performance.

Concurrency Documentation

Posts under Processes & Concurrency subtopic

Post

Replies

Boosts

Views

Activity

WatchConnectivity Swift 6 - Incorrect actor executor assumption
I am trying to migrate a WatchConnectivity App to Swift6 and I found an Issue with my replyHandler callback for sendMessageData. I am wrapping sendMessageData in withCheckedThrowingContinuation, so that I can await the response of the reply. I then update a Main Actor ObservableObject that keeps track of the count of connections that have not replied yet, before returning the data using continuation.resume. ... @preconcurrency import WatchConnectivity actor ConnectivityManager: NSObject, WCSessionDelegate { private var session: WCSession = .default private let connectivityMetaInfoManager: ConnectivityMetaInfoManager ... private func sendMessageData(_ data: Data) async throws -> Data? { Logger.shared.debug("called on Thread \(Thread.current)") await connectivityMetaInfoManager.increaseOpenSendConnectionsCount() return try await withCheckedThrowingContinuation({ continuation in self.session.sendMessageData( data, replyHandler: { data in Task { await self.connectivityMetaInfoManager .decreaseOpenSendConnectionsCount() } continuation.resume(returning: data) }, errorHandler: { (error) in Task { await self.connectivityMetaInfoManager .decreaseOpenSendConnectionsCount() } continuation.resume(throwing: error) } ) }) } Calling sendMessageData somehow causing the app to crash and display the debug message: Incorrect actor executor assumption. The code runs on swift 5 with SWIFT_STRICT_CONCURRENCY = complete. However when I switch to swift 6 the code crashes. I rebuilt a simple version of the App. Adding bit by bit until I was able to cause the crash. See Broken App Awaiting sendMessageData and wrapping it in a task and adding the @Sendable attribute to continuation, solve the crash. See Fixed App But I do not understand why yet. Is this intended behaviour? Should the compiler warn you about this? Is it a WatchConnectivity issue? I initially posted on forums.swift.org, but was told to repost here.
3
0
1.2k
Jan ’25
SMAppService - How does this work?
I'm a developer using Lazarus Pascal, so converting ObjC and Swift comes with its challenges. I'm trying to figure how to properly use SMAppService to add my application as a login item for the App Store. I have learned that the old method (< macOS 13) uses a helper tool, included in the app bundle, which calls the now deprecated SMLoginItemSetEnabled. Now this is already quite a pain to deal with if you're not using XCode, not to mention converting the headers being rather complicated when you're not experienced with doing this. The "new" method (as of macOS 13) is using SMAppService. Can anyone explain how to use this? The documentation (for me anyway) is a not very clear about that and neither are examples that can be found all over the Internet. My main question is: Can I now use the SMAppService functions to add/remove a login item straight in my application, or is a helper tool still required?
3
0
161
Mar ’25
Reliable 30-minute background data fetching for safety-critical monitoring app?
I'm developing a safety-critical monitoring app that needs to fetch data from government APIs every 30 minutes and trigger emergency audio alerts for threshold violations. The app must work reliably in background since users depend on it for safety alerts even while sleeping. Main Challenge: iOS background limitations seem to prevent consistent 30-minute intervals. Standard BGTaskScheduler and timers get suspended after a few minutes in background. Question: What's the most reliable approach to ensure consistent 30-minute background monitoring for a safety-critical app where missed alerts could have serious consequences? Are there special entitlements or frameworks for emergency/safety applications? The app needs to function like an alarm clock - working reliably even when backgrounded with emergency audio override capabilities.
1
0
548
Jul ’25
Spawning electron app from deamon process
Hi all, Our company has an application that runs on several machines, this app is launched via a deamon that keeps it alive. One of the feature of this app, is to start a headless electron application to run some tests. When spawning this electron application with the new arm64 OS, we are getting this issue: Silent Test Agent Worker exited with code: 133 [ERROR] [75873:0205/135842.347044:ERROR:mach_port_rendezvous.cc(384)] bootstrap_look_up com.hivestreaming.silenttestagent.MachPortRendezvousServer.1: Permission denied (1100) [ERROR] [75873:0205/135842.347417:ERROR:shared_memory_switch.cc(237)] No rendezvous client, terminating process (parent died?) [ERROR] [75872:0205/135842.347634:ERROR:mach_port_rendezvous.cc(384)] bootstrap_look_up com.hivestreaming.silenttestagent.MachPortRendezvousServer.1: Permission denied (1100) [ERROR] [75872:0205/135842.347976:ERROR:shared_memory_switch.cc(237)] No rendezvous client, terminating process (parent died?) Both application (main app and electron one) are signed and notarized, but it seems that there is some other permission issue. If we run the electron application manually, all runs as expected. I added the crash report as attachment CrashReport.log
2
0
528
Feb ’25
DispatchSourceTimer Not Firing in Local Push Connectivity Extension When App Is in Foreground and Device Is Locked
Hi, I’m using a Local Push Connectivity Extension and encountering an issue with DispatchSourceTimer. In my extension, I create a DispatchSourceTimer that is supposed to fire every 1 second. It works as expected at first. However, when the app is in the foreground and the device is locked, the timer eventually stops firing after 1–3 hours. The extension process is still alive, and no errors are thrown Has anyone experienced this behavior? Is this a known limitation for timers inside NEAppPushProvider, or is the extension being deprioritized silently by the system? Any insights or suggestions would be greatly appreciated. Thanks!
2
0
99
Apr ’25
BGContinuedProcessingTask compatibility with background URLSession
My app does really large uploads. Like several GB. We use the AWS SDK to upload to S3. It seemed like using BGContinuedProcessingTask to complete a set of uploads for a particular item may improve UX as well as performance and reliability. When I tried to get BGContinuedProcessingTask working with the AWS SDK I found that the task would fail after maybe 30 seconds. It looked like this was because the app stopped receiving updates from the AWS upload and the task wants consistent updates. The AWS SDK always uses a background URLSession and this is not configurable. I understand the background URLSession runs in a separate process from the app and maybe that is why progress updates did not continue when the app was in the background. Is it expected that BGContinuedProcessingTask and background URLSession are not really compatible? It would not be shocking since they are 2 separate background APIs. Would the Apple recommendation be to use a normal URLSession for this, in which case AWS would need to change their SDK? Or does Apple think that BGContinuedProcessingTask should just not be used with uploads? In other words use an upload specific API. Thanks!
2
0
153
Aug ’25
How can I get a Subscriber to subscribe to get only 4 elements from an array?
Hello, I am trying to implement a subscriber which specifies its own demand for how many elements it wants to receive from a publisher. My code is below: import Combine var array = [1, 2, 3, 4, 5, 6, 7] struct ArraySubscriber<T>: Subscriber { typealias Input = T typealias Failure = Never let combineIdentifier = CombineIdentifier() func receive(subscription: any Subscription) { subscription.request(.max(4)) } func receive(_ input: T) -> Subscribers.Demand { print("input,", input) return .max(4) } func receive(completion: Subscribers.Completion<Never>) { switch completion { case .finished: print("publisher finished normally") case .failure(let failure): print("publisher failed due to, ", failure) } } } let subscriber = ArraySubscriber<Int>() array.publisher.subscribe(subscriber) According to Apple's documentation, I specify the demand inside the receive(subscription: any Subscription) method, see link. But when I run this code I get the following output: input, 1 input, 2 input, 3 input, 4 input, 5 input, 6 input, 7 publisher finished normally Instead, I expect the subscriber to only "receive" elements 1, 2, 3, 4 from the array. How can I accomplish this?
0
0
127
Aug ’25
Why is xpc_connection_set_peer_code_signing_requirement() closing the connection instead of returning XPC_ERROR_PEER_CODE_SIGNING_REQUIREMENT?
I'm using libxpc in a C server and Swift client. I set up a code-signing requirement in the server using xpc_connection_set_peer_code_signing_requirement(). However, when the client doesn't meet the requirement, the server just closes the connection, and I get XPC_ERROR_CONNECTION_INTERRUPTED on the client side instead of XPC_ERROR_PEER_CODE_SIGNING_REQUIREMENT, making debugging harder. What I want: To receive XPC_ERROR_PEER_CODE_SIGNING_REQUIREMENT on the client when code-signing fails, for better debugging. What I’ve tried: Using xpc_connection_set_peer_code_signing_requirement(), but it causes the connection to be dropped immediately. Questions: Why does the server close the connection without sending the expected error? How can I receive the correct error on the client side? Are there any other methods for debugging code-signing failures with libxpc? Thanks for any insights!
1
0
482
Feb ’25
Electron app with Express + Python child processes not running in macOS production build
Hi all, I’ve built an Electron application that uses two child processes: An Express.js server A Python executable (packaged .exe/binary) During the development phase, everything works fine — the Electron app launches, both child processes start, and the app functions as expected. But when I create a production build for macOS, the child processes don’t run. Here’s a simplified snippet from my electron.mjs: import { app, BrowserWindow } from "electron"; import { spawn } from "child_process"; import path from "path"; let mainWindow; const createWindow = () =&gt; { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: true, }, }); mainWindow.loadFile("index.html"); // Start Express server const serverPath = path.join(process.resourcesPath, "app.asar.unpacked", "server", "index.js"); const serverProcess = spawn(process.execPath, [serverPath], { stdio: "inherit", }); // Start Python process const pythonPath = path.join(process.resourcesPath, "app.asar.unpacked", "python", "myapp"); const pythonProcess = spawn(pythonPath, [], { stdio: "inherit", }); serverProcess.on("error", (err) =&gt; console.error("Server process error:", err)); pythonProcess.on("error", (err) =&gt; console.error("Python process error:", err)); }; app.whenReady().then(createWindow); I’ve already done the following: Configured package.json with the right build settings Set up extraResources / asarUnpack to include the server and Python files Verified both child processes work standalone Questions: What’s the correct way to package and spawn these child processes for macOS production builds? Do I need to move them into a specific location (like Contents/Resources/app.asar.unpacked) and reference them differently? Is there a more reliable pattern for handling Express + Python child processes inside an Electron app bundle? Any insights or working examples would be really appreciated!
2
0
85
Sep ’25
Mac: Best way to distinguish native app process and script process spawned from executable (e.g. python node) through process_id
I'm working on a Mac app that receives a process ID via NSXPCConnection, and I'm trying to figure out the best way to determine whether that process is a native macOS app like Safari—with bundles and all—or just a script launched by something like Node or Python. The executable is signed with a Team ID using codesign. I was thinking about getting the executable's path as one way to handle it, but I’m wondering if there’s a more reliable method than relying on the folder structure.
1
0
209
Sep ’25
Background refresh or processing app
I am writing an app which mainly is used to update data used by other apps on the device. After the user initializes some values in the app, they almost never have to return to it (occasionally to add a "friend"). The app needs to run a background task at least daily, however, without the user's intervention (or even awareness, once they've given permission). My understanding of background refresh tasks is that if the user doesn't activate the app in the foreground periodically, the scheduled background tasks may never run. If this is true, do I want to use a background processing task instead, or is there a better solution (or have I misunderstood entirely)?
1
0
412
Jan ’25
Combine delay & switchToLatest publisher don't emit value sometimes
Hello, I recently implemented a conditional debounce publisher using Swift's Combine. If a string with a length less than 2 is passed, the event is sent downstream immediately without delay. If a string with a length of 2 or more is passed, the event is emitted downstream with a 0.2-second delay. While writing test logic related to this, I noticed a strange phenomenon: sometimes the publisher, which should emit events with a 0.2-second delay, does not emit an event. The test code below should have all indices from 1 to 100 in the array, but sometimes some indices are missing, causing the assertion to fail. Even after observing completion, cancel, and output events through handleEvents, I couldn't find any cause. Am I using Combine incorrectly, or is there a bug in Combine? I would appreciate it if you could let me know. import Foundation import Combine var cancellables: Set<AnyCancellable> = [] @MainActor func text(index: Int, completion: @escaping () -> Void) { let subject = PassthroughSubject<String, Never>() let textToSent = "textToSent" subject .map { text in if text.count >= 2 { return Just<String>(text) .delay(for: .seconds(0.2), scheduler: RunLoop.main) .eraseToAnyPublisher() } else { return Just<String>(text) .eraseToAnyPublisher() } } .switchToLatest() .sink { if $0.count >= 2 { completion() } }.store(in: &cancellables) for i in 0..<textToSent.count { let stringIndex = textToSent.index(textToSent.startIndex, offsetBy: i) let stringToSent = String(textToSent[textToSent.startIndex...stringIndex]) subject.send(stringToSent) } } var array = [Int]() for i in 1...100 { text(index: i) { array.append(i) } } DispatchQueue.main.asyncAfter(deadline: .now() + 5) { for i in 1...100 { assert(array.contains(i)) } } RunLoop.main.run(until: .now + 10)
0
0
403
Feb ’25
Help me implement SMAppServices
I have followed these steps as mentioned in this link :(https://developer.apple.com/forums/thread/721737) My projects app bundle structure is like this : TWGUI.app TWGUI.app/Contents TWGUI.app/Contents/_CodeSignature TWGUI.app/Contents/_CodeSignature/CodeResources TWGUI.app/Contents/MacOS TWGUI.app/Contents/MacOS/TWAgent TWGUI.app/Contents/MacOS/TWGUI TWGUI.app/Contents/Resources TWGUI.app/Contents/Library TWGUI.app/Contents/Library/LaunchAgents TWGUI.app/Contents/Library/LaunchAgents/com.example.TWGUI.agent.plist TWGUI.app/Contents/Info.plist TWGUI.app/Contents/PkgInfo TWGUI is my main GUI App , i which i want to embed TWAgent (a command line tool target) and register it using SMAppServices so that launchd can launch it. In TWGUI, code for registering to launchd using SMAppServices is structure as follow : import SwiftUI import ServiceManagement struct ContentView: View { let agent = SMAppService.agent(plistName: "com.example.TWGUI.agent.plist") var body: some View { VStack { Button("Register Agent") { RegisterAgent () } .padding() Button("Unregister Agent") { UnregisterAgent () } .padding() } } func RegisterAgent() { DispatchQueue.global(qos: .background).async { do { print("Registering Agent. Status: \(agent.status.rawValue)") try agent.register() print("Agent registered") } catch { print("Failed to register agent: \(error)") } } } func UnregisterAgent() { DispatchQueue.global(qos: .background).async { do { print("Unregistering Agent. Status: \(agent.status.rawValue)") try agent.unregister() print("Agent unregistered") } catch { print("Failed to unregister agent: \(error)") } } } } com.example.TWGUI.agent.plist : &lt;?xml version="1.0" encoding="UTF-8"?&gt; &lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs$ &lt;plist version="1.0"&gt; &lt;dict&gt; &lt;key&gt;Label&lt;/key&gt; &lt;string&gt;com.example.TWGUI.agent&lt;/string&gt; &lt;key&gt;ProgramArguments&lt;/key&gt; &lt;array&gt; &lt;string&gt;Contents/MacOS/TWAgent&lt;/string&gt; &lt;/array&gt; &lt;key&gt;RunAtLoad&lt;/key&gt; &lt;true/&gt; &lt;key&gt;KeepAlive&lt;/key&gt; &lt;true/&gt; &lt;/dict&gt; &lt;/plist&gt; I have used ProgramArguements instead of using Program in above plist because i was getting this error when i was using Program earlier : Registering Agent. Status: 3 Failed to register agent: Error Domain=SMAppServiceErrorDomain Code=111 "Invalid or missing Program/ProgramArguments" UserInfo={NSLocalizedFailureReason=Invalid or missing Program/ProgramArguments} TWGUI apps Info.plist is : &lt;?xml version="1.0" encoding="UTF-8"?&gt; &lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&gt; &lt;plist version="1.0"&gt; &lt;dict&gt; &lt;key&gt;BuildMachineOSBuild&lt;/key&gt; &lt;string&gt;23C71&lt;/string&gt; &lt;key&gt;CFBundleDevelopmentRegion&lt;/key&gt; &lt;string&gt;en&lt;/string&gt; &lt;key&gt;CFBundleExecutable&lt;/key&gt; &lt;string&gt;TWGUI&lt;/string&gt; &lt;key&gt;CFBundleIdentifier&lt;/key&gt; &lt;string&gt;com.example.TWAgent&lt;/string&gt; &lt;key&gt;CFBundleInfoDictionaryVersion&lt;/key&gt; &lt;string&gt;6.0&lt;/string&gt; &lt;key&gt;CFBundleName&lt;/key&gt; &lt;string&gt;TWGUI&lt;/string&gt; &lt;key&gt;CFBundlePackageType&lt;/key&gt; &lt;string&gt;APPL&lt;/string&gt; &lt;key&gt;CFBundleShortVersionString&lt;/key&gt; &lt;string&gt;1.0&lt;/string&gt; &lt;key&gt;CFBundleSupportedPlatforms&lt;/key&gt; &lt;array&gt; &lt;string&gt;MacOSX&lt;/string&gt; &lt;/array&gt; &lt;key&gt;CFBundleVersion&lt;/key&gt; &lt;string&gt;1&lt;/string&gt; &lt;key&gt;DTCompiler&lt;/key&gt; &lt;string&gt;com.apple.compilers.llvm.clang.1_0&lt;/string&gt; &lt;key&gt;DTPlatformBuild&lt;/key&gt; &lt;string&gt;&lt;/string&gt; &lt;key&gt;DTPlatformName&lt;/key&gt; &lt;string&gt;macosx&lt;/string&gt; &lt;key&gt;DTPlatformVersion&lt;/key&gt; &lt;string&gt;14.2&lt;/string&gt; &lt;key&gt;DTSDKBuild&lt;/key&gt; &lt;string&gt;23C53&lt;/string&gt; &lt;key&gt;DTSDKName&lt;/key&gt; &lt;string&gt;macosx14.2&lt;/string&gt; &lt;key&gt;DTXcode&lt;/key&gt; &lt;string&gt;1510&lt;/string&gt; &lt;key&gt;DTXcodeBuild&lt;/key&gt; &lt;string&gt;15C65&lt;/string&gt; &lt;key&gt;LSMinimumSystemVersion&lt;/key&gt; &lt;string&gt;14.2&lt;/string&gt; &lt;/dict&gt; &lt;/plist&gt; TWAgent target has main.swift file which does this : import Foundation let startTime = CFAbsoluteTimeGetCurrent() func logTimeSinceStart() { let elapsedTime = CFAbsoluteTimeGetCurrent() - startTime NSLog("Time since program started: \(elapsedTime) seconds") } func startLoggingTime() { Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in logTimeSinceStart() } } // Start logging time startLoggingTime() // Keep the run loop running CFRunLoopRun() I followed these exact same steps in another project earlier and my agent was getting registered, although i lost that project due to some reasons. But now i am getting this error when i am registering or unregistering agent using SMAppServices from the code above : Registering Agent. Status: 3 Failed to register agent: Error Domain=SMAppServiceErrorDomain Code=1 "Operation not permitted" UserInfo={NSLocalizedFailureReason=Operation not permitted} I tried diffrent fixes for like this : Moved app bundle to /applications folder Gave permission for full disc access to this app . Code sign again (both agent and TWGUI ... But nothing seems to work , getting same error. I tried to launch agent using : Launchctl load com.example.TWGUI.agent.plist and it worked , so there is no issue with my plist implementation. Can someone help me understand how can i solve this issue ? or if i am following right steps ? Can give steps need to follow to implement this and steps so that i can register and start my agent using SMAppServices? And i also tried the project give in apples official documentation : [https://developer.apple.com/documentation/servicemanagement/updating-your-app-package-installer-to-use-the-new-service-management-api) but got same error in this project as well .
2
0
143
Apr ’25
BGContinuedProcessingTask Notification Error
Hello im creating an expo module using this new API, but the problem i found currently testing this functionality is that when the task fails, the notification error doesn't go away and is always showing the failed task notification even if i start a new task and complete that one. I want to implement this module into the production app but i feel like having always the notification error might confuse our users or find it a bit bothersome. Is there a way for the users to remove this notification? Best regards!
1
0
94
Sep ’25
Possible to allow x code builds to run background processes for over 3 minutes
I have an app that I'm using for my own purposes and is not in the app store. I would like to run an http server in the background for more than the allotted 3 minutes to allow persistent communications with a connected Bluetooth device. The Bluetooth device would poll the service at intervals. Is this possible to do? This app does not need app store approval since it's only for personal use.
2
0
395
Feb ’25
Did GCD change in macOS 26
Some users of my Mac app are complaining of redrawing delays. Based on what I see in logs, my GCD timer event handlers are not being run in a timely manner although the runloop is still pumping events: sometimes 500ms pass before a 15ms timer runs. During this time, many keypresses are routed through -[NSApplication sendEvent:], which is how I know it's not locked up in synchronous code. This issue has not been reported in older versions of macOS. I start the timer like this: _gcdUpdateTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); dispatch_source_set_timer(_gcdUpdateTimer, dispatch_time(DISPATCH_TIME_NOW, period * NSEC_PER_SEC), period * NSEC_PER_SEC, 0.0005 * NSEC_PER_SEC); dispatch_source_set_event_handler(_gcdUpdateTimer, ^{ …redraw… });
1
0
111
Sep ’25
XPC Resources
XPC is the preferred inter-process communication (IPC) mechanism on Apple platforms. XPC has three APIs: The high-level NSXPCConnection API, for Objective-C and Swift The low-level Swift API, introduced with macOS 14 The low-level C API, which, while callable from all languages, works best with C-based languages General: Forums subtopic: App & System Services > Processes & Concurrency Forums tag: XPC Creating XPC services documentation NSXPCConnection class documentation Low-level API documentation XPC has extensive man pages — For the low-level API, start with the xpc man page; this is the original source for the XPC C API documentation and still contains titbits that you can’t find elsewhere. Also read the xpcservice.plist man page, which documents the property list format used by XPC services. Daemons and Services Programming Guide archived documentation WWDC 2012 Session 241 Cocoa Interprocess Communication with XPC — This is no longer available from the Apple Developer website )-: Technote 2083 Daemons and Agents — It hasn’t been updated in… well… decades, but it’s still remarkably relevant. TN3113 Testing and Debugging XPC Code With an Anonymous Listener XPC and App-to-App Communication forums post Validating Signature Of XPC Process forums post This forums post summarises the options for bidirectional communication This forums post explains the meaning of privileged flag Related tags include: Inter-process communication, for other IPC mechanisms Service Management, for installing and uninstalling Service Management login items, launchd agents, and launchd daemons Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com"
0
0
3.2k
Nov ’25
Getting Started with SMAppService
I was stuck on a long train journey this weekend, so I thought I’d use that time to write up the process for installing a launchd daemon using SMAppService. This involves a number of deliberate steps and, while the overall process isn’t too hard — it’s certainly a lot better than with the older SMJobBless — it’s easy to accidentally stray from the path and get very confused. If you have questions or comments, start a new thread in the App & System Services > Processes & Concurrency subtopic and tag it with Service Management. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" Getting Started with SMAppService This post explains how to use SMAppService to install a launchd daemon. I tested these instructions using Xcode 26.0 on macOS 15.6.1. Things are likely to be slightly different with different Xcode and macOS versions. Create the container app target To start, I created a new project: I choose File > New > Project. In the template picker, I chose macOS > App. In options page, I set the Product Name field to SMAppServiceTest [1]. And I selected my team in the Team popup. And I verified that the Organization Identifier was set to com.example.apple-samplecode, the standard for Apple sample code [1]. I selected SwiftUI in the Interface popup. There’s no requirement to use SwiftUI here; I chose it because that’s what I generally use these days. And None in the Testing System popup. And None in the Storage popup. I then completed the new project workflow. I configured basic settings on the project: In the Project navigator, I selected the SMAppServiceTest project. In the Project editor, I selected the SMAppServiceTest target. At the top I selected Signing & Capabilities. In the Signing section, I made sure that “Automatically manage signing” was checked. And that my team was selected in the Team popup. And that the bundle ID of the app ended up as com.example.apple-samplecode.SMAppServiceTest. Still in the Signing & Capabilities tab, I removed the App Sandbox section. Note It’s possible to use SMAppService to install a daemon from a sandboxed app, but in that case the daemon also has to be sandboxed. That complicates things, so I’m disabling the sandbox for the moment. See Enable App Sandbox, below, for more on this. Next I tweaked some settings to make it easier to keep track of which target is which: At the top, I selected the Build Settings tab. I changed the Product Name build setting from $(TARGET_NAME) to SMAppServiceTest. On the left, I renamed the target to App. I chose Product > Scheme > Manage Schemes. In the resulting sheet, I renamed the scheme from SMAppServiceTest to App, just to keep things in sync. [1] You are free to choose your own value, of course. However, those values affect other values later in the process, so I’m giving the specific values I used so that you can see how everything lines up. Create the daemon target I then created a daemon target: I chose File > New > Target. In the template picker, I chose macOS > Command Line Tool. In the options page, I set the Product Name field to Daemon. And I selected my team in the Team popup. And I verified that the Organization Identifier was set to com.example.apple-samplecode, the standard for Apple sample code. I selected Swift in the Language popup. And verified that SMAppServiceTest was set in the Project popup. I clicked Finish. I configured basic settings on the target: In the Project navigator, I selected the SMAppServiceTest project. In the Project editor, I selected the Daemon target. At the top I selected Signing & Capabilities. In the Signing section, I made sure that “Automatically manage signing” was checked. And that my team was selected in the Team popup. Note The Bundle Identifier field is blank, and that’s fine. There are cases where you want to give a daemon a bundle identifier, but it’s not necessary in this case. Next I tweaked some settings to make it easier to keep track of which target is which: At the top, I selected the Build Settings tab. I changed the Product Name build setting from $(TARGET_NAME) to SMAppServiceTest-Daemon. I forced the Enable Debug Dylib Support to No. IMPORTANT To set it to No, you first have to set it to Yes and then set it back to No. I edited Daemon/swift.swift to look like this: import Foundation import os.log let log = Logger(subsystem: "com.example.apple-samplecode.SMAppServiceTest", category: "daemon") func main() { log.log("Hello Cruel World!") dispatchMain() } main() This just logs a ‘first light’ log message and parks [1] the main thread in dispatchMain(). Note For more about first light log points, see Debugging a Network Extension Provider. [1] Technically the main thread terminates in this case, but I say “parks” because that’s easier to understand (-: Test the daemon executable I selected the Daemon scheme and chose Product > Run. The program ran, logging its first light log entry, and then started waiting indefinitely. Note Weirdly, in some cases the first time I ran the program I couldn’t see its log output. I had to stop and re-run it. I’m not sure what that’s about. I chose Product > Stop to stop it. I then switched back the App scheme. Embed the daemon in the app I added a build phase to embed the daemon executable into app: In the Project navigator, I selected the SMAppServiceTest project. In the Project editor, I selected the App target. At the top I selected Build Phases. I added a new copy files build phase. I renamed it to Embed Helper Tools. I set its Destination popup to Executables. I clicked the add (+) button under the list and selected SMAppServiceTest-Daemon. I made sure that Code Sign on Copy was checked for that. I then created a launchd property list file for the daemon: In the Project navigator, I selected SMAppServiceTestApp.swift. I chose Product > New > File from Template. I selected the Property List template. In the save sheet, I named the file com.example.apple-samplecode.SMAppServiceTest-Daemon.plist. And made sure that the Group popup was set to SMAppServiceTest. And that only the App target was checked in the Targets list. I clicked Create to create the file. In the property list editor, I added two properties: Label, with a string value of com.example.apple-samplecode.SMAppServiceTest-Daemon BundleProgram, with a string value of Contents/MacOS/SMAppServiceTest-Daemon I added a build phase to copy that property list into app: In the Project navigator, I selected the SMAppServiceTest project. In the Project editor, I selected the App target. At the top I selected Build Phases. I added a new copy files build phase. I renamed it to Copy LaunchDaemons Property Lists. I set its Destination popup to Wrapper. And set the Subpath field to Contents/Library/LaunchDaemons. I disclosed the contents of the Copy Bundle Resources build phase. I dragged com.example.apple-samplecode.SMAppServiceTest-Daemon.plist from the Copy Bundle Resources build phase to the new Copy LaunchDaemons Property Lists build phase. I made sure that Code Sign on Copy was unchecked. Register and unregister the daemon In the Project navigator, I selected ContentView.swift and added the following to the imports section: import os.log import ServiceManagement I then added this global variable: let log = Logger(subsystem: "com.example.apple-samplecode.SMAppServiceTest", category: "app") Finally, I added this code to the VStack: Button("Register") { do { log.log("will register") let service = SMAppService.daemon(plistName: "com.example.apple-samplecode.SMAppServiceTest-Daemon.plist") try service.register() log.log("did register") } catch let error as NSError { log.log("did not register, \(error.domain, privacy: .public) / \(error.code)") } } Button("Unregister") { do { log.log("will unregister") let service = SMAppService.daemon(plistName: "com.example.apple-samplecode.SMAppServiceTest-Daemon.plist") try service.unregister() log.log("did unregister") } catch let error as NSError { log.log("did not unregister, \(error.domain, privacy: .public) / \(error.code)") } } IMPORTANT None of this is code is structured as I would structure a real app. Rather, this is the absolutely minimal code needed to demonstrate this API. Check the app structure I chose Product > Build and verified that everything built OK. I then verified that the app’s was structured correctly: I then choose Product > Show Build Folder in Finder. I opened a Terminal window for that folder. In Terminal, I changed into the Products/Debug directory and dumped the structure of the app: % cd "Products/Debug" % find "SMAppServiceTest.app" SMAppServiceTest.app SMAppServiceTest.app/Contents SMAppServiceTest.app/Contents/_CodeSignature SMAppServiceTest.app/Contents/_CodeSignature/CodeResources SMAppServiceTest.app/Contents/MacOS SMAppServiceTest.app/Contents/MacOS/SMAppServiceTest.debug.dylib SMAppServiceTest.app/Contents/MacOS/SMAppServiceTest SMAppServiceTest.app/Contents/MacOS/__preview.dylib SMAppServiceTest.app/Contents/MacOS/SMAppServiceTest-Daemon SMAppServiceTest.app/Contents/Resources SMAppServiceTest.app/Contents/Library SMAppServiceTest.app/Contents/Library/LaunchDaemons SMAppServiceTest.app/Contents/Library/LaunchDaemons/com.example.apple-samplecode.SMAppServiceTest-Daemon.plist SMAppServiceTest.app/Contents/Info.plist SMAppServiceTest.app/Contents/PkgInfo There are a few things to note here: The com.example.apple-samplecode.SMAppServiceTest-Daemon.plist property list is in Contents/Library/LaunchDaemons. The daemon executable is at Contents/MacOS/SMAppServiceTest-Daemon. The app is still built as debug dynamic library (SMAppServiceTest.debug.dylib) but the daemon is not. Test registration I chose Product > Run. In the app I clicked the Register button. The program logged: will register did not register, SMAppServiceErrorDomain / 1 Error 1 indicates that installing a daemon hasn’t been approved by the user. The system also presented a notification: Background Items Added “SMAppServiceTest” added items that can run in the background for all users. Do you want to allow this? Options > Allow > Don’t Allow I chose Allow and authenticated the configuration change. In Terminal, I verified that the launchd daemon was loaded: % sudo launchctl list com.example.apple-samplecode.SMAppServiceTest-Daemon { "LimitLoadToSessionType" = "System"; "Label" = "com.example.apple-samplecode.SMAppServiceTest-Daemon"; "OnDemand" = true; "LastExitStatus" = 0; "Program" = "Contents/MacOS/SMAppServiceTest-Daemon"; }; IMPORTANT Use sudo to target the global launchd context. If you omit this you end up targeting the launchd context in which Terminal is running, a GUI login context, and you won't find any launchd daemons there. I started monitoring the system log: I launched the Console app. I pasted subsystem:com.example.apple-samplecode.SMAppServiceTest into the search box. I clicked “Start streaming”. Back in Terminal, I started the daemon: % sudo launchctl start com.example.apple-samplecode.SMAppServiceTest-Daemon In Console, I saw it log its first light log point: type: default time: 17:42:20.626447+0100 process: SMAppServiceTest-Daemon subsystem: com.example.apple-samplecode.SMAppServiceTest category: daemon message: Hello Cruel World! Note I’m starting the daemon manually because my goal here is to show how to use SMAppService, not how to use XPC to talk to a daemon. For general advice about XPC, see XPC Resources. Clean up Back in the app, I clicked Unregister. The program logged: will unregister did unregister In Terminal, I confirmed that the launchd daemon was unloaded: % sudo launchctl list com.example.apple-samplecode.SMAppServiceTest-Daemon Could not find service "com.example.apple-samplecode.SMAppServiceTest-Daemon" in domain for system Note This doesn’t clean up completely. The system remembers your response to the Background Items Added notification, so the next time you run the app and register your daemon it will be immediately available. To reset that state, run the sfltool with the resetbtm subcommand. Install an Agent Rather Than a Daemon The above process shows how to install a launchd daemon. Tweaking this to install a launchd agent is easy. There are only two required changes: In the Copy Launch Daemon Plists copy files build phase, set the Subpath field to Contents/Library/LaunchAgents. In ContentView.swift, change the two SMAppService.daemon(plistName:) calls to SMAppService.agent(plistName:). There are a bunch of other changes you should make, like renaming everything from daemon to agent, but those aren’t required to get your agent working. Enable App Sandbox In some cases you might want to sandbox the launchd job (the term job to refer to either a daemon or an agent.) This most commonly crops up with App Store apps, where the app itself must be sandboxed. If the app wants to install a launchd agent, that agent must also be sandboxed. However, there are actually four combinations, of which three are supported: App Sandboxed | Job Sandboxed | Supported ------------- | ------------- | --------- no | no | yes no | yes | yes yes | no | no [1] yes | yes | yes There are also two ways to sandbox the job: Continue to use a macOS > Command Line Tool target for the launchd job. Use an macOS > App target for the launchd job. In the first approach you have to use some low-level build settings to enable the App Sandbox. Specifically, you must assign the program a bundle ID and then embed an Info.plist into the executable via the Create Info.plist Section in Binary build setting. In the second approach you can use the standard Signing & Capabilities editor to give the job a bundle ID and enable the App Sandbox, but you have to adjust the BundleProgram property to account for the app-like wrapper. IMPORTANT The second approach is required if your launchd job uses restricted entitlements, that is, entitlements that must be authorised by a provisioning profile. In that case you need an app-like wrapper to give you a place to store the provisioning profile. For more on this idea, see Signing a daemon with a restricted entitlement. For more background on how provisioning profiles authorise the use of entitlements, see TN3125 Inside Code Signing: Provisioning Profiles. On balance, the second approach is the probably the best option for most developers. [1] When SMAppService was introduced it was possible to install a non-sandboxed daemon from a sandboxed app. That option is blocked by macOS 14.2 and later.
0
0
197
Sep ’25
Background Task Scheduler
Hello, An application I am working on would like to schedule push notifications for a medication reminder app. I am trying to use BGTaskScheduler to wake up periodically and submit the notifications based on the user's medication schedule. I set up the task registration in my AppDelegate's didFinishLaunchingWithOptions method: BGTaskScheduler.shared.register( forTaskWithIdentifier: backgroundTaskIdentifier, using: nil) { task in self.scheduleNotifications() task.setTaskCompleted(success: true) self.scheduleAppRefresh() } scheduleAppRefresh() I then schedule the task using: func scheduleAppRefresh() { let request = BGAppRefreshTaskRequest(identifier: backgroundTaskIdentifier) request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 1) do { try BGTaskScheduler.shared.submit(request) } catch { } } In my testing, I can see the background task getting called once, but if I do not launch the application during the day. The background task does not get called the next day. Is there something else I need to add to get repeated calls from the BGTaskScheduler? Thank You, JR
2
0
151
Oct ’25