Xcode+Swift+XPC: How to start and deploy a Swift XPC target on MacOS - swift

DISCLAIMER: I am relatively new to MacOS/Xcode
I want to build a simple XPC Launch Agent in Swift (ie: in ~/Library/LaunchAgents) but I could not find much documentation.
I started with Xcode XPC template but I do not know if it was a good idea for my Swift project.
I notice I should also have ~/Library/LaunchAgents/com.demo.myservice.plist
Versions:
- MacOS: 10.13.2
- Xcode: 9.2
Instruction to create the Xcode XPC Project:
File > New project
I chose the MacOS template: XPC
I create the bundle ‘com.demo.myservice’
It creates me an Objective-C project. So I delete all files (ie: myserviceProtocol.h, myservice.h, myservice.m, main.m and Info.plist
Create the files:
myserviceProtocol.swift
import Foundation
#objc(myserviceProtocol) protocol myserviceProtocol {
func ping()
}
myservice.swift
import Foundation
class myservice : NSObject, myserviceProtocol {
func ping() {
print("ping")
}
}
main.swift
import Foundation
class ServiceDelegate : NSObject, NSXPCListenerDelegate {
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
newConnection.exportedInterface = NSXPCInterface(with:myserviceProtocol.self)
let exportedObject = myservice()
newConnection.exportedObject = exportedObject
newConnection.resume()
return true
}
}
// Create the listener and resume it:
//
let delegate = ServiceDelegate()
let listener = NSXPCListener.service()
listener.delegate = delegate;
listener.resume()
Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>com.demo.myservice</string>
<key>ProgramArguments</key>
<array>
<string>myservice</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
I build it:
I copied Info.plist into ~/Library/LaunchAgents/ : cp ~/Documents/myservice/myservice/Info.plist ~/Library/LaunchAgents/com.demo.myservice.plist
I retrieve my userid with id -u
And then I try to execute it from the command line (as it does not seem to do anything from Xcode):
sudo launchctl debug user/501/com.demo.myservice /Users/olivier/Library/Developer/Xcode/DerivedData/myservice-hbwefcgibyqbajguvblgcmxsnrmd/Build/Products/Debug/myservice.xpc
Configuration failed: 113: Could not find specified service
Could not find service "com.demo.myservice" in domain for uid: 501
I am not really sure of what I am doing. Was I right to use XPC template to create my swift XPC.

If you want your Agent to provide an XPC service, you need to expose it as a Mach Service.
The way you are initialising your Listener is for an XPC Service (notice capital S), an XPC Service is a bundle that is part of your application's bundle, located inside the Contents/XPCServices/ directory.
So in summary, you would have to:
1.) Create an Agent that exposes an XPC service via mach service. Your listener would look like:
let listener = NSXPCListener(machServiceName: "com.rderik.exampleXPC" )
2.) To consume your Agent's service you'll need to build the connection to that mach service.
let connection = NSXPCConnection(machServiceName: "com.rderik.exampleXPC")
I hope that helps.
If you want to read more, I wrote a tutorial on how to do that here:
https://rderik.com/blog/creating-a-launch-agent-that-provides-an-xpc-service-on-macos/

I'm not sure what your goal is. If you just want a daemon that is managed by launchd, then you don't need XPC. Just create a daemon (probably with the Command Line Tool project template) and create a launchd plist configuration file (see man launchd.plist).
XPC is intended to communicate with a host app. The service binary should be embedded in your app bundle and launchd will launch it when your app tries to connect. You don't need to modify ~/Library/LaunchAgents for this.
Your app needs to setup an NSXPCConnection to connect to the XPCListener you created.
Something like:
let connection = NSXPCConnection(serviceName: "com.demo.myservice")
let interface = NSXPCInterface(with: myserviceProtocol.self)
connection.remoteObjectInterface = interface
connection.resume()
let proxy = connection.remoteObjectProxyWithErrorHandler {(error) in
os_log("Connection Error: %{public}#", error.localizedDescription)
} as! myserviceProtocol
// message proxy here to communicate with service
Both scenarios are covered in more detail in the Daemons and Services Programming Guide

Related

CarPlay parking app crashed when launching from Xcode 12.5.1 CarPlay simulator

