Attempt to address #1264

This commit is contained in:
Greg Neagle
2025-09-04 14:51:04 -07:00
parent bad5e1ed75
commit 09a46efd55
4 changed files with 263 additions and 12 deletions
@@ -50,6 +50,7 @@
C0C5DF6C20F5C27700CA0687 /* MSCStatusController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C5DF6B20F5C27700CA0687 /* MSCStatusController.swift */; };
C0D30BEB20CA445A005E876E /* MunkiItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D30BEA20CA445A005E876E /* MunkiItems.swift */; };
C0E2599E210AD8CE00C3A3D9 /* Socket.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E2599D210AD8CD00C3A3D9 /* Socket.swift */; };
C0E8F96B2E6A3FB8003A298C /* UNIXProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E8F96A2E6A3FB8003A298C /* UNIXProcessInfo.swift */; };
C0FBF1BD20D4945B00FAD1BE /* mschtml.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FBF1BC20D4945B00FAD1BE /* mschtml.swift */; };
C0FBF1BF20D4A0BB00FAD1BE /* Template.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FBF1BE20D4A0BB00FAD1BE /* Template.swift */; };
C0FD710A20D485A80018A002 /* HtmlFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FD710920D485A80018A002 /* HtmlFilter.swift */; };
@@ -140,6 +141,7 @@
C0C5DF6B20F5C27700CA0687 /* MSCStatusController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MSCStatusController.swift; sourceTree = "<group>"; };
C0D30BEA20CA445A005E876E /* MunkiItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MunkiItems.swift; sourceTree = "<group>"; };
C0E2599D210AD8CD00C3A3D9 /* Socket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socket.swift; sourceTree = "<group>"; };
C0E8F96A2E6A3FB8003A298C /* UNIXProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNIXProcessInfo.swift; sourceTree = "<group>"; };
C0F8E3DF2105622F00718259 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C0F8E3E221057A1500718259 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/MainMenu.strings; sourceTree = "<group>"; };
C0F8E3E421057A2D00718259 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/MainMenu.strings; sourceTree = "<group>"; };
@@ -237,6 +239,7 @@
C04F827D20BB319B00F9C57D /* Managed Software Center */ = {
isa = PBXGroup;
children = (
C0E8F96A2E6A3FB8003A298C /* UNIXProcessInfo.swift */,
8428C7F12E0DB23200D83D41 /* XIBs */,
C0E2599D210AD8CD00C3A3D9 /* Socket.swift */,
C0F8E3DF2105622F00718259 /* Info.plist */,
@@ -484,6 +487,7 @@
C0BCAD7D2442A0B3001D2FDD /* appleupdates.swift in Sources */,
C04CA61E20E6ADA100711461 /* MainWindowController.swift in Sources */,
8428C7E82E0CB55000D83D41 /* MainSplitViewController.swift in Sources */,
C0E8F96B2E6A3FB8003A298C /* UNIXProcessInfo.swift in Sources */,
C01603CF20CF8B6100DEF9E4 /* iconutils.swift in Sources */,
56C33E812174D7A200D727DC /* MSCTableCellView.swift in Sources */,
8428C7F02E0DABC700D83D41 /* LogWindowController.swift in Sources */,
@@ -69,7 +69,6 @@ class MSCStatusController: NSObject {
// Monitors managedsoftwareupdate process for failure to start
// or unexpected exit, so we're not waiting around forever if
// managedsoftwareupdate isn't running.
let PYTHON_SCRIPT_NAME = "managedsoftwareupdate"
let NEVER_STARTED = -2
let UNEXPECTEDLY_QUIT = -1
if !session_started {
@@ -82,7 +81,7 @@ class MSCStatusController: NSObject {
saw_process = true
// clear the flag so we have to get another status update
got_status_update = false
} else if pythonScriptRunning(PYTHON_SCRIPT_NAME) {
} else if managedsoftwareupdateInstanceRunning() != nil {
timeout_counter = 6
saw_process = true
} else {
@@ -0,0 +1,198 @@
//
// UNIXProcessInfo.swift
// munki
//
// Created by Greg Neagle on 9/4/24.
//
// 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 Darwin
import Foundation
struct UNIXProcessInfo {
let pid: Int32
let ppid: Int32
let uid: UInt32
let command: String
}
/// Returns a list of running processes
func UNIXProcessList() -> [UNIXProcessInfo] {
var list = [UNIXProcessInfo]()
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0]
var size = 0
// First need to need size of process list array
var result = sysctl(&mib, u_int(mib.count), nil, &size, nil, 0)
assert(result == KERN_SUCCESS)
// Get process list
let procCount = size / MemoryLayout<kinfo_proc>.stride
var kinfoList = [kinfo_proc](repeating: kinfo_proc(), count: procCount)
result = sysctl(&mib, u_int(mib.count), &kinfoList, &size, nil, 0)
assert(result == KERN_SUCCESS)
for task in kinfoList {
var kinfo = task
let command = withUnsafePointer(to: &kinfo.kp_proc.p_comm) {
String(cString: UnsafeRawPointer($0).assumingMemoryBound(to: CChar.self))
}
list.append(
UNIXProcessInfo(
pid: kinfo.kp_proc.p_pid,
ppid: kinfo.kp_eproc.e_ppid,
uid: kinfo.kp_eproc.e_ucred.cr_uid,
command: command
)
)
}
return list
}
/// Returns a list of running processes with the given parent pid
func processesWithPPID(_ ppid: Int32) -> [UNIXProcessInfo] {
let list = UNIXProcessList()
return list.filter { $0.ppid == ppid }
}
/// Gets the (raw) process argument data
func argumentData(for pid: pid_t) -> Data? {
// Lifted from Quinn's work here: https://developer.apple.com/forums/thread/681817
// There should be a better way to get a processs arguments
// (FB9149624) but right now you have to use `KERN_PROCARGS2`
// and then parse the results.
var argMax: CInt = 0
var argMaxSize = size_t(MemoryLayout.size(ofValue: argMax))
let err = sysctlbyname("kern.argmax", &argMax, &argMaxSize, nil, 0)
guard err >= 0 else {
return nil
}
// precondition(argMaxSize != 0)
var result = Data(count: Int(argMax))
let resultSize = result.withUnsafeMutableBytes { buf -> Int in
var mib: [CInt] = [
CTL_KERN,
KERN_PROCARGS2,
pid,
]
var bufSize = buf.count
let err = sysctl(&mib, CUnsignedInt(mib.count), buf.baseAddress!, &bufSize, nil, 0)
guard err >= 0 else {
return -1
}
return bufSize
}
if resultSize < 0 {
return nil
}
result = result.prefix(resultSize)
return result
}
enum ParseError: Error {
case unexpectedEnd
case argumentIsNotUTF8
}
/// Parses the argument data into a list of strings
func parseArgumentData(_ data: Data) throws -> [String] {
// Lifted from Quinn's work here: https://developer.apple.com/forums/thread/681817
// The algorithm here was was stolen from the Darwin source for `ps`.
//
// <https://opensource.apple.com/source/adv_cmds/adv_cmds-176/ps/print.c.auto.html>
// returns a list of strings: [0] is the executable path,
// the rest is `argv[0]` through `argv[argc - 1]
// Parse `argc`. Were assuming the value is little endian here, which is
// currently accurate but it could be a problem if weve gone back to
// metric.
var remaining = data[...]
guard remaining.count >= 6 else {
throw ParseError.unexpectedEnd
}
let count32 = remaining.prefix(4).reversed().reduce(0) { $0 << 8 | UInt32($1) }
remaining = remaining.dropFirst(4)
// Get the executable path
let exeBytes = remaining.prefix(while: { $0 != 0 })
guard let executable = String(bytes: exeBytes, encoding: .utf8) else {
throw ParseError.argumentIsNotUTF8
}
remaining = remaining.dropFirst(exeBytes.count)
guard remaining.count != 0 else {
throw ParseError.unexpectedEnd
}
// Skip any zeros until the next non-zero
remaining = remaining.drop(while: { $0 == 0 })
// Now parse `argv[0]` through `argv[argc - 1]`.
var result: [String] = [executable]
for _ in 0 ..< count32 {
let argBytes = remaining.prefix(while: { $0 != 0 })
guard let arg = String(bytes: argBytes, encoding: .utf8) else {
throw ParseError.argumentIsNotUTF8
}
result.append(arg)
remaining = remaining.dropFirst(argBytes.count)
guard remaining.count != 0 else {
throw ParseError.unexpectedEnd
}
remaining = remaining.dropFirst()
}
return result
}
/// Returns the executable path and all arguments as a list of strings
func executableAndArgsForPid(_ pid: Int32) -> [String]? {
if let data = argumentData(for: pid) {
return try? parseArgumentData(data)
}
return nil
}
struct UNIXProcessInfoWithPath {
let pid: Int32
let ppid: Int32
let uid: UInt32
let command: String
let path: String
}
/// Returns a list of running processes with pid, ppid, uid, command, and path
func UNIXProcessListWithPaths() -> [UNIXProcessInfoWithPath] {
let procList = UNIXProcessList()
var processes = [UNIXProcessInfoWithPath]()
for proc in procList {
if proc.pid != 0,
let data = argumentData(for: proc.pid)
{
let args = (try? parseArgumentData(data)) ?? []
if !args.isEmpty {
processes.append(
UNIXProcessInfoWithPath(
pid: proc.pid,
ppid: proc.ppid,
uid: proc.uid,
command: proc.command,
path: args[0]
)
)
}
}
}
return processes
}
@@ -484,20 +484,70 @@ func justUpdate() throws {
}
}
func pythonScriptRunning(_ scriptName: String) -> Bool {
let output = exec("/bin/ps", args: ["-eo", "command="])
let lines = output.components(separatedBy: "\n")
for line in lines {
let part = line.components(separatedBy: " ")
if (part[0].contains("/MacOS/Python") || part[0].contains("python")) {
if part.count > 1 {
if part[1].contains(scriptName) {
return true
/// Returns ProcessID for a running python script matching the scriptName
/// as long as the pid is not the same as ours
/// this is used to see if the managedsoftwareupdate script is already running
func pythonScriptRunning(_ scriptName: String) -> Int32? {
let ourPid = ProcessInfo().processIdentifier
let processes = UNIXProcessListWithPaths()
for item in processes {
if item.pid == ourPid {
continue
}
let executable = (item.path as NSString).lastPathComponent
if executable.contains("python") || executable.contains("Python") {
// get all the args for this pid
if var args = executableAndArgsForPid(item.pid), args.count > 2 {
// first value is executable path, drop it
// next value is command, drop it
args = Array(args.dropFirst(2))
// drop leading args that start with a hyphen
args = Array(args.drop(while: { $0.hasPrefix("-") }))
if args.count > 0, args[0].hasSuffix(scriptName) {
return item.pid
}
}
}
}
return false
return nil
}
/// Returns Process ID for a running executable matching the name
func executableRunning(_ name: String) -> Int32? {
let processes = UNIXProcessListWithPaths()
for item in processes {
if name.hasPrefix("/") {
// full path, so exact comparison
if item.path == name {
return item.pid
}
} else {
// does executable path end with the name?
if item.path.hasSuffix(name) {
return item.pid
}
}
}
return nil
}
/// Returns the pid of managedsoftwareupdate process, if found
func managedsoftwareupdateInstanceRunning() -> Int32? {
// A Python version of managedsoftwareupdate might be running,
// or a compiled version
if let pid = executableRunning("managedsoftwareupdate") {
return pid
}
if let pid = pythonScriptRunning(".managedsoftwareupdate.py") {
return pid
}
if let pid = pythonScriptRunning("managedsoftwareupdate.py") {
return pid
}
if let pid = pythonScriptRunning("managedsoftwareupdate") {
return pid
}
return nil
}
func getRunningProcessesWithUsers() -> [[String:String]] {