Basic implementation of supervisor binary

This commit is contained in:
Greg Neagle
2025-04-28 13:40:36 -07:00
parent 5dd1608031
commit 93d4bfceaa
2 changed files with 279 additions and 0 deletions
@@ -216,6 +216,9 @@
C0684DFA2DBFE8130091E774 /* bootstrapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06C21332C8793720023E9D9 /* bootstrapping.swift */; };
C0684DFB2DBFE8130091E774 /* bootstrapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06C21332C8793720023E9D9 /* bootstrapping.swift */; };
C0684DFC2DBFE8130091E774 /* bootstrapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06C21332C8793720023E9D9 /* bootstrapping.swift */; };
C0684E212DBFF81B0091E774 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = C0684E202DBFF81B0091E774 /* ArgumentParser */; };
C0684E232DC0029B0091E774 /* stderrout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C008A0552D25B20E0073ADBA /* stderrout.swift */; };
C0684E252DC008A30091E774 /* UNIXProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06C21392C88CA1F0023E9D9 /* UNIXProcessInfo.swift */; };
C06C21342C8793720023E9D9 /* bootstrapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06C21332C8793720023E9D9 /* bootstrapping.swift */; };
C06C21352C8793720023E9D9 /* bootstrapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06C21332C8793720023E9D9 /* bootstrapping.swift */; };
C06C213A2C88CA1F0023E9D9 /* UNIXProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06C21392C88CA1F0023E9D9 /* UNIXProcessInfo.swift */; };
@@ -484,6 +487,15 @@
);
runOnlyForDeploymentPostprocessing = 1;
};
C0684DFF2DBFF33E0091E774 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = /usr/share/man/man1/;
dstSubfolderSpec = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 1;
};
C07A6FA32C2A82B400090743 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -620,6 +632,7 @@
C05DB2022DAC53150081FACD /* manifestutil */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = manifestutil; sourceTree = BUILT_PRODUCTS_DIR; };
C05DB2122DAC67760081FACD /* manifestutil-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "manifestutil-Bridging-Header.h"; sourceTree = "<group>"; };
C0684DD12DBFDBD20091E774 /* precache_agent */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = precache_agent; sourceTree = BUILT_PRODUCTS_DIR; };
C0684E012DBFF33E0091E774 /* supervisor */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = supervisor; sourceTree = BUILT_PRODUCTS_DIR; };
C06C21332C8793720023E9D9 /* bootstrapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = bootstrapping.swift; sourceTree = "<group>"; };
C06C21392C88CA1F0023E9D9 /* UNIXProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNIXProcessInfo.swift; sourceTree = "<group>"; };
C06C213C2C88CACE0023E9D9 /* SignalHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalHandler.swift; sourceTree = "<group>"; };
@@ -687,6 +700,7 @@
C00519A22D2A5B850060DDB6 /* authrestartd */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = authrestartd; sourceTree = "<group>"; };
C05DB2032DAC53150081FACD /* manifestutil */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = manifestutil; sourceTree = "<group>"; };
C0684DD22DBFDBD20091E774 /* precache_agent */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = precache_agent; sourceTree = "<group>"; };
C0684E022DBFF33E0091E774 /* supervisor */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = supervisor; sourceTree = "<group>"; };
C0D66AC02D2CA1DE009EF807 /* logouthelper */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = logouthelper; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
@@ -739,6 +753,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C0684DFE2DBFF33E0091E774 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C0684E212DBFF81B0091E774 /* ArgumentParser in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C07A6FA22C2A82B400090743 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -913,6 +935,7 @@
C0D66AC02D2CA1DE009EF807 /* logouthelper */,
C05DB2032DAC53150081FACD /* manifestutil */,
C0684DD22DBFDBD20091E774 /* precache_agent */,
C0684E022DBFF33E0091E774 /* supervisor */,
C07A6FA62C2A82B400090743 /* Products */,
C01364502C311DFA008DB215 /* Frameworks */,
);
@@ -936,6 +959,7 @@
C0D66ABF2D2CA1DE009EF807 /* logouthelper */,
C05DB2022DAC53150081FACD /* manifestutil */,
C0684DD12DBFDBD20091E774 /* precache_agent */,
C0684E012DBFF33E0091E774 /* supervisor */,
);
name = Products;
sourceTree = "<group>";
@@ -1240,6 +1264,29 @@
productReference = C0684DD12DBFDBD20091E774 /* precache_agent */;
productType = "com.apple.product-type.tool";
};
C0684E002DBFF33E0091E774 /* supervisor */ = {
isa = PBXNativeTarget;
buildConfigurationList = C0684E052DBFF33E0091E774 /* Build configuration list for PBXNativeTarget "supervisor" */;
buildPhases = (
C0684DFD2DBFF33E0091E774 /* Sources */,
C0684DFE2DBFF33E0091E774 /* Frameworks */,
C0684DFF2DBFF33E0091E774 /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
C0684E022DBFF33E0091E774 /* supervisor */,
);
name = supervisor;
packageProductDependencies = (
C0684E202DBFF81B0091E774 /* ArgumentParser */,
);
productName = supervisor;
productReference = C0684E012DBFF33E0091E774 /* supervisor */;
productType = "com.apple.product-type.tool";
};
C07A6FA42C2A82B400090743 /* managedsoftwareupdate */ = {
isa = PBXNativeTarget;
buildConfigurationList = C07A6FAC2C2A82B400090743 /* Build configuration list for PBXNativeTarget "managedsoftwareupdate" */;
@@ -1447,6 +1494,9 @@
C0684DD02DBFDBD10091E774 = {
CreatedOnToolsVersion = 16.3;
};
C0684E002DBFF33E0091E774 = {
CreatedOnToolsVersion = 16.3;
};
C07A6FA42C2A82B400090743 = {
CreatedOnToolsVersion = 15.4;
LastSwiftMigration = 1540;
@@ -1510,6 +1560,7 @@
C0D66ABE2D2CA1DE009EF807 /* logouthelper */,
C05DB2012DAC53150081FACD /* manifestutil */,
C0684DD02DBFDBD10091E774 /* precache_agent */,
C0684E002DBFF33E0091E774 /* supervisor */,
);
};
/* End PBXProject section */
@@ -1693,6 +1744,15 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C0684DFD2DBFF33E0091E774 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C0684E252DC008A30091E774 /* UNIXProcessInfo.swift in Sources */,
C0684E232DC0029B0091E774 /* stderrout.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C07A6FA12C2A82B400090743 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -2224,6 +2284,34 @@
};
name = Release;
};
C0684E062DBFF33E0091E774 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = com.googlecode.munki.supervisor;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
C0684E072DBFF33E0091E774 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = com.googlecode.munki.supervisor;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
C07A6FAA2C2A82B400090743 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -2685,6 +2773,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C0684E052DBFF33E0091E774 /* Build configuration list for PBXNativeTarget "supervisor" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C0684E062DBFF33E0091E774 /* Debug */,
C0684E072DBFF33E0091E774 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C07A6FA02C2A82B400090743 /* Build configuration list for PBXProject "munki" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -2812,6 +2909,11 @@
package = C0B715BC2DA6F43E00F255FB /* XCRemoteSwiftPackageReference "swift-certificates" */;
productName = X509;
};
C0684E202DBFF81B0091E774 /* ArgumentParser */ = {
isa = XCSwiftPackageProductDependency;
package = C0E994182C5C10E1006FDF44 /* XCRemoteSwiftPackageReference "swift-argument-parser" */;
productName = ArgumentParser;
};
C0848D1E2C94D4610008B463 /* ArgumentParser */ = {
isa = XCSwiftPackageProductDependency;
package = C0E994182C5C10E1006FDF44 /* XCRemoteSwiftPackageReference "swift-argument-parser" */;
+177
View File
@@ -0,0 +1,177 @@
//
// supervisor.swift
// supervisor
//
// Created by Greg Neagle on 4/28/25.
//
// Copyright 2024-2025 Greg Neagle.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import ArgumentParser
import Foundation
private let PROCESS_DID_NOT_START: Int32 = 127
private let PROCESS_TIMED_OUT: Int32 = 126
private let signalName = [
SIGINT: "SIGINT",
SIGTERM: "SIGTERM",
]
func signalHandler(_ sig: Int32) -> DispatchSourceSignal {
// the intent here is to kill our child process(es) when we get a SIGINT or SIGTERM
// (sadly we can't do it for SIGKILL) so they don't keep running if we're stopped
// by the user (or killed by another process)
signal(sig, SIG_IGN) // // Make sure the signal does not terminate the application.
let sigSrc = DispatchSource.makeSignalSource(signal: sig, queue: .main)
sigSrc.setEventHandler {
printStderr("Got signal \(signalName[sig] ?? String(sig))")
// kill all our child processes
let ourPid = ProcessInfo.processInfo.processIdentifier
for task in processesWithPPID(ourPid) {
printStderr("Sending signal \(signalName[sig] ?? String(sig)) to \(task.command), pid \(task.pid)...")
let osErr = kill(task.pid, sig)
if osErr != noErr {
printStderr("Got err \(osErr) when sending \(signalName[sig] ?? String(sig)) to \(task.command), pid \(task.pid)")
}
}
// reset the signal handler to default
signal(sig, SIG_DFL)
kill(ourPid, sig)
}
return sigSrc
}
class SupervisorProcessRunner {
let task = Process()
var timeout: Int = 0
init(_ command: String, arguments: [String] = [], timeout: Int = 0) {
task.executableURL = URL(fileURLWithPath: command)
task.arguments = arguments
self.timeout = timeout
}
deinit {
// make sure the task gets terminated
killTask()
}
func killTask() {
let KILL_WAIT_TIME_USEC = useconds_t(1_000_000)
task.terminate() // sends SIGTERM
usleep(KILL_WAIT_TIME_USEC)
if !task.isRunning {
return
}
let pid = task.processIdentifier
_signal.kill(pid, SIGKILL)
usleep(KILL_WAIT_TIME_USEC)
if task.isRunning {
// log("pid \(pid) won't die")
}
}
func run() async -> Int32 {
var deadline: Date?
if !task.isRunning {
do {
if timeout > 0 {
deadline = Date().addingTimeInterval(TimeInterval(timeout))
}
try task.run()
} catch {
// task didn't start
printStderr("ERROR running \(task.executableURL?.path ?? "")")
printStderr(error.localizedDescription)
return PROCESS_DID_NOT_START
}
}
while task.isRunning {
// loop until process exits
if let deadline {
if Date() >= deadline {
printStderr("ERROR: \(task.executableURL?.path ?? "") timed out after \(timeout) seconds")
killTask()
return PROCESS_TIMED_OUT
}
}
await Task.yield()
}
return task.terminationStatus
}
}
class Supervisor {
var timeout: Int
var delayRandom: Int
var command: String
var arguments: [String]
init(timeout: Int, delayRandom: Int, command: String, arguments: [String]) {
self.timeout = timeout
self.delayRandom = delayRandom
self.command = command
self.arguments = arguments
}
func execute() async -> Int32 {
// log("Executing \(command) with arguments: \(arguments)")
if delayRandom > 0 {
let randomDelay = Int.random(in: 0 ... delayRandom)
usleep(useconds_t(randomDelay * 1_000_000))
}
return await SupervisorProcessRunner(command, arguments: arguments, timeout: timeout).run()
}
}
@main
struct SupervisorCommand: AsyncParsableCommand {
static var configuration = CommandConfiguration(
usage: "supervisor [options] -- <path_to_executable> [arguments]"
)
@Option(help: ArgumentHelp("after n seconds, terminate the executable",
discussion: "0 seconds means never timeout",
valueName: "n")
)
var timeout: Int = 0
@Option(name: [.customLong("delayrandom")],
help: ArgumentHelp("delay the execution of executable by random seconds up to n", valueName: "n"))
var delayRandom: Int = 0
@Argument(parsing: .postTerminator,
help: ArgumentHelp(valueName: "path_to_executable [arguments]"))
var commandAndArgs: [String]
mutating func run() async throws {
// install handlers for SIGINT and SIGTERM
let sigintSrc = signalHandler(SIGINT)
sigintSrc.activate()
let sigtermSrc = signalHandler(SIGTERM)
sigtermSrc.activate()
let command = commandAndArgs[0]
let arguments = Array(commandAndArgs[1...])
throw await ExitCode(
Supervisor(
timeout: timeout,
delayRandom: delayRandom,
command: command,
arguments: arguments
).execute()
)
}
}