I'm new in iOS development. Got task on job to extend our iOS app with CarPlay. I created class 'CarPlaySceneDelegate' as entry point for Car Play. Code is below:
class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
var interfaceController: CPInterfaceController?
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
didConnect interfaceController: CPInterfaceController) {
self.interfaceController = interfaceController
...
interfaceController.setRootTemplate(tabBarTemplate, animated: true)
}
private func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
didDisconnect interfaceController: CPInterfaceController) {
self.interfaceController = nil
}
}
To Info.plist I added this configuration:
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>Configuration Name</key>
<string>Default configuration</string>
<key>Delegate Class Name</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>Storyboard Name</key>
<string>Main</string>
</dict>
</array>
<key>CPTemplateApplicationSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>CPTemplateApplicationScene</string>
<key>UISceneConfigurationName</key>
<string>BlueGate-Car</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).CarPlaySceneDelegate</string>
</dict>
</array>
</dict>
</dict>
On launching on simulator CarPlay launches good according to CarPlaySceneDelegate class. But on iPhone Simulator it's not launched at all. App loads and screen become black.
In console:
2021-11-03 12:45:11.228204+0200 BlueGate[5806:119147] - <AppMeasurement>[I-ACS036001] Analytics screen reporting is disabled. UIViewController transitions will not be logged.
2021-11-03 10:45:11 +0000 [AppDelegate.swift]:[application(_:didFinishLaunchingWithOptions:)][103:19] start function
2021-11-03 12:45:11.714422+0200 BlueGate[5806:118883] [Firebase/Crashlytics] Version 8.7.0
2021-11-03 12:45:12.108628+0200 BlueGate[5806:119153] 8.7.0 - [Firebase/Analytics][I-ACS023007] Analytics v.8.7.0 started
2021-11-03 12:45:12.119347+0200 BlueGate[5806:119153] 8.7.0 - [Firebase/Analytics][I-ACS023008] To enable debug logging set the following application argument: -FIRAnalyticsDebugEnabled (see http://<someLink>)
2021-11-03 12:45:12.140585+0200 BlueGate[5806:119154] 8.7.0 - [Firebase/Analytics][I-ACS025036] App Delegate Proxy is disabled
2021-11-03 10:45:12 +0000 [AppDelegate.swift]:[configureNetworkMonitoring()][89:35] Network is ON
2021-11-03 12:45:12.316632+0200 BlueGate[5806:119154] 8.7.0 - [Firebase/Messaging][I-FCM002022] APNS device token not set before retrieving FCM Token for Sender ID '188981723956'. Notifications to this FCM Token will not be delivered over APNS.Be sure to re-retrieve the FCM token once the APNS device token is set.
2021-11-03 12:45:17.626397+0200 BlueGate[5806:119147] 8.7.0 - [Firebase/Analytics][I-ACS800023] No pending snapshot to activate. SDK name: app_measurement
2021-11-03 12:45:17.735262+0200 BlueGate[5806:119147] 8.7.0 - [Firebase/Analytics][I-ACS023012] Analytics collection enabled
My environment Xcode 12.5.1
Did I do anything wrong or Is there any other way I can implement the CarPlay part feature by Xcode/swift while keeping the mobile app on iOS?
Appreciate any comments or help.
The keys of your application scene in the plist seem to be wrong. Try using UISceneConfigurationName instead of Configuration Name, UISceneDelegateClassName instead of Delegate Class Name and UILaunchStoryboardName instead of Storyboard Name.

iOS Network Extension error creating TUN/TAP interface SIOCGIFMTU failed: device not configured

Currently working on a Network Extension that lets me stablish a connection using a VPN using a .ovpn file, by using OpenVPNAdapter library. I have saved correcly my configuration to the System settings and when running the extension to perform debug my extension status changes from disconnected, stays in connecting for a while and then disconnects. Further inspecting the console logs for the device anf filtering by the network extension I get three main error messages.
Log message from provider: TUN Error: cannot acquire tun interface socket
SIOCGIFMTU failed: Device not configured
NEVirtualInterfaceAdjustReadBufferSize: interface_get_mtu failed (6), defaulting to max mtu
I don't know where to head now as I am debugging the network extension using the Console from the device.
Okay I managed to solve this on my own. I created a Packet Tunnel Network Extension and in my PacketTunnelProvider class came the problems. It did not crash so setting up the debugger in that class was not worth it. I ran my target and started my app and set several NSLogs in the functions so I could see in the device's console what was happening. My problem was that I tried to set a nil value in a dictionary for a key thus terminating the extension. That crash message can easily be seen in the console.
The problem was when extending PacketTunnelProvider with OpenVPNAdapterDelegate in the function to configure the tunnel
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, completionHandler: #escaping (OpenVPNAdapterPacketFlow?) -> Void) {
networkSettings?.dnsSettings?.matchDomains = [""];
}
Previously I had networkSettings.dnsSettings?.matchDomains = [""]; so networkSettings was unwrapped and it was nil making it crash the extension and the tunnel not being able to get connected.

