Hey, thanks for taking the time to write!
It would be better to develop a
regular 'ole app that can run on either iOS or macOS and then you never
have to deal with these kinds of problems. Yeah, I know I'm getting myself into tricky waters, but it's a pretty useful utility I'm building for in-house use, that might even have eventual product potential. I've considered parsing CLI tools like ps, but as you know, that cuts off App Store access.
When you said "Libproc is a nice approach", which did you mean, using proc_pidinfo to get CPU info for each thread of each process (summing to get total process usage), or using proc_pid_rusage like iStat does? In the former case, that won't work for other user's processes, and in the latter case, I'm not sure how to convert the data proc_pid_rusage returns into a proper cpu usage percentage.
It is like you just open the window and tiny birds fly in and just type
out the code on your keyboard using SwiftUI while you drink Mojitos. Lol
Post
Replies
Boosts
Views
Activity
I've also asked about this on StackOverflow. It's a little niche, so understandably it didn't attract much of a crowd. Feel free to answer there for the internet points https://stackoverflow.com/q/64435545/3141234
Thought I'd share my research on what workarounds might exist, in case that sparks any ideas.
In narrow cases (IBActions for the menu or toolbar items), the need for top-to-bottom data passing can be replaced with the responder chain. However, this won't help for other kinds of information, e.g. injecting a DownloaderService object into the view from the window, from the app delegate.
It looks like the new NSStoryBoard.instantiateInitialController(creator:) APIs introduced in 10.15 can remove the two-stage initialization pattern for NSWindowControllers. You're given the coder instance and expected to initialize the window controller yourself. This allows you to call any initializer you want. This is great, because you can make a MyWindowController.init?(coder: NSCoder, mySupplimentaryValues: ...). Since you're now calling a real init, the fields it sets no longer have to be implicitly unwrapped optionals. Sweet! Even still, I'm not sure how/when you would pass data from the window controller to the content view controller. aaaaand it appears to be total completely broken - https://stackoverflow.com/q/60343551/3141234, so I can't use it yet. Huge bummer :(
Segues are usually how you can pass data from one controller to another, but from what I can tell. This seems like a good point to "kick off" the top-down initialization process, but it appears that the containment segue between a window controller and its content view controller doesn't actually trigger a segue. Similarly, if you have a @IBSegueAction setup between an NSWindowController and an NSViewController, it will be ignored, as mentioned in the "known issues" section of the Xcode 11 release notes - https://developer.apple.com/documentation/xcode-release-notes/xcode-11-release-notes
I can give up on Storyboards and switch back to Nibs, where each window/view has its controller manually instantiated by code from its parent, allowing me to override init?(coder:) with init?(coder: NSCoder, mySupplimentaryValues: ...), where I can do all my work with all the values I need. I'm apprehensive to do this however, because I find it hard to believe that such a simple use case can't be implemented in Storyboards.
I can pass data from my App Delegate directly to my main window's content view controller using a global variable, or a singleton. yuck.
Aw man, that's disappoint to hear. But I think it's time for me to roll up my sleeves and move things back over.
Interestingly, it seems like the folder icons in /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources, such as GenericFolderIcon.icns, are actually the Catalina version of these assets.
I found a Big Sur folder asset in /System/Library/PrivateFrameworks/IconFoundation.framework/Versions/A/Resources/Assets.car (which I opened with AssetCatalogTinkerer). I found the plain folder icon in several colours, but not specialized icons for the desktop, music, images, etc. folders. I suspect these might be computationally generated by Finder.
Hi Quinn,
In summary, are these the right steps for uninstallation?
#!/bin/sh
sudo launchctl remove /Library/LaunchDaemons/com.foo.bar.plist
sudo rm /Library/LaunchDaemons/com.foo.bar.plist
sudo rm /Library/PrivilegedHelperTools/com.foo.bar
I have a few questions about it:
I see that launchctl remove is an synchronous variant of unload. Is there a possibility that the privileged helper gets killed before the "rm" steps? Or will launchd wait for the process to exit on its own accord?
I would prefer to avoid using a script (I'm worried that it's a point of vulnerability if I get the permissions/signing wrong on it), so I planning to use the Process and FileManager APIs to achieve the same thing, right from my helper process. Is there any danger posed by a program deleting its own executable from the file system? I assume it keeps the inode open (is that the right terminology?) and won't be an issue, but could you please confirm that?
Out of curiosity, what would happen if you deleted the plist and executable without first unloading or removing it with launchctl?
Thanks
Are you building an XPC Service? Or vending an XPC service from a launchd daemon or agent?
Both, in the same pattern as the "BetterAuthorizationSample" (using an XPC service to bless a privileged helper, on behalf of a sandboxed application).
do you have transactions enabled (EnableTransactions)?
I do not. (yet?)
Spot on! That was exactly it.
Thanks Quinn!
Hmmmm hold the phone, spoke to soon!
This worked, in that the XPC service is now able to present an interactive prompt for the password, and SMJobBless succeeds, but it still breaks when the main app is made to be sandboxed (which was the whole goal of this EBAS sample).
Here's what the logs have to say:
info authd Process /usr/libexec/smd (PID 788) evaluates 1 rights with flags 00000003 (engine 213): (
"com.apple.ServiceManagement.blesshelper"
)
debug authd engine 213: user not used password
debug authd engine 213: checking if rule com.apple.ServiceManagement.blesshelper contains password-only item
debug authd engine 213: _preevaluate_rule com.apple.ServiceManagement.blesshelper
error authd Sandbox denied authorizing right 'com.apple.ServiceManagement.blesshelper' for authorization created by '/MyApp.app/Contents/XPCServices/IntermediatorXPCService.xpc' [13471] (engine 213)
debug authd engine 213: authorize result: -60005
(-60005 is errAuthorizationDenied)
SMJobBless itself fails with The operation couldn’t be completed. (CFErrorDomainLaunchd error 4.)
This is another case where intuitively, this makes sense. The docs for JoinExistingSession say:
Boolean. Indicates that your service runs in the same security session as the caller.
The default value is False, which indicates that the service is run in a new security session.
Set the value to True if the service needs to access to the user’s keychain, the pasteboard, or other per-session resources and services.
If we're sharing the same security session, and the parent is made sandboxed, then how would it be possible for the XPC service to use the authorization services APIs, when the main app can't? I'm left stumped with how the EBAS makes this work.
A security session is set up when the user logs in, but you don’t get a new session when you launch a sandboxed app. You can confirm this with SessionGetInfo.
Indeed, it appears that every "regular app" is sharing the same security and audit sessions.
Here are some details about the security and audit sessions:
Without JoinExistingSession:
Main app:
My security session (id 100019) has attributes: [.sessionHasGraphicAccess, .sessionHasTTY])
My audit session (id 100019) and has flags: [AU_SESSION_FLAG_HAS_GRAPHIC_ACCESS, AU_SESSION_FLAG_HAS_TTY, AU_SESSION_FLAG_HAS_CONSOLE_ACCESS, AU_SESSION_FLAG_HAS_AUTHENTICATED]
XPC Service:
My security session (id 103826) has attributes: [])
My audit session (id 103826) and has flags: []
With JoinExistingSession:
EBAS has these same flags, as well.
Main app:
My security session (id 100019) has attributes: [.sessionHasGraphicAccess, .sessionHasTTY])
My audit session (id 100019) and has flags: [AU_SESSION_FLAG_HAS_GRAPHIC_ACCESS, AU_SESSION_FLAG_HAS_TTY, AU_SESSION_FLAG_HAS_CONSOLE_ACCESS]
XPC Service:
My security session (id 100019) has attributes: [.sessionHasGraphicAccess, .sessionHasTTY])
My audit session (id 100019) and has flags: [AU_SESSION_FLAG_HAS_GRAPHIC_ACCESS, AU_SESSION_FLAG_HAS_TTY, AU_SESSION_FLAG_HAS_CONSOLE_ACCESS, AU_SESSION_FLAG_HAS_AUTHENTICATED]
Security and Audit session logging code
Here's the code I used to generate those messages, in case you're curious.
extension SessionAttributeBits: CaseIterable {
public static var allCases: [SessionAttributeBits] {
[
.sessionIsRoot,
.sessionHasGraphicAccess,
.sessionHasTTY,
.sessionIsRemote,
]
}
}
extension SessionAttributeBits: CustomStringConvertible {
public var description: String {
switch self {
case .sessionIsRoot: return ".sessionIsRoot"
case .sessionHasGraphicAccess: return ".sessionHasGraphicAccess"
case .sessionHasTTY: return ".sessionHasTTY"
case .sessionIsRemote: return ".sessionIsRemote"
default: return Self.allCases.filter(self.contains).description
}
}
}
// Why isn't `audit_session_flags` imported as an OptionSet to begin with?
extension audit_session_flags: OptionSet {}
extension audit_session_flags: CaseIterable {
public static var allCases: [audit_session_flags] {
[
AU_SESSION_FLAG_IS_INITIAL,
AU_SESSION_FLAG_HAS_GRAPHIC_ACCESS,
AU_SESSION_FLAG_HAS_TTY,
AU_SESSION_FLAG_IS_REMOTE,
AU_SESSION_FLAG_HAS_CONSOLE_ACCESS,
AU_SESSION_FLAG_HAS_AUTHENTICATED,
]
}
}
extension audit_session_flags: CustomStringConvertible {
public var description: String {
switch self {
case AU_SESSION_FLAG_IS_INITIAL: return "AU_SESSION_FLAG_IS_INITIAL"
case AU_SESSION_FLAG_HAS_GRAPHIC_ACCESS: return "AU_SESSION_FLAG_HAS_GRAPHIC_ACCESS"
case AU_SESSION_FLAG_HAS_TTY: return "AU_SESSION_FLAG_HAS_TTY"
case AU_SESSION_FLAG_IS_REMOTE: return "AU_SESSION_FLAG_IS_REMOTE"
case AU_SESSION_FLAG_HAS_CONSOLE_ACCESS: return "AU_SESSION_FLAG_HAS_CONSOLE_ACCESS"
case AU_SESSION_FLAG_HAS_AUTHENTICATED: return "AU_SESSION_FLAG_HAS_AUTHENTICATED"
default: return Self.allCases.filter(self.contains).description
}
}
}
extension auditinfo_addr_t {
static func forCurrentProcess() -> Self {
var auditInfo = auditinfo_addr_t();
let errorCode = getaudit_addr(&auditInfo, Int32(MemoryLayout.size(ofValue: auditInfo)))
assert(errorCode == 0, "getaudit_addr returned a non-zero error code: \(errorCode)")
return auditInfo
}
var auditSessionFlags: audit_session_flags {
audit_session_flags(rawValue: UInt32(self.ai_flags))
}
}
public func printSecuritySessionInfo() {
var mySessionID = SecuritySessionId()
var sessionAttributes = SessionAttributeBits()
let errorCode = SessionGetInfo(callerSecuritySession, &mySessionID, &sessionAttributes)
assert(errorCode == errSecSuccess)
print("My security session (id \(mySessionID)) has attributes: \(sessionAttributes))")
}
public func printAuditSessionInfo() {
let auditSession = auditinfo_addr_t.forCurrentProcess()
print("My audit session (id \(auditSession.ai_asid)) and has flags: \(auditSession.auditSessionFlags)")
}