I created a LSPCatalyst class in my Mac Catalyst app to replace the MacOS LanguageServerProcessHost. To make that work, I replaced the process
property with a processProxy
that accesses the process instance in a MacOS bundle using the FoundationApp protocol as explained below.
Following @Adam's suggestion, I created a MacOS bundle to proxy for the process instance. You follow the same idea as he pointed to for AppKit access from Catalyst apps, but you just need Foundation to get access to Process. I called the bundle FoundationGlue and put everything in a FoundationGlue folder in my Xcode project. The bundle needs an Info.plist that identifies the principal class as "FoundationGlue.MacApp", and the MacApp.swift looks like:
import Foundation
class MacApp: NSObject, FoundationApp {
var process: Process!
var terminationObserver: NSObjectProtocol!
func initProcess(_ launchPath: String!, _ arguments: [String]?, _ environment: [String : String]?) {
process = Process()
process.launchPath = launchPath
process.arguments = arguments
process.environment = environment
}
func setTerminationCompletion(_ completion: (()->Void)!) {
let terminationCompletion = {
NotificationCenter.default.removeObserver(self.terminationObserver!)
completion?()
}
terminationObserver =
NotificationCenter.default.addObserver(
forName: Process.didTerminateNotification,
object: process,
queue: nil) { notification -> Void in
terminationCompletion()
}
}
func setupProcessPipes(_ stdin: Pipe!, _ stdout: Pipe!, _ stderr: Pipe!) {
process.standardInput = stdin
process.standardOutput = stdout
process.standardError = stderr
}
func launchProcess() {
process.launch()
print("Launched process (process.processIdentifier)")
}
func terminateProcess() {
process.terminate()
}
func isRunningProcess() -> Bool {
return process.isRunning
}
}
The corresponding header I called FoundationApp.h looks like:
#import <Foundation/Foundation.h>
@protocol FoundationApp <NSObject>
typedef void (^terminationCompletion) ();
- (void)initProcess: (NSString *) launchPath :(NSArray<NSString *> *) arguments :(NSDictionary<NSString *, NSString *> *) environment;
- (void)setTerminationCompletion: (terminationCompletion) completion;
- (void)setupProcessPipes: (NSPipe *) stdin :(NSPipe *) stdout :(NSPipe *) stderr;
- (void)launchProcess;
- (void)terminateProcess;
- (bool)isRunningProcess;
@end
And the FoundationAppGlue-Bridging-Header.h just contains:
#import "FoundationApp.h"
Once you have the bundle built for MacOS, add it as a framework to your Mac Catalyst project. I created a Catalyst.swift in that project for access to the FoundationGlue bundle functionality::
import Foundation
@available(macCatalyst 13, *)
struct Catalyst {
/// Catalyst.foundation gives access to the Foundation functionality identified in FoundationApp.h and implemented in FoundationGlue/MacApp.swift
static var foundation: FoundationApp! {
let url = Bundle.main.builtInPlugInsURL?.appendingPathComponent("FoundationGlue.bundle")
let bundle = Bundle(path: url!.path)!
bundle.load()
let cls = bundle.principalClass as! NSObject.Type
return cls.init() as? FoundationApp
}
}
Then, you use it from your app like:
let foundationApp = Catalyst.foundation!
foundationApp.initProcess("/bin/sh", ["-c", "echo 1
sleep 1
echo 2
sleep 1
echo 3
sleep 1
echo 4
sleep 1
exit
"], nil)
foundationApp.setTerminationCompletion({print("terminated")})
foundationApp.launchProcess()