How to write to local Application Support directory on OS X

I'm writing a Swift app for OS X whose primary purpose is to read data from a usb device plugged into the computer and upload it to our services layer for analyzation and storage. The app is meant to be usable by any user that has an account on the Mac it is installed on.
For support and further analytical purposes, the app is also required to include its install id, a UUID generated during the first launch of the application, in every upload. This allows our support team to associate an installation instance of our app with the set of users who have access to it so that troubleshooting and data collection is more accurate and precise.
In my app, I'm storing the install id in a file and trying to store that file in a central location, the local Application Support directory.
More specifically, I would like to store it at the following location:
Macintosh HD/Library/Application Support/MyApp/installId/installId.txt.
This is how I try save files in the Application Support directory:
var installId = String()
let fileManager = FileManager.default
var isDir: ObjCBool = false
if let appSupportDirectory = fileManager.urls(for: .applicationSupportDirectory, in: .localDomainMask).first {
let installIdDirectory = appSupportDirectory.appendingPathComponent(Bundle.main.bundleIdentifier ?? "MyApp").appendingPathComponent("installId")
let installIdFile = installIdDirectory.appendingPathComponent("installId.txt")
do {
if fileManager.fileExists(atPath: installIdFile.path, isDirectory: &isDir) {
if !isDir.boolValue {
let data = try String.init(contentsOf: installIdFile)
installId = String(data.split(separator: ":")[1])installId))")
}
else {
print("\nError: installId file appears to be a directory.")
}
}
else {
try fileManager.createDirectory(at: installIdDirectory, withIntermediateDirectories: true, attributes: nil)
let pendingInstallId = "installId:\(UUID())"
try pendingInstallId.write(to: installIdFile, atomically: false, encoding: String.Encoding.utf8)
installId = pendingInstallId
}
} catch {
print("\nError: \(error.localizedDescription)")
}
}
else {
print("\nError: Could not find Application Support Directory.")
}
When I run my app, I receive the following error:
You don't have permission to save the file "installId" in the folder "MyApp".
The error does not occur; however, if I choose to store my file in the Application Support directory in the user domain mask. The file containing the install id is created and stored in a folder called MyApp within my user Application Support directory.
I've tried searching for a solution to my problem, but it has not been too fruitful. Some posts claim that the directory I'm trying to store my file in is reserved for apps with admin privileges (source 1) while others claim i should instead be using the Application Support directory in the user domain mask for such tasks (source 2). However, I need this file to be accessible to any user who has an account on the Mac that the app is installed on, so the local domain masks' Application Support directory seems to be a better fit for this scenario.
Could someone help me out or point me in the right direction? How can I save data to this directory? If I can't feasibly do so, is there another central location that I can do it where a user is unlikely to venture into and delete that data?
Thanks in advance!
The directory /Library/Application Support/ belongs to root. You can see that in a Terminal by typing:
$ ls -al /Library | grep Appl*
drwxr-xr-x 15 root admin 480 Jan 4 16:10 Application Support
To write to that directory your App needs root privileges. Refer to Apple Documentation to securely implement this. The Apple documentation mentions authopen which seems reasonable to create a file in the support folder at the first run of your App.

Failed to load launch URL with error: Error Domain=TVMLKitErrorDomain Code=3 "(null)"

