livebook/elixirkit/elixirkit_swift/Sources/ElixirKit/ElixirKit.swift
2023-01-16 21:09:47 +01:00

179 lines
5.1 KiB
Swift

import Foundation
import Network
public class API {
static var process: Process?
private static var release: Release?
public static var isRunning: Bool {
get {
release != nil && release!.isRunning;
}
}
public static func start(name: String, logPath: String? = nil, terminationHandler: ((Process) -> Void)? = nil) {
release = Release(name: name, logPath: logPath, terminationHandler: terminationHandler)
}
public static func publish(_ name: String, _ data: String) {
release!.publish(name, data)
}
public static func stop() {
release!.stop();
}
public static func waitUntilExit() {
release!.waitUntilExit();
}
}
private class Release {
let listener: NWListener
let startProcess: Process
let semaphore = DispatchSemaphore(value: 0)
var logHandle: FileHandle?
var connection: NWConnection?
var isRunning: Bool {
get {
startProcess.isRunning
}
}
init(name: String, logPath: String? = nil, terminationHandler: ((Process) -> Void)? = nil) {
listener = try! NWListener(using: .tcp, on: .any)
let bundle = Bundle.main
var rootDir = "";
if bundle.bundlePath.hasSuffix(".app") {
rootDir = "\(bundle.bundlePath)/Contents/Resources"
}
else {
rootDir = bundle.bundlePath
}
startProcess = Process()
if logPath != nil {
let logPath = logPath!
let fm = FileManager.default
if !fm.fileExists(atPath: logPath) { fm.createFile(atPath: logPath, contents: Data()) }
logHandle = FileHandle(forUpdatingAtPath: logPath)!
logHandle!.seekToEndOfFile()
let stdout = Pipe()
let stderr = Pipe()
startProcess.standardOutput = stdout
startProcess.standardError = stderr
let stdouth = stdout.fileHandleForReading
let stderrh = stderr.fileHandleForReading
stdouth.waitForDataInBackgroundAndNotify()
stderrh.waitForDataInBackgroundAndNotify()
NotificationCenter.default.addObserver(
self,
selector: #selector(receiveStdout(n:)),
name: NSNotification.Name.NSFileHandleDataAvailable,
object: stdouth
)
NotificationCenter.default.addObserver(
self,
selector: #selector(receiveStderr(n:)),
name: NSNotification.Name.NSFileHandleDataAvailable,
object: stderrh
)
}
startProcess.launchPath = "\(rootDir)/rel/bin/\(name)"
startProcess.arguments = ["start"]
startProcess.terminationHandler = terminationHandler
listener.stateUpdateHandler = stateDidChange(to:)
listener.newConnectionHandler = didAccept(connection:)
listener.start(queue: .global())
let timeout = DispatchTime.now() + DispatchTimeInterval.seconds(5)
if semaphore.wait(timeout: timeout) == .timedOut {
fatalError("waited for connection for more than 5s")
}
}
func stateDidChange(to state: NWListener.State) {
switch state {
case .ready:
start(port: listener.port!.rawValue.description)
case .failed(let error):
print("Server error: \(error.localizedDescription)")
exit(EXIT_FAILURE)
default:
break
}
}
func start(port: String) {
var env = ProcessInfo.processInfo.environment
env["ELIXIRKIT_PORT"] = port
startProcess.environment = env
try! startProcess.run()
}
func didAccept(connection: NWConnection) {
self.connection = connection
self.connection!.start(queue: .main)
semaphore.signal()
}
func send(_ string: String) {
connection!.send(
content: (string + "\n").data(using: .utf8),
completion: .contentProcessed { error in
if error != nil {
print(error!)
}
}
)
}
@objc
func receiveStdout(n: NSNotification) {
let h = n.object as! FileHandle
let data = h.availableData
if !data.isEmpty {
FileHandle.standardOutput.write(data)
logHandle!.write(data)
h.waitForDataInBackgroundAndNotify()
}
}
@objc
func receiveStderr(n: NSNotification) {
let h = n.object as! FileHandle
let data = h.availableData
if !data.isEmpty {
logHandle!.write(data)
FileHandle.standardError.write(data)
h.waitForDataInBackgroundAndNotify()
}
}
public func publish(_ name: String, _ data: String) {
let encoded = data.data(using: .utf8)!.base64EncodedString()
let message = "event:\(name):\(encoded)"
send(message)
}
public func stop() {
connection!.cancel()
listener.cancel()
waitUntilExit()
}
public func waitUntilExit() {
startProcess.waitUntilExit()
}
}