Description:
I created a new TVML project and launched it. The first error was the App Transport Security, which I fixed via Info.plist :
App Transport Security Settings -> Allow Arbitrary Loads -> YES
Then I ran it again and I'm getting this error:
Failed to load launch URL with error: (null)
appController(_:didFailWithError:) invoked with error: Error
Domain=TVMLKitErrorDomain Code=3 "(null)"
The project seems to stop here (application func in AppDelegate.swift):
appControllerContext.launchOptions["BASEURL"] = AppDelegate.tvBaseURL
print(launchOptions) //returns nil
//error on following line
if let launchOptions = launchOptions as? [String: AnyObject] {
//does not enter here
for (kind, value) in launchOptions {
appControllerContext.launchOptions[kind] = value
}
}
What I've tried:
I attempted changing the tvBaseURL from "http://localhost:9001/" to http://MY-IP-ADDRESS-HERE:9001/
but that didn't change anything.
Question:
What is causing this error and how do I solve it?
You should start the server with port number
enter the following command in terminal
ruby -run -ehttpd . -p9001
And finally your tvBaseURL should navigate to the server folder like this
"http://yourLocalhost:9001/Downloads/TVMLCatalogUsingTVMLTemplates/Server/"
I also faced the same problem, I solved it by changing tvBaseURL in AppDelegate
static let tvBaseURL = "http://127.0.0.1:9001/Downloads/TVMLCatalogUsingTVMLTemplates/Server/"
As you see - I have to show exact path to Server folder. That also works if you put it to some web server.
Hope that it can help!
I just ran into this issue. You need to pay close attention to the terminal output.
I got:
[2019-03-15 12:28:43] INFO WEBrick 1.3.1
[2019-03-15 12:28:43] INFO ruby 2.3.7 (2018-03-28) [universal.x86_64-darwin17]
/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/ruby/2.3.0/socket.rb:205:
in `bind': Address already in use - bind(2) for 0.0.0.0:9001 (Errno::EADDRINUSE)
Address already in use - bind(2) for 0.0.0.0:9001
At this point you either have to choose a different port number (if you decide to do such then make sure your server's port and your Xcode's project port match) or kill the previous server by ctrl + c or just killing that terminal window.
Also note in some of Apple's sample projects the ruby -run -ehttpd . -p9001 command is to be done in a folder named Server and for others it's just suppose to be done in the App's main folder. Just look into the README file to figure this out.

Get Zookeeper started automatically in OS X Yosemite using launchd

Like the title said, I'm trying to launch zookeeper automatically using launchd/launchctl in OS X Yosemite.
This is my plist file "/Library/LaunchDaemons/com.test.zookeeper.plist", it's owned by root:wheel.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.test.zookeeper</string>
<key>RunAtLoad</key>
<true/>
<key>ProgramArguments</key>
<array>
<string>/opt/zookeeper/bin/zkServer.sh</string>
<string>start</string>
</array>
<key>StandardInPath</key>
<string>/var/log/zookeeper_stdin.log</string>
<key>StandardOutPath</key>
<string>/var/log/zookeeper_stdout.log</string>
<key>StandardErrorPath</key>
<string>/var/log/zookeeper_stderr.log</string>
</dict>
</plist>
after executing "sudo launchctl load com.test.zookeeper.plist",
this is what's in the "/var/log/system.log", user name has been replaced with asterisks.
Apr 17 11:13:22 ***-03 sudo[97284]: ****.** : TTY=ttys003 ; PWD=/Library/LaunchDaemons ; USER=root ; COMMAND=/bin/launchctl load com.test.zookeeper.plist
Apr 17 11:13:22 ***-03 com.apple.xpc.launchd[1] (com.apple.xpc.launchd.domain.system): Session adoption is only allowed in user domains.
Apr 17 11:13:22 ***-03 nohup[97299]: Could not adopt Background session: 125: Domain does not support specified action
"/var/log/zookeeper_stdout.log"
Starting zookeeper ... STARTED
"/var/log/zookeeper_stderr.log"
JMX enabled by default
Using config: /opt/zookeeper/bin/../conf/zoo.cfg
So it seems like the job was executed by launchd, but somehow the actual service is not present when I execute "ps -ef | grep zoo" to check for the service, which I would normally see if I started zookeeper manually using "sudo /opt/zookeeper/bin/zkServer.sh start"
Can someone help me? Thank you.
I had a simple ping being run by launchd and I would get
Session adoption is only allowed in user domains.
in the system log -- and the process would not run.
My guess is it gets blocked due to that issue in the log.
If you do launchctl list |grepplist name and it shows - in the first column (which is supposed to be a PID), then the job never ran.
Below, is an example of a working plist running a bash script.
# launchctl list |grep ping
549 0 com.pingServers