So much new code to support software installation

This commit is contained in:
Greg Neagle
2024-08-06 09:32:16 -07:00
parent c404fb23ef
commit 4cd2f3bf66
19 changed files with 1705 additions and 53 deletions
+11
View File
@@ -0,0 +1,11 @@
//
// main.swift
// appusaged
//
// Created by Greg Neagle on 8/3/24.
//
import Foundation
print("Hello, World!")
+165 -6
View File
@@ -131,12 +131,32 @@
C0D00FB82C45BCB90021DA9C /* errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D00FB52C45BCB90021DA9C /* errors.swift */; };
C0D00FB92C45BCB90021DA9C /* errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D00FB52C45BCB90021DA9C /* errors.swift */; };
C0D00FBA2C45BCB90021DA9C /* errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D00FB52C45BCB90021DA9C /* errors.swift */; };
C0D9C2462C5C5B370019A067 /* socketclient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2452C5C5B370019A067 /* socketclient.swift */; };
C0D9C24E2C5C5C920019A067 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C24D2C5C5C920019A067 /* main.swift */; };
C0D9C2522C5C5D1F0019A067 /* errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D00FB52C45BCB90021DA9C /* errors.swift */; };
C0D9C2532C5C5D4F0019A067 /* socketclient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2452C5C5B370019A067 /* socketclient.swift */; };
C0D9C2542C5C5EBC0019A067 /* constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FB12C2B22D300090743 /* constants.swift */; };
C0D9C2552C5C5EC00019A067 /* plistutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364422C2DD1BA008DB215 /* plistutils.swift */; };
C0D9C2572C5D391C0019A067 /* launchd.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2562C5D391C0019A067 /* launchd.swift */; };
C0D9C2592C5D4E570019A067 /* appusage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2582C5D4E570019A067 /* appusage.swift */; };
C0D9C27F2C5EA2490019A067 /* server.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C27C2C5EA2200019A067 /* server.swift */; };
C0D9C2822C5EA2A00019A067 /* common.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2812C5EA2A00019A067 /* common.swift */; };
C0D9C2832C5EA33A0019A067 /* client.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2452C5C5B370019A067 /* client.swift */; };
C0D9C2842C5EA33C0019A067 /* client.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2452C5C5B370019A067 /* client.swift */; };
C0D9C2852C5EA3410019A067 /* common.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2812C5EA2A00019A067 /* common.swift */; };
C0D9C28D2C5EDFAE0019A067 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C28C2C5EDFAE0019A067 /* main.swift */; };
C0D9C2912C5EDFD70019A067 /* launchd.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2562C5D391C0019A067 /* launchd.swift */; };
C0D9C2922C5EE0A80019A067 /* cliutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FD92C2CF19600090743 /* cliutils.swift */; };
C0D9C2942C5F08CA0019A067 /* pkg.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2932C5F08CA0019A067 /* pkg.swift */; };
C0D9C2952C5F08CA0019A067 /* pkg.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2932C5F08CA0019A067 /* pkg.swift */; };
C0D9C2972C6012C80019A067 /* dmg.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2962C6012C80019A067 /* dmg.swift */; };
C0D9C2982C6012C80019A067 /* dmg.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2962C6012C80019A067 /* dmg.swift */; };
C0D9C29A2C607D390019A067 /* xattr.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2992C607D390019A067 /* xattr.swift */; };
C0D9C29B2C607D390019A067 /* xattr.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2992C607D390019A067 /* xattr.swift */; };
C0D9C29D2C6151350019A067 /* core.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C29C2C6151350019A067 /* core.swift */; };
C0D9C29E2C6151350019A067 /* core.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C29C2C6151350019A067 /* core.swift */; };
C0D9C2A02C6175A10019A067 /* processes.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C29F2C6175A10019A067 /* processes.swift */; };
C0D9C2A12C6175A10019A067 /* processes.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C29F2C6175A10019A067 /* processes.swift */; };
C0D9C2A32C61939E0019A067 /* scriptutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2A22C61939E0019A067 /* scriptutils.swift */; };
C0D9C2A42C61939E0019A067 /* scriptutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D9C2A22C61939E0019A067 /* scriptutils.swift */; };
C0E993F82C5BEDAB006FDF44 /* removepackages.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E993F72C5BEDAB006FDF44 /* removepackages.swift */; };
C0E993FC2C5BEDE7006FDF44 /* rmpkgs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C043ED222C483EEE0047C025 /* rmpkgs.swift */; };
C0E993FD2C5BEDFA006FDF44 /* display.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364572C3265D6008DB215 /* display.swift */; };
@@ -214,6 +234,15 @@
);
runOnlyForDeploymentPostprocessing = 1;
};
C0D9C2882C5EDFAE0019A067 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = /usr/share/man/man1/;
dstSubfolderSpec = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 1;
};
C0E993F32C5BEDAB006FDF44 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -270,9 +299,21 @@
C0D00FA72C45814F0021DA9C /* repoutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = repoutils.swift; sourceTree = "<group>"; };
C0D00FAF2C458EAA0021DA9C /* version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = version.swift; sourceTree = "<group>"; };
C0D00FB52C45BCB90021DA9C /* errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = errors.swift; sourceTree = "<group>"; };
C0D9C2452C5C5B370019A067 /* socketclient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = socketclient.swift; sourceTree = "<group>"; };
C0D9C2452C5C5B370019A067 /* client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = client.swift; sourceTree = "<group>"; };
C0D9C24B2C5C5C920019A067 /* app_usage_monitor */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = app_usage_monitor; sourceTree = BUILT_PRODUCTS_DIR; };
C0D9C24D2C5C5C920019A067 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
C0D9C2562C5D391C0019A067 /* launchd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = launchd.swift; sourceTree = "<group>"; };
C0D9C2582C5D4E570019A067 /* appusage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = appusage.swift; sourceTree = "<group>"; };
C0D9C27C2C5EA2200019A067 /* server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = server.swift; sourceTree = "<group>"; };
C0D9C2812C5EA2A00019A067 /* common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = common.swift; sourceTree = "<group>"; };
C0D9C28A2C5EDFAE0019A067 /* appusaged */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = appusaged; sourceTree = BUILT_PRODUCTS_DIR; };
C0D9C28C2C5EDFAE0019A067 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
C0D9C2932C5F08CA0019A067 /* pkg.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = pkg.swift; sourceTree = "<group>"; };
C0D9C2962C6012C80019A067 /* dmg.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dmg.swift; sourceTree = "<group>"; };
C0D9C2992C607D390019A067 /* xattr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = xattr.swift; sourceTree = "<group>"; };
C0D9C29C2C6151350019A067 /* core.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = core.swift; sourceTree = "<group>"; };
C0D9C29F2C6175A10019A067 /* processes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = processes.swift; sourceTree = "<group>"; };
C0D9C2A22C61939E0019A067 /* scriptutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = scriptutils.swift; sourceTree = "<group>"; };
C0E993F52C5BEDAB006FDF44 /* removepackages */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = removepackages; sourceTree = BUILT_PRODUCTS_DIR; };
C0E993F72C5BEDAB006FDF44 /* removepackages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = removepackages.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -326,6 +367,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C0D9C2872C5EDFAE0019A067 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
C0E993F22C5BEDAB006FDF44 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -390,6 +438,9 @@
isa = PBXGroup;
children = (
C043ED222C483EEE0047C025 /* rmpkgs.swift */,
C0D9C2932C5F08CA0019A067 /* pkg.swift */,
C0D9C2962C6012C80019A067 /* dmg.swift */,
C0D9C29C2C6151350019A067 /* core.swift */,
);
path = installer;
sourceTree = "<group>";
@@ -405,6 +456,7 @@
C030A9CE2C41F798007F0B34 /* munkiimport */,
C0E993F62C5BEDAB006FDF44 /* removepackages */,
C0D9C24C2C5C5C920019A067 /* app_usage_monitor */,
C0D9C28B2C5EDFAE0019A067 /* appusaged */,
C07A6FA62C2A82B400090743 /* Products */,
C01364502C311DFA008DB215 /* Frameworks */,
);
@@ -420,6 +472,7 @@
C030A9CD2C41F798007F0B34 /* munkiimport */,
C0E993F52C5BEDAB006FDF44 /* removepackages */,
C0D9C24B2C5C5C920019A067 /* app_usage_monitor */,
C0D9C28A2C5EDFAE0019A067 /* appusaged */,
);
name = Products;
sourceTree = "<group>";
@@ -451,6 +504,7 @@
C07A6FD02C2B631800090743 /* shared */ = {
isa = PBXGroup;
children = (
C0D9C27E2C5EA22D0019A067 /* socket */,
C043ED212C483ED40047C025 /* installer */,
C01364472C2F8EC0008DB215 /* munkirepo */,
C013643A2C2DC50E008DB215 /* admin */,
@@ -475,7 +529,11 @@
C0D00FAF2C458EAA0021DA9C /* version.swift */,
C0D00FB52C45BCB90021DA9C /* errors.swift */,
C043ED1E2C4822C70047C025 /* sqlite3.swift */,
C0D9C2452C5C5B370019A067 /* socketclient.swift */,
C0D9C2562C5D391C0019A067 /* launchd.swift */,
C0D9C2582C5D4E570019A067 /* appusage.swift */,
C0D9C2992C607D390019A067 /* xattr.swift */,
C0D9C29F2C6175A10019A067 /* processes.swift */,
C0D9C2A22C61939E0019A067 /* scriptutils.swift */,
);
path = shared;
sourceTree = "<group>";
@@ -488,6 +546,24 @@
path = app_usage_monitor;
sourceTree = "<group>";
};
C0D9C27E2C5EA22D0019A067 /* socket */ = {
isa = PBXGroup;
children = (
C0D9C2452C5C5B370019A067 /* client.swift */,
C0D9C27C2C5EA2200019A067 /* server.swift */,
C0D9C2812C5EA2A00019A067 /* common.swift */,
);
path = socket;
sourceTree = "<group>";
};
C0D9C28B2C5EDFAE0019A067 /* appusaged */ = {
isa = PBXGroup;
children = (
C0D9C28C2C5EDFAE0019A067 /* main.swift */,
);
path = appusaged;
sourceTree = "<group>";
};
C0E993F62C5BEDAB006FDF44 /* removepackages */ = {
isa = PBXGroup;
children = (
@@ -616,6 +692,23 @@
productReference = C0D9C24B2C5C5C920019A067 /* app_usage_monitor */;
productType = "com.apple.product-type.tool";
};
C0D9C2892C5EDFAE0019A067 /* appusaged */ = {
isa = PBXNativeTarget;
buildConfigurationList = C0D9C28E2C5EDFAE0019A067 /* Build configuration list for PBXNativeTarget "appusaged" */;
buildPhases = (
C0D9C2862C5EDFAE0019A067 /* Sources */,
C0D9C2872C5EDFAE0019A067 /* Frameworks */,
C0D9C2882C5EDFAE0019A067 /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = appusaged;
productName = appusaged;
productReference = C0D9C28A2C5EDFAE0019A067 /* appusaged */;
productType = "com.apple.product-type.tool";
};
C0E993F42C5BEDAB006FDF44 /* removepackages */ = {
isa = PBXNativeTarget;
buildConfigurationList = C0E993FB2C5BEDAB006FDF44 /* Build configuration list for PBXNativeTarget "removepackages" */;
@@ -666,6 +759,9 @@
C0D9C24A2C5C5C920019A067 = {
CreatedOnToolsVersion = 15.4;
};
C0D9C2892C5EDFAE0019A067 = {
CreatedOnToolsVersion = 15.4;
};
C0E993F42C5BEDAB006FDF44 = {
CreatedOnToolsVersion = 15.4;
};
@@ -694,6 +790,7 @@
C030A9CC2C41F798007F0B34 /* munkiimport */,
C0E993F42C5BEDAB006FDF44 /* removepackages */,
C0D9C24A2C5C5C920019A067 /* app_usage_monitor */,
C0D9C2892C5EDFAE0019A067 /* appusaged */,
);
};
/* End PBXProject section */
@@ -731,6 +828,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C0D9C27F2C5EA2490019A067 /* server.swift in Sources */,
C030A9EB2C4241AC007F0B34 /* munkilog.swift in Sources */,
C030A9D42C41F849007F0B34 /* munkiimportOptions.swift in Sources */,
C0D00FBA2C45BCB90021DA9C /* errors.swift in Sources */,
@@ -771,11 +869,17 @@
C07074E82C34932400B86310 /* pkgutils.swift in Sources */,
C07074E52C34910F00B86310 /* osutils.swift in Sources */,
C07A6FDA2C2CF19600090743 /* cliutils.swift in Sources */,
C0D9C2A02C6175A10019A067 /* processes.swift in Sources */,
C01364432C2DD1BA008DB215 /* plistutils.swift in Sources */,
C030A98F2C39C135007F0B34 /* munkihash.swift in Sources */,
C0D00FB62C45BCB90021DA9C /* errors.swift in Sources */,
C07A6FB02C2B22A400090743 /* prefs.swift in Sources */,
C0D9C2972C6012C80019A067 /* dmg.swift in Sources */,
C07A6FA92C2A82B400090743 /* managedsoftwareupdate.swift in Sources */,
C0D9C2A32C61939E0019A067 /* scriptutils.swift in Sources */,
C0D9C29D2C6151350019A067 /* core.swift in Sources */,
C0D9C29A2C607D390019A067 /* xattr.swift in Sources */,
C0D9C2942C5F08CA0019A067 /* pkg.swift in Sources */,
C0D00FB02C458EAA0021DA9C /* version.swift in Sources */,
C043ED1F2C4822C70047C025 /* sqlite3.swift in Sources */,
C07074DC2C33AE5F00B86310 /* munkilog.swift in Sources */,
@@ -801,12 +905,17 @@
C0D00FB72C45BCB90021DA9C /* errors.swift in Sources */,
C030A9C82C41F32E007F0B34 /* munkiimportOptions.swift in Sources */,
C030A9C02C409738007F0B34 /* iconutils.swift in Sources */,
C0D9C2462C5C5B370019A067 /* socketclient.swift in Sources */,
C0D9C2A12C6175A10019A067 /* processes.swift in Sources */,
C030A9F62C435183007F0B34 /* readline.swift in Sources */,
C0D9C2572C5D391C0019A067 /* launchd.swift in Sources */,
C0D9C2952C5F08CA0019A067 /* pkg.swift in Sources */,
C0D9C2592C5D4E570019A067 /* appusage.swift in Sources */,
C0D00FB12C458EAA0021DA9C /* version.swift in Sources */,
C030A9C32C41B581007F0B34 /* pkginfoOptions.swift in Sources */,
C01364552C321FE7008DB215 /* dmgutils.swift in Sources */,
C0D9C29B2C607D390019A067 /* xattr.swift in Sources */,
C030A9BC2C3F4CF4007F0B34 /* munkiimportlib.swift in Sources */,
C0D9C29E2C6151350019A067 /* core.swift in Sources */,
C07A6FDB2C2CF19600090743 /* cliutils.swift in Sources */,
C043ED202C4822C70047C025 /* sqlite3.swift in Sources */,
C0D00FA82C45814F0021DA9C /* repoutils.swift in Sources */,
@@ -820,11 +929,15 @@
C07074E02C33B9A000B86310 /* reports.swift in Sources */,
C07074E62C34910F00B86310 /* osutils.swift in Sources */,
C030A9C12C419565007F0B34 /* osinstaller.swift in Sources */,
C0D9C2982C6012C80019A067 /* dmg.swift in Sources */,
C030A9C22C41B556007F0B34 /* pkginfolib.swift in Sources */,
C07074E92C34932400B86310 /* pkgutils.swift in Sources */,
C07074EC2C34A6AD00B86310 /* versionutils.swift in Sources */,
C013644A2C2F8EFE008DB215 /* GitFileRepo.swift in Sources */,
C0D9C2A42C61939E0019A067 /* scriptutils.swift in Sources */,
C043ED242C483EEE0047C025 /* rmpkgs.swift in Sources */,
C0D9C2842C5EA33C0019A067 /* client.swift in Sources */,
C0D9C2822C5EA2A00019A067 /* common.swift in Sources */,
C07A6FD32C2B659300090743 /* utils.swift in Sources */,
C07A6FD72C2B7F2100090743 /* FileRepo.swift in Sources */,
C01364402C2DCA5C008DB215 /* admincommon.swift in Sources */,
@@ -864,9 +977,20 @@
files = (
C0D9C2522C5C5D1F0019A067 /* errors.swift in Sources */,
C0D9C24E2C5C5C920019A067 /* main.swift in Sources */,
C0D9C2532C5C5D4F0019A067 /* socketclient.swift in Sources */,
C0D9C2852C5EA3410019A067 /* common.swift in Sources */,
C0D9C2542C5C5EBC0019A067 /* constants.swift in Sources */,
C0D9C2552C5C5EC00019A067 /* plistutils.swift in Sources */,
C0D9C2832C5EA33A0019A067 /* client.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C0D9C2862C5EDFAE0019A067 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C0D9C2922C5EE0A80019A067 /* cliutils.swift in Sources */,
C0D9C28D2C5EDFAE0019A067 /* main.swift in Sources */,
C0D9C2912C5EDFD70019A067 /* launchd.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1162,12 +1286,36 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 52ZFZKM6BK;
ENABLE_HARDENED_RUNTIME = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
C0D9C2512C5C5C920019A067 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 52ZFZKM6BK;
ENABLE_HARDENED_RUNTIME = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Release;
};
C0D9C28F2C5EDFAE0019A067 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 52ZFZKM6BK;
ENABLE_HARDENED_RUNTIME = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
C0D9C2902C5EDFAE0019A067 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
@@ -1184,6 +1332,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 52ZFZKM6BK;
ENABLE_HARDENED_RUNTIME = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
@@ -1195,6 +1344,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 52ZFZKM6BK;
ENABLE_HARDENED_RUNTIME = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
@@ -1266,6 +1416,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C0D9C28E2C5EDFAE0019A067 /* Build configuration list for PBXNativeTarget "appusaged" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C0D9C28F2C5EDFAE0019A067 /* Debug */,
C0D9C2902C5EDFAE0019A067 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C0E993FB2C5BEDAB006FDF44 /* Build configuration list for PBXNativeTarget "removepackages" */ = {
isa = XCConfigurationList;
buildConfigurations = (
+209
View File
@@ -0,0 +1,209 @@
//
// appusage.swift
// munki
//
// Created by Greg Neagle on 8/2/24.
//
import Foundation
func appUsageDBPath() -> String {
// returns path to our appusage DB
// let dbDir = pref("ManagedInstallDir") as? String ?? DEFAULT_MANAGED_INSTALLS_DIR
let dbDir = "/tmp"
return (dbDir as NSString).appendingPathComponent("application_usage.sqlite")
}
class ApplicationUsageRecorder {
// Tracks application launches, activations, and quits.
// Also tracks Munki selfservice install and removal requests.
func _connect(_ databasePath: String = "") throws -> SQL3Connection {
var db = ""
if !databasePath.isEmpty {
db = databasePath
} else {
db = appUsageDBPath()
}
return try SQL3Connection(db)
}
func _close(_ conn: SQL3Connection) {
try? conn.close()
}
func _detect_table(_ conn: SQL3Connection, detectQuery: String) throws -> Bool {
// Detect whether a table exists by trying to perform a query against it
do {
let _ = try SQL3Statement(connection: conn, SQLString: detectQuery)
// don't even have to run the query: if the table doesn't exist, the
// statement prepare will throw an exception
return true
} catch let error as SQL3Error {
if error.description.hasPrefix("Could not prepare statement: no such table:") {
return false
}
throw SQL3Error(error.description)
}
}
func _detect_application_usage_table(_ conn: SQL3Connection) throws -> Bool {
// Detect whether the application usage table exists
let APPLICATION_USAGE_TABLE_DETECT = "SELECT * FROM application_usage LIMIT 1"
return try _detect_table(conn, detectQuery: APPLICATION_USAGE_TABLE_DETECT)
}
func _detect_install_request_table(_ conn: SQL3Connection) throws -> Bool {
// Detect whether the install request table exists
let INSTALL_REQUEST_TABLE_DETECT = "SELECT * FROM install_requests LIMIT 1"
return try _detect_table(conn, detectQuery: INSTALL_REQUEST_TABLE_DETECT)
}
func _create_application_usage_table(_ conn: SQL3Connection) throws {
// Create application usage table when it does not exist
try conn.execute("""
CREATE TABLE application_usage (
event TEXT,
bundle_id TEXT,
app_version TEXT,
app_path TEXT,
last_time INTEGER DEFAULT 0,
number_times INTEGER DEFAULT 0,
PRIMARY KEY (event, bundle_id)
)
""")
}
func _create_install_request_table(_ conn: SQL3Connection) throws {
// Create install request table when it does not exist
try conn.execute("""
CREATE TABLE install_requests (
event TEXT,
item_name TEXT,
item_version TEXT,
last_time INTEGER DEFAULT 0,
number_times INTEGER DEFAULT 0,
PRIMARY KEY (event, item_name)
)
""")
}
func _insert_application_usage(_ conn: SQL3Connection, event: String, appData: [String: String]) throws {
// Insert usage data into application usage table.
// Uses an "upsert" statement so one action either creates a new record
// or updates an existing record
let now = Int(Date().timeIntervalSince1970)
let bundleID = appData["bundle_id"] ?? "UNKNOWN_APP"
let appVersion = appData["version"] ?? "0"
let appPath = appData["path"] ?? ""
let upsert = try SQL3Statement(
connection: conn,
SQLString: """
INSERT INTO application_usage VALUES (
?, ?, ?, ?, ?, 1
)
ON CONFLICT(event, bundle_id) DO UPDATE SET
app_version=excluded.app_version,
app_path=excluded.app_path,
last_time=excluded.last_time,
number_times=number_times+1
"""
)
try upsert.bindText(event, position: 1)
try upsert.bindText(bundleID, position: 2)
try upsert.bindText(appVersion, position: 3)
try upsert.bindText(appPath, position: 4)
try upsert.bindInt64(now, position: 5)
let result = upsert.step()
if result != SQL3Status.done {
throw SQL3Error("Unexpected SQL insert/update result: \(result)")
}
}
func _insert_install_request(_ conn: SQL3Connection, request: [String: String]) throws {
// Insert install request into install request table.
// Uses an "upsert" statement so one action either creates a new record
// or updates an existing record
let now = Int(Date().timeIntervalSince1970)
let event = request["event"] ?? "UNKNOWN_EVENT"
let name = request["name"] ?? "UNKNOWN_ITEM"
let version = request["version"] ?? "0"
let upsert = try SQL3Statement(
connection: conn,
SQLString: """
INSERT INTO install_requests VALUES (
?, ?, ?, ?, 1
)
ON CONFLICT (event, item_name) DO UPDATE SET
item_version=excluded.item_version,
last_time=excluded.last_time,
number_times=number_times+1
"""
)
try upsert.bindText(event, position: 1)
try upsert.bindText(name, position: 2)
try upsert.bindText(version, position: 3)
try upsert.bindInt64(now, position: 4)
let result = upsert.step()
if result != SQL3Status.done {
throw SQL3Error("Unexpected SQL insert/update result: \(result)")
}
}
func _recover_database() {
// TODO: implement this
}
func verify_database() {
// TODO: implement this
}
func log_application_usage(event: String, appData: [String: String]) {
// log application usage and add to database
if appData["bundle_id"] == nil {
// TODO: log.warning "Application object had no bundle_id"
return
}
// TODO: log.debug
/* logging.debug('%s: bundle_id: %s version: %s path: %s', event,
app_dict.get('bundle_id'),
app_dict.get('version'),
app_dict.get('path')) */
do {
let conn = try _connect()
defer { _close(conn) }
if try !_detect_application_usage_table(conn) {
try _create_application_usage_table(conn)
}
try _insert_application_usage(conn, event: event, appData: appData)
} catch {
// TODO: logging.error("Could not add app launch/quit event to database")
}
}
func log_install_request(_ request: [String: String]) {
// log install requests and add to database
if request["event"] == nil || request["name"] == nil {
// TODO: logging.warning("Request dict is missing event or name:")
return
}
// TODO: log.debug
/* logging.debug('%s: name: %s version: %s',
request_dict.get('event'),
request_dict.get('name'),
request_dict.get('version')) */
do {
let conn = try _connect()
defer { _close(conn) }
if try !_detect_install_request_table(conn) {
try _create_install_request_table(conn)
}
try _insert_install_request(conn, request: request)
} catch {
// TODO: logging.error("Could not add install/remove event to database")
}
}
}
+13 -1
View File
@@ -148,10 +148,22 @@ class AsyncProcessRunner {
var results = CLIResults()
var delegate: AsyncProcessDelegate?
init(_ tool: String, arguments: [String] = [], stdIn _: String = "") {
// TODO: implement a timeout in the run() method
init(_ tool: String,
arguments: [String] = [],
environment: [String: String] = [:],
stdIn _: String = "")
{
task.executableURL = URL(fileURLWithPath: tool)
task.arguments = arguments
if !environment.isEmpty {
task.environment = environment
}
// set up input pipe
let inPipe = Pipe()
task.standardInput = inPipe
// set up our stdout and stderr pipes and handlers
let outputPipe = Pipe()
outputPipe.fileHandleForReading.readabilityHandler = { fh in
+4 -4
View File
@@ -21,8 +21,8 @@
import Darwin.C
import Foundation
struct DisplayOptions {
// a Singleton struct to hold shared config values
class DisplayOptions {
// a Singleton class to hold shared config values
// this might eventually be replaced by a more encompassing struct
static let shared = DisplayOptions()
@@ -163,7 +163,7 @@ func displayWarning(_ message: String, addToReport: Bool = true) {
munkiLog(warning, logFile: "warnings.log")
if addToReport {
Report.shared.add(warning, to: "Warnings")
Report.shared.add(string: warning, to: "Warnings")
}
}
@@ -177,6 +177,6 @@ func displayError(_ message: String, addToReport: Bool = true) {
munkiLog(errorMsg, logFile: "errors.log")
if addToReport {
Report.shared.add(errorMsg, to: "Errors")
Report.shared.add(string: errorMsg, to: "Errors")
}
}
+19 -18
View File
@@ -72,37 +72,38 @@ class TempDir {
}
}
func pathExists(_ path: String) -> Bool {
// Returns true if path exists
return FileManager.default.fileExists(atPath: path)
}
func fileType(_ path: String) -> String? {
// FileAttributeType is really a String
return try? (FileManager.default.attributesOfItem(atPath: path) as NSDictionary).fileType()
}
func pathIsRegularFile(_ path: String) -> Bool {
// Returns true if path is a regular file
let filemanager = FileManager.default
do {
let fileType = try (filemanager.attributesOfItem(atPath: path) as NSDictionary).fileType()
if let fileType = fileType(path) {
return fileType == FileAttributeType.typeRegular.rawValue
} catch {
return false
}
return false
}
func pathIsSymlink(_ path: String) -> Bool {
// Returns true if path is a symlink
let filemanager = FileManager.default
do {
let fileType = try (filemanager.attributesOfItem(atPath: path) as NSDictionary).fileType()
if let fileType = fileType(path) {
return fileType == FileAttributeType.typeSymbolicLink.rawValue
} catch {
return false
}
return false
}
func pathIsDirectory(_ path: String) -> Bool {
// Returns true if path is a directory
let filemanager = FileManager.default
do {
let fileType = try (filemanager.attributesOfItem(atPath: path) as NSDictionary).fileType()
if let fileType = fileType(path) {
return fileType == FileAttributeType.typeDirectory.rawValue
} catch {
return false
}
return false
}
func getSizeOfDirectory(_ path: String) -> Int {
@@ -116,7 +117,7 @@ func getSizeOfDirectory(_ path: String) -> Int {
if pathIsRegularFile(fullpath) {
if let attributes = try? filemanager.attributesOfItem(atPath: fullpath) {
let filesize = (attributes as NSDictionary).fileSize()
totalSize += Int(filesize / 1024)
totalSize += Int(filesize)
}
}
}
@@ -126,9 +127,9 @@ func getSizeOfDirectory(_ path: String) -> Int {
func getAbsolutePath(_ path: String) -> String {
// returns absolute path to item referred to by path
if (path as NSString).isAbsolutePath {
return path
return ((path as NSString).standardizingPath as NSString).resolvingSymlinksInPath
}
let cwd = FileManager.default.currentDirectoryPath
let composedPath = (cwd as NSString).appendingPathComponent(path)
return (composedPath as NSString).standardizingPath
return ((composedPath as NSString).standardizingPath as NSString).resolvingSymlinksInPath
}
+383
View File
@@ -0,0 +1,383 @@
//
// core.swift
// munki
//
// Created by Greg Neagle on 8/5/24.
//
import Foundation
func removeCopiedItems(_ itemList: [PlistDict]) -> Bool {
// Removes filesystem items based on info in itemlist.
// These items were typically installed via copy_from_dmg
// This current aborts and returns false on the first error;
// might it make sense to try to continue and remove as much
// as we can?
if itemList.isEmpty {
displayError("Nothing to remove!")
return false
}
for item in itemList {
var itemName = ""
var destinationPath = ""
if let destinationItem = item["destination_item"] as? String {
itemName = (destinationItem as NSString).lastPathComponent
destinationPath = (destinationItem as NSString).deletingLastPathComponent
} else {
itemName = item["source_item"] as? String ?? ""
itemName = (itemName as NSString).lastPathComponent
}
if itemName.isEmpty {
displayError("Missing item name to remove.")
return false
}
if destinationPath.isEmpty,
let providedDestinationPath = item["destination_path"] as? String
{
destinationPath = providedDestinationPath
}
if destinationPath.isEmpty {
displayError("Missing path for item to remove.")
return false
}
let pathToRemove = (destinationPath as NSString).appendingPathComponent(itemName)
if pathExists(pathToRemove) {
displayMinorStatus("Removing \(pathToRemove)")
do {
try FileManager.default.removeItem(atPath: pathToRemove)
} catch let err as NSError {
displayError("Removal error for \(pathToRemove): \(err.localizedDescription)")
return false
} catch {
displayError("Removal error for \(pathToRemove): \(error)")
return false
}
} else {
// pathToRemove doesn't exist. note it, but not an error
displayDetail("Path \(pathToRemove) doesn't exist.")
}
}
return true
}
func itemPrereqsInSkippedItems(currentItem: PlistDict, skippedItems: [PlistDict]) -> [String] {
// Looks for item prerequisites (requires and update_for) in the list
// of skipped items. Returns a list of matches.
var matchedPrereqs = [String]()
if skippedItems.isEmpty {
return matchedPrereqs
}
let name = currentItem["name"] as? String ?? ""
let version = currentItem["version"] as? String ?? ""
displayDebug1("Checking for skipped prerequisites for \(name)-\(version)")
// get list of prerequisites for this item
var prerequisites = currentItem["requires"] as? [String] ?? [String]()
prerequisites += currentItem["update_for"] as? [String] ?? [String]()
if prerequisites.isEmpty {
displayDebug1("\(name)-\(version) has no prerequisites.")
return matchedPrereqs
}
displayDebug1("Prerequisites: \(prerequisites.joined(separator: ", "))")
// build a dictionary of names and versions of skipped items
var skippedItemDict = PlistDict()
for item in skippedItems {
if let name = item["name"] as? String {
let version = item["version_to_install"] as? String ?? "0.0"
let normalizedVersion = trimVersionString(version)
displayDebug1("Adding skipped item: \(name)-\(normalizedVersion)")
var versions = skippedItemDict[name] as? [String] ?? [String]()
versions.append(normalizedVersion)
skippedItemDict[name] = versions
}
}
// now check prereqs against the skipped items
for prereq in prerequisites {
let (pName, pVersion) = nameAndVersion(prereq, onlySplitOnHyphens: true)
displayDebug1("Comparing \(pName)-\(pVersion) against skipped items")
if let versionsForMatchedName = skippedItemDict[pName] as? [String] {
if !pVersion.isEmpty {
let trimmedVersion = trimVersionString(pVersion)
if versionsForMatchedName.contains(trimmedVersion) {
matchedPrereqs.append(prereq)
}
} else {
matchedPrereqs.append(prereq)
}
}
}
return matchedPrereqs
}
func requiresRestart(_ item: PlistDict) -> Bool {
// Returns boolean to indicate if the item needs a restart
let restartAction = item["RestartAction"] as? String ?? ""
return ["RequireRestart", "RecommendRestart"].contains(restartAction)
}
func handleApplePackageInstall(pkginfo: PlistDict, itemPath: String) async -> (Int, Bool) {
// Process an Apple package for install. Returns retcode, needs_restart
if let suppressBundleRelocation = pkginfo["suppress_bundle_relocation"] as? Bool {
displayWarning("Item has 'suppress_bundle_relocation' attribute. This feature is no longer supported.")
}
if hasValidDiskImageExt(itemPath) {
let dmgName = (itemPath as NSString).lastPathComponent
displayMinorStatus("Mounting disk image \(dmgName)")
guard let mountpoint = try? mountdmg(itemPath, skipVerification: true) else {
let dmgPath = pkginfo["installer_item"] as? String ?? dmgName
displayError("Could not mount disk image file \(dmgPath)")
return (-99, false)
}
defer { unmountdmg(mountpoint) }
if let pkgPath = pkginfo["package_path"] as? String,
hasValidPackageExt(pkgPath)
{
// admin has specified the relative path of the pkg on the DMG
// this is useful if there is more than one pkg on the DMG,
// or the actual pkg is not at the root of the DMG
let fullPkgPath = (mountpoint as NSString).appendingPathComponent(pkgPath)
if pathExists(fullPkgPath) {
let (retcode, needToRestart) = await install(pkgPath, options: pkginfo)
return (retcode, needToRestart || requiresRestart(pkginfo))
} else {
displayError("Did not find \(pkgPath) on disk image \(dmgName)")
return (-99, false)
}
} else {
// no relative path to pkg on dmg, so just install first
// pkg found at the root of the mountpoint
// (hopefully there's only one)
let (retcode, needToRestart) = await installFromDirectory(mountpoint, options: pkginfo)
return (retcode, needToRestart || requiresRestart(pkginfo))
}
} else if hasValidPackageExt(itemPath) {
let (retcode, needToRestart) = await install(itemPath, options: pkginfo)
return (retcode, needToRestart || requiresRestart(pkginfo))
}
// we didn't find anything we know how to install
munkiLog("Found nothing we know how to install in \(itemPath)")
return (-99, false)
}
// TODO: break this long confusing function up
func installWithInstallInfo(
cachePath: String,
installList: [PlistDict],
onlyUnattended: Bool = false
) async -> (Bool, [PlistDict]) {
// Uses the installInfo installs list to install items in the
// correct order and with additional options
var restartFlag = false
var itemIndex = 0
var skippedInstalls = [PlistDict]()
for item in installList {
// Keep track of when this particular install started.
let startTime = Date()
itemIndex += 1
let installerType = item["installer_type"] as? String ?? "pkg_install"
let itemName = item["name"] as? String ?? "<unknown>"
if installerType == "startosinstall" {
skippedInstalls.append(item)
displayDebug1("Skipping install of \(itemName) because it's a startosinstall item. Will install later.")
continue
}
if onlyUnattended {
let unattendedInstall = item["unattended_install"] as? Bool ?? false
if !unattendedInstall {
skippedInstalls.append(item)
displayDetail("Skipping install of \(itemName) because it's not unattended, and we can only do unattended installs at this time.")
continue
}
if blockingApplicationsRunning(item) {
skippedInstalls.append(item)
displayDetail("Skipping unattended install of \(itemName) because blocking applications are running.")
continue
}
}
let skippedPrereqs = itemPrereqsInSkippedItems(currentItem: item, skippedItems: skippedInstalls)
if !skippedPrereqs.isEmpty {
// one or more prerequisite for this item was skipped or failed;
// need to skip this item too
skippedInstalls.append(item)
var skipActionText = "not installed"
if onlyUnattended {
skipActionText = "skipped"
}
displayDetail("Skipping unattended install of \(itemName) because these prerequisites were \(skipActionText): \(skippedPrereqs.joined(separator: ", "))")
continue
}
// TODO: implement processes.stop_requested()
// return (restartflag, skipped_installs)
let displayName = item["display_name"] as? String ?? item["name"] as? String ?? "<unknown>"
let versionToInstall = item["version_to_install"] as? String ?? ""
let installerItem = item["installer_item"] as? String ?? ""
let installerItemPath = (cachePath as NSString).appendingPathComponent(installerItem)
displayMajorStatus("Installing \(displayName) (\(itemIndex) of \(installList.count))")
var retcode = 0
if item["preinstall_script"] is String {
retcode = await runEmbeddedScript(name: "preinstall_script", pkginfo: item)
}
if retcode == 0 {
if installerType != "nopkg" {
if installerItem.isEmpty {
displayError("Item \(installerItem) has no defined installer_item. Skipping.")
retcode = -99
}
if !pathExists(installerItemPath) {
// can't install, so we should stop. Since later items might
// depend on this one, we shouldn't continue
displayError("Installer item \(installerItem) was not found. Skipping.")
retcode = -99
}
}
switch installerType {
case "pkg_install":
let (result, restartNeeded) = await handleApplePackageInstall(pkginfo: item, itemPath: installerItemPath)
retcode = result
if restartNeeded {
restartFlag = true
}
case "copy_from_dmg":
if let itemList = item["items_to_copy"] as? [PlistDict] {
retcode = await copyFromDmg(dmgPath: installerItemPath, itemList: itemList)
if retcode == 0, requiresRestart(item) {
restartFlag = true
}
}
case "stage_os_installer":
if let itemList = item["items_to_copy"] as? [PlistDict] {
retcode = await copyFromDmg(dmgPath: installerItemPath, itemList: itemList)
if retcode == 0 {
// TODO: implement osinstaller.record_staged_os_installer
// osinstaller.record_staged_os_installer(item)
}
}
case "nopkg":
restartFlag = restartFlag || requiresRestart(item)
default:
// unknown or no longer supported installer type
if ["appdmg", "profiles"].contains(installerType) || installerType.hasPrefix("Adobe") {
displayError("Installer type '\(installerType)' for \(installerItem) is no longer supported.")
} else {
displayError("Installer type '\(installerType)' for \(installerItem) is an unknown installer type.")
}
retcode = -99
}
}
// If install failed, add to skippedInstalls so that any item later in the list
// that requires this item is skipped as well.
if retcode != 0 {
skippedInstalls.append(item)
}
// if install succeeded, look for a postinstall_script to run
if retcode == 0, item["postinstall_script"] is String {
let scriptexit = await runEmbeddedScript(name: "postinstall_script", pkginfo: item)
if scriptexit != 0 {
// we won't consider postinstall script failures as fatal
// since the item has been installed via package/disk image
// but admin should be notified
displayWarning("Postinstall script for \(itemName) returned \(scriptexit)")
}
}
// if install was successful and this is a SelfService OnDemand install
// remove the item from the SelfServeManifest's managed_installs
if retcode == 0,
item["OnDemand"] as? Bool ?? false
{
// TODO: manifestutils.remove_from_selfserve_installs(item['name'])
}
// log install success/failure
var logMessage = "Install of \(displayName)-\(versionToInstall): "
if retcode == 0 {
logMessage += "SUCCESSFUL"
} else {
logMessage += "FAILED with return code: \(retcode)"
}
munkiLog(logMessage, logFile: "Install.log")
// Calculate install duration; note, if a machine is put to sleep
// during the install this time may be inaccurate.
let installDuration = Int(Date().timeIntervalSince(startTime))
let downloadSpeed = item["download_kbytes_per_sec"] as? Int ?? 0
// add install result to report object
let installResult: PlistDict = [
"display_name": displayName,
"name": item["name"] as? String ?? "<unknown>",
"version": versionToInstall,
"applesus": false,
"status": retcode,
"time": Date(),
"duration_seconds": installDuration,
"download_kbytes_per_sec": downloadSpeed,
"unattended": onlyUnattended,
]
Report.shared.add(dict: installResult, to: "InstallResults")
// check to see if this installer item is needed by any additional
// items in installinfo
// this might happen if there are multiple things being installed
// with choicesXML files applied to a distribution package or
// multiple packages being installed from a single DMG
var stillNeeded = false
let currentInstallerItem = installerItem
// are we at the end of the installlist?
// (we already incremented itemindex for display
// so with zero-based arrays itemindex now points to the item
// after the current item)
if itemIndex < installList.count {
// there are remaining items, let's check them
for laterItem in installList[itemIndex...] {
let laterInstallerItem = laterItem["installer_item"] as? String ?? ""
if laterInstallerItem == currentInstallerItem {
stillNeeded = true
break
}
}
}
// need to check skipped_installs as well
if !stillNeeded {
for skippedItem in skippedInstalls {
let skippedInstallerItem = skippedItem["installer_item"] as? String ?? ""
if skippedInstallerItem == currentInstallerItem {
stillNeeded = true
break
}
}
}
// check to see if the item is both precache and OnDemand
let precache = item["precache"] as? Bool ?? false
let onDemand = item["OnDemand"] as? Bool ?? false
if !stillNeeded, precache, onDemand {
// keep precached OnDemand items in the cache indefinitely
stillNeeded = true
}
// cleanup unneeded install_items
if !stillNeeded, retcode == 0 {
// remove the item from the install cache
// (if it's still there)
if pathExists(installerItemPath) {
try? FileManager.default.removeItem(atPath: installerItemPath)
if hasValidDiskImageExt(installerItemPath) {
let shadowFile = installerItemPath + ".shadow"
if pathExists(shadowFile) {
try? FileManager.default.removeItem(atPath: shadowFile)
}
}
}
}
}
return (restartFlag, skippedInstalls)
}
+288
View File
@@ -0,0 +1,288 @@
//
// dmg.swift
// munki
//
// Created by Greg Neagle on 8/4/24.
//
import Foundation
func setPermissions(_ itemInfo: PlistDict, path: String) -> Int {
// Sets owner, group and mode for path from info in itemInfo.
// Returns 0 on success, non-zero otherwise.
// Yes, we could call FileManager methods like setAttributes(_:ofItemAtPath:),
// But the user and group might be names or numbers, and the mode is
// supported to be symbolic (but could also be in the format of "777",
// So we're just going to call `chown` and `chmod`. This also allow us to easily
// set these attributes recursively.
// set owner and group
let user = itemInfo["user"] as? String ?? "root"
let group = itemInfo["group"] as? String ?? "admin"
displayDetail("Setting owner and group for '\(path)' to '\(user):\(group)'")
let chownResult = runCLI("/usr/sbin/chown", arguments: ["-R", user + ":" + group, path])
if chownResult.exitcode != 0 {
displayError("Error setting owner and group for \(path): (\(chownResult.exitcode)) \(chownResult.error)")
return chownResult.exitcode
}
// set mode
let mode = itemInfo["mode"] as? String ?? "o-w,go+rX"
displayDetail("Setting mode for '\(path)' to '\(mode)'")
let chmodResult = runCLI("/bin/chmod", arguments: ["-R", mode, path])
if chmodResult.exitcode != 0 {
displayError("Error setting mode for \(path): \(chmodResult.error)")
return chownResult.exitcode
}
// success!
return 0
}
func createMissingDirs(_ path: String) -> Bool {
// Creates any missing intermediate directories so we can copy item.
// Returns boolean to indicate success or failure
let filemanager = FileManager.default
if filemanager.fileExists(atPath: path) {
// the path exists; don't need to create anything
return true
}
var parentPath = path
// find a parent path that actually exists
while !filemanager.fileExists(atPath: parentPath) {
parentPath = (parentPath as NSString).deletingLastPathComponent
}
// get the owner, group and mode of this directory
do {
let attrs = try filemanager.attributesOfItem(atPath: parentPath)
let user = (attrs as NSDictionary).fileOwnerAccountID() ?? NSNumber(0)
let group = (attrs as NSDictionary).fileGroupOwnerAccountID() ?? NSNumber(0)
var mode = (attrs as NSDictionary).filePosixPermissions()
if mode == 0 {
mode = 0o755
}
let preservedAttrs = [
FileAttributeKey.ownerAccountID: user,
FileAttributeKey.groupOwnerAccountID: group,
FileAttributeKey.posixPermissions: mode,
] as [FileAttributeKey: Any]
try filemanager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: preservedAttrs)
return true
} catch {
displayError("Error creating path \(path): \(error.localizedDescription)")
return false
}
}
func removeQuarantineXattrFromItem(_ path: String) {
// Removes com.apple.quarantine xattr from a path
do {
let xattrs = try listXattrs(atPath: path)
if xattrs.contains("com.apple.quarantine") {
try removeXattr("com.apple.quarantine", atPath: path)
}
} catch let err as MunkiError {
displayWarning("\(err.description)")
} catch {
displayWarning("\(error)")
}
}
func removeQuarantineXattrsRecursively(_ path: String) {
// Removes com.apple.quarantine xattr from a path, recursively if needed
removeQuarantineXattrFromItem(path)
if pathIsDirectory(path) {
let dirEnum = FileManager.default.enumerator(atPath: path)
while let item = dirEnum?.nextObject() as? String {
let itempath = (path as NSString).appendingPathComponent(item)
removeQuarantineXattrFromItem(itempath)
}
}
}
func validateSourceAndDestination(mountpoint: String, item: PlistDict) -> (Bool, String, String) {
// Validates source and destination for item to be copied from a mounted
// disk image.
// Returns a tuple of (success, source_path, destination_path)
// Ensure source item is defined
guard let sourceItemName = item["source_item"] as? String else {
displayError("Missing name of item to copy!")
return (false, "", "")
}
// Ensure source item exists
let sourceItemPath = (mountpoint as NSString).appendingPathComponent(sourceItemName)
if !pathExists(sourceItemPath) {
displayError("Source item \(sourceItemName) does not exist!")
return (false, "", "")
}
// get destination path and name
var destinationPath = item["destination_path"] as? String ?? ""
var destinationItemName = item["destination_item"] as? String ?? ""
if destinationPath.isEmpty {
destinationPath = item["destination_item"] as? String ?? ""
if !destinationPath.isEmpty {
destinationItemName = (destinationPath as NSString).lastPathComponent
destinationPath = (destinationPath as NSString).deletingLastPathComponent
}
}
if destinationPath.isEmpty {
// fatal!
displayError("Missing destination path for item!")
return (false, "", "")
}
// create any needed intermediate directories for the destpath or fail
if !createMissingDirs(destinationPath) {
return (false, "", "")
}
// setup full destination path using 'destination_item', if supplied,
// source_item if not.
var fullDestinationPath = ""
if destinationItemName.isEmpty {
fullDestinationPath = (destinationPath as NSString).appendingPathComponent(sourceItemName)
} else {
fullDestinationPath = (destinationPath as NSString).appendingPathComponent(destinationItemName)
}
return (true, sourceItemPath, fullDestinationPath)
}
func getSize(_ path: String) -> Int {
// Recursively gets size of pathname in bytes
if pathIsDirectory(path) {
return getSizeOfDirectory(path)
}
if pathIsRegularFile(path) {
if let attributes = try? FileManager.default.attributesOfItem(atPath: path) {
return Int((attributes as NSDictionary).fileSize())
}
}
return 0
}
class DittoRunner: AsyncProcessRunner {
// subclass of AsyncProcessRunner that handles the progress output from
// /usr/bin/ditto
var remainingErrorOutput = ""
var totalBytesCopied = 0
var sourceItemSize = 1
init(sourcePath: String, destinationPath: String) {
let tool = "/usr/bin/ditto"
let arguments = ["-V", "--noqtn", sourcePath, destinationPath]
sourceItemSize = getSize(sourcePath)
super.init(tool, arguments: arguments)
}
func linesAndRemainderOf(_ str: String) -> ([String], String) {
var lines = str.components(separatedBy: "\n")
var remainder = ""
if lines.count > 0, !str.hasSuffix("\n") {
// last line of string did not end with a newline; might be a partial
remainder = lines.last ?? ""
lines.removeLast()
}
return (lines, remainder)
}
// ditto -V progress output goes to stderr(!)
override func processError(_ str: String) {
super.processError(str)
let (lines, remainder) = linesAndRemainderOf(remainingErrorOutput + str)
remainingErrorOutput = remainder
for line in lines {
let words = line.components(separatedBy: .whitespaces)
if words.count > 1, words[1] == "bytes" {
let bytesCopied = Int(words[0]) ?? 0
totalBytesCopied += bytesCopied
displayPercentDone(current: totalBytesCopied, maximum: sourceItemSize)
}
}
}
}
func dittoWithProgress(sourcePath: String, destinationPath: String) async -> Int {
// Uses ditto to copy an item and provides progress output
let proc = DittoRunner(sourcePath: sourcePath, destinationPath: destinationPath)
await proc.run()
return proc.results.exitcode
}
func copyItemsFromMountpoint(_ mountpoint: String, itemList: [PlistDict]) async -> Int {
// copies items from the mountpoint to the startup disk
// Returns 0 if no issues; some error code otherwise.
guard let tempDestinationDir = TempDir.shared.makeTempDir() else {
displayError("Could not create a temporary directory!")
return -1
}
for item in itemList {
let (success, sourcePath, destinationPath) = validateSourceAndDestination(mountpoint: mountpoint, item: item)
if !success {
return -1
}
// validation passed, OK to copy
displayMinorStatus("Copying \((sourcePath as NSString).lastPathComponent) to \(destinationPath)")
let tempDestinationPath = (tempDestinationDir as NSString).appendingPathComponent((destinationPath as NSString).lastPathComponent)
// copy the file or directory, removing the quarantine xattr and
// preserving HFS+ compression
let dittoresult = await dittoWithProgress(sourcePath: sourcePath, destinationPath: tempDestinationPath)
if dittoresult != 0 {
displayError("Error copying \(sourcePath) to \(tempDestinationPath)")
return dittoresult
}
// remove com.apple.quarantine xattr since `man ditto` lies and doesn't
// seem to actually always remove it
removeQuarantineXattrsRecursively(tempDestinationPath)
// set desired permissions for item
let permsresult = setPermissions(item, path: tempDestinationPath)
if permsresult != 0 {
// setPermissions already displayed an error
return permsresult
}
// remove any previously exiting item at destinationPatj
if pathExists(destinationPath) {
do {
try FileManager.default.removeItem(atPath: destinationPath)
} catch let err as NSError {
displayError("Error removing existing item at destination: \(err.localizedDescription)")
return -1
} catch {
displayError("Error removing existing item at destination: \(error)")
return -1
}
}
// move tempDestinationPath to final destination path
do {
try FileManager.default.moveItem(atPath: tempDestinationPath, toPath: destinationPath)
} catch let err as NSError {
displayError("Error moving item to destination: \(err.localizedDescription)")
return -1
} catch {
displayError("Error moving item to destination: \(error)")
return -1
}
}
// all items were copied successfully, clean up
try? FileManager.default.removeItem(atPath: tempDestinationDir)
return 0
}
func copyFromDmg(dmgPath: String, itemList: [PlistDict]) async -> Int {
// Copies items from disk image to local disk
if itemList.isEmpty {
displayError("No items to copy!")
return -1
}
displayMinorStatus("Mounting disk image \((dmgPath as NSString).lastPathComponent)")
if let mountpoint = try? mountdmg(dmgPath, skipVerification: true) {
let retcode = await copyItemsFromMountpoint(mountpoint, itemList: itemList)
if retcode == 0 {
displayMinorStatus("The software was successfully installed.")
}
unmountdmg(mountpoint)
return retcode
} else {
displayError("Could not mount disk image file \((dmgPath as NSString).lastPathComponent)")
return -1
}
}
+265
View File
@@ -0,0 +1,265 @@
//
// pkg.swift
// munki
//
// Created by Greg Neagle on 8/3/24.
//
// Copyright 2024 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 Foundation
func removeBundleRelocationInfo() {
// this function existed in the Python version of Munki
// but could work only on bundle-style packages
// May not be worth porting.
displayWarning("'suppress_bundle_relocation' is no longer supported. Ignoring.")
}
func pkgNeedsRestart(_ pkgpath: String, options: PlistDict) -> Bool {
// Query a package for its RestartAction. Returns true if a restart is
// needed, false otherwise
let tool = "/usr/sbin/installer"
var arguments = ["-query", "RestartAction", "-pkg", pkgpath, "-plist"]
if let choicesXML = options["installer_choices_xml"] as? PlistDict,
let tempDir = TempDir.shared.path
{
let choicesXMLPath = (tempDir as NSString).appendingPathComponent("choices.xml")
do {
try writePlist(choicesXML, toFile: choicesXMLPath)
arguments += ["-applyChoiceChangesXML", choicesXMLPath]
} catch {}
}
if let allowUntrusted = options["allow_untrusted"] as? Bool,
allowUntrusted == true
{
arguments.append("-allowUntrusted")
}
let result = runCLI(tool, arguments: arguments)
if result.exitcode != 0 {
displayWarning("/usr/bin/installer error when getting restart info for \((pkgpath as NSString).lastPathComponent): \(result.error)")
return false
}
let (pliststr, _) = parseFirstPlist(fromString: result.output)
if !pliststr.isEmpty,
let plist = try? readPlist(fromString: pliststr) as? PlistDict,
let restartAction = plist["RestartAction"] as? String
{
return ["RequireRestart", "RecommendRestart"].contains(restartAction)
}
displayWarning("/usr/bin/installer returned unexpected value when getting restart info for \((pkgpath as NSString).lastPathComponent): \(result.output)")
return false
}
func getInstallerEnvironment(_ customEnv: [String: String]?) -> [String: String] {
var env = ProcessInfo.processInfo.environment
env["USER"] = NSUserName()
env["HOME"] = NSHomeDirectory()
if let customEnv {
for (key, value) in customEnv {
if key == "USER", value == "CURRENT_CONSOLE_USER" {
var consoleUser = getConsoleUser()
if consoleUser == "" || consoleUser == "loginwindow" {
consoleUser = "root"
}
env["USER"] = consoleUser
env["HOME"] = NSHomeDirectoryForUser(consoleUser)
} else {
env[key] = value
}
}
displayDebug1("Using custom installer environment variables: \(env)")
}
return env
}
func displayInstallerOutput(_ text: String) {
// Parses a line of output from installer, displays it as progress output
// and logs it
if !text.hasPrefix("installer:") {
// this should not have been sent to this function!
return
}
var msg = text.trimmingCharacters(in: .newlines)
// delete "installer:" prefix
msg.removeFirst("installer:".count)
if msg.hasPrefix("PHASE:") {
msg.removeFirst("PHASE:".count)
if !msg.isEmpty {
displayMinorStatus(msg)
}
} else if msg.hasPrefix("STATUS:") {
msg.removeFirst("STATUS:".count)
if !msg.isEmpty {
displayMinorStatus(msg)
}
} else if msg.hasPrefix("%") {
msg.removeFirst()
if let percent = Double(msg) {
// TODO: munkistatus.percent(percent)
displayMinorStatus("\(msg) percent complete")
}
} else if msg.hasPrefix(" Error") || msg.hasPrefix(" Cannot install") {
displayError(msg)
// TODO: munkistatus.detail(msg)
} else {
munkiLog(msg)
}
}
class installerRunner: AsyncProcessRunner {
// subclass of AsyncProcessRunner that handles the progress output from
// /usr/sbin/installer
var remainingOutput = ""
var lastProcessedOutputLine = ""
override init(_ tool: String = "/usr/sbin/installer",
arguments: [String] = [],
environment: [String: String] = [:],
stdIn: String = "")
{
super.init(tool, arguments: arguments, environment: environment, stdIn: stdIn)
}
func linesAndRemainderOf(_ str: String) -> ([String], String) {
var lines = str.components(separatedBy: "\n")
var remainder = ""
if lines.count > 0, !str.hasSuffix("\n") {
// last line of string did not end with a newline; might be a partial
remainder = lines.last ?? ""
lines.removeLast()
}
return (lines, remainder)
}
override func processOutput(_ str: String) {
super.processOutput(str)
let (lines, remainder) = linesAndRemainderOf(remainingOutput + str)
remainingOutput = remainder
for line in lines {
if line == lastProcessedOutputLine {
// if the output line is the same, just skip
continue
} else if line.hasPrefix("installer:") {
displayInstallerOutput(line)
lastProcessedOutputLine = line
}
}
}
}
func runInstaller(arguments: [String], environment: [String: String], pkgName: String) async -> Int {
// Runs /usr/sbin/installer, parses and displays the output, and returns
// the process exit code
let proc = installerRunner(arguments: arguments, environment: environment)
await proc.run()
let results = proc.results
if results.exitcode != 0 {
displayMinorStatus("Install of \(pkgName) failed with return code \(results.exitcode)")
displayError(String(repeating: "-", count: 78))
for line in results.output.components(separatedBy: "\n") {
displayError(line)
}
for line in results.error.components(separatedBy: "\n") {
displayError(line)
}
displayError(String(repeating: "-", count: 78))
}
return results.exitcode
}
func install(_ pkgpath: String, options: PlistDict = [:]) async -> (Int, Bool) {
// Uses the Apple installer to install the package or metapackage at pkgpath.
// Returns a tuple:
// the installer return code and restart needed as a boolean.
var restartNeeded = false
let packageName = (pkgpath as NSString).lastPathComponent
let displayName = options["display_name"] as? String ?? options["name"] as? String ?? packageName
var resolvedPkgPath = pkgpath
if pathIsSymlink(pkgpath) {
resolvedPkgPath = getAbsolutePath(pkgpath)
}
if let suppressBundleRelocation = options["suppress_bundle_relocation"] as? Bool, suppressBundleRelocation == true {
removeBundleRelocationInfo()
}
munkiLog("Installing \(displayName) from \(packageName)")
if pkgNeedsRestart(resolvedPkgPath, options: options) {
displayMinorStatus("\(displayName) requires a restart after installation.")
restartNeeded = true
}
var arguments = ["-verboseR", "-pkg", resolvedPkgPath, "-target", "/"]
if let choicesXML = options["installer_choices_xml"] as? PlistDict,
let tempDir = TempDir.shared.path
{
// choices_xml_file was already built by pkgNeedsRestart(),
// just re-use it
let choicesXMLPath = (tempDir as NSString).appendingPathComponent("choices.xml")
arguments += ["-applyChoiceChangesXML", choicesXMLPath]
}
if let allowUntrusted = options["allow_untrusted"] as? Bool,
allowUntrusted == true
{
arguments.append("-allowUntrusted")
}
// get installer environment
let envVars = getInstallerEnvironment(options["installer_environment"] as? [String: String])
// run it
let retcode = await runInstaller(arguments: arguments, environment: envVars, pkgName: packageName)
if retcode != 0 {
restartNeeded = false
}
return (retcode, restartNeeded)
}
func installFromDirectory(_ directoryPath: String, options: PlistDict = [:]) async -> (Int, Bool) {
// The Python version of Munki would actually install _all_ the pkgs from a given
// directory (which was usually the root of a mounted disk image. This was rarely
// was was actaully wanted. This version just installs the first installable item in
// the directory.
// Returns a tuple containing the exit code of the installer process and a boolean
// indicating if a restart is needed
if let items = try? FileManager.default.contentsOfDirectory(atPath: directoryPath) {
for item in items {
// TODO: support user stop requests
let itempath = (directoryPath as NSString).appendingPathComponent(item)
if hasValidDiskImageExt(item) {
displayInfo("Mounting disk image \(item)")
guard let mountpoint = try? mountdmg(itempath, useShadow: true, skipVerification: true) else {
displayError("No filesystems mounted from \(item)")
return (-1, false)
}
// makre sure we unmount this when done
defer { unmountdmg(mountpoint) }
// call us recursively to install a pkg at the root of this diskimage
return await installFromDirectory(mountpoint, options: options)
}
if hasValidPackageExt(item) {
return await install(itempath, options: options)
}
}
}
// if we get here, no valid items to install were found
displayWarning("No items to install were found in \(directoryPath)")
return (-1, false)
}
+59
View File
@@ -0,0 +1,59 @@
//
// launchd.swift
// munki
//
// Created by Greg Neagle on 8/2/24.
//
// Copyright 2024 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 Darwin.C
import Foundation
func getSocketFd(_ socketName: String) throws -> [CInt] {
// Retrieve named socket file descriptors from launchd.
var fdsCount = 0
var fds = UnsafeMutablePointer<CInt>.allocate(capacity: 0)
let originalFds = fds
let err = launch_activate_socket(
socketName,
&fds,
&fdsCount
)
if err != 0 {
originalFds.deallocate()
var errorDescription = ""
switch err {
case ENOENT:
errorDescription = "The socket name specified does not exist in the caller's launchd.plist"
case ESRCH:
errorDescription = "The calling process is not managed by launchd"
case EALREADY:
errorDescription = "The specified socket has already been activated"
default:
let errStr = String(cString: strerror(err))
errorDescription = "Error \(errStr)"
}
throw MunkiError("Failed to retrieve sockets from launchd: \(errorDescription)")
}
// make sure we clean up these allocations
defer { fds.deallocate() }
defer { originalFds.deallocate() }
// fds is now a pointer to a list of filedescriptors. Transform into Swift array
let outputFds = UnsafeMutableBufferPointer<CInt>(
start: fds,
count: Int(fdsCount)
)
return [CInt](outputFds)
}
+1 -1
View File
@@ -182,7 +182,7 @@ func makeStageOSInstallerPkgInfo(_ appPath: String) throws -> PlistDict {
// describing a stage_os_installer item
// calculate the size of the installer app
let appSize = getSizeOfDirectory(appPath)
let appSize = getSizeOfDirectory(appPath) / 1024 // this value is kbytes
let appName = (appPath as NSString).lastPathComponent
let appInfo = try getInfoFromInstallMacOSApp(appPath)
guard let version = appInfo["version"] as? String else {
+6
View File
@@ -19,6 +19,7 @@
// limitations under the License.
import Foundation
import SystemConfiguration
func getOSVersion(onlyMajorMinor: Bool = true) -> String {
let version = ProcessInfo().operatingSystemVersion
@@ -29,3 +30,8 @@ func getOSVersion(onlyMajorMinor: Bool = true) -> String {
return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
}
}
func getConsoleUser() -> String {
// Return console user
return SCDynamicStoreCopyConsoleUser(nil, nil, nil) as? String ?? ""
}
+79
View File
@@ -0,0 +1,79 @@
//
// processes.swift
// munki
//
// Created by Greg Neagle on 8/5/24.
//
import Foundation
func getRunningProcesses() -> [String] {
// Returns a list of paths of running processes
let result = runCLI("/bin/ps", arguments: ["-axo", "comm="])
if result.exitcode == 0 {
return result.output.components(separatedBy: .newlines).filter { $0.hasPrefix("/") }
}
return [String]()
}
func isAppRunning(_ appName: String) -> Bool {
// Tries to determine if the application in appname is currently
// running
displayDetail("Checking if \(appName) is running...")
let procList = getRunningProcesses()
var matchingItems = [String]()
if appName.hasPrefix("/") {
// search by exact path
matchingItems = procList.filter { $0 == appName }
} else if appName.hasSuffix(".app") {
// search by filename
let searchName = "/" + appName + "/Contents/MacOS/"
matchingItems = procList.filter { $0.contains(searchName) }
} else {
// check executable name
matchingItems = procList.filter { $0.hasSuffix("/" + appName) }
}
if matchingItems.isEmpty {
// try adding '.app' to the name and check again
let searchName = "/" + appName + ".app/Contents/MacOS/"
matchingItems = procList.filter { $0.contains(searchName) }
}
if !matchingItems.isEmpty {
// it's running!
displayDebug1("Matching process list: \(matchingItems)")
displayDebug1("\(appName) is running!")
return true
}
// if we get here, we have no evidence that appname is running
return false
}
func blockingApplicationsRunning(_ pkginfo: PlistDict) -> Bool {
// Returns true if any application in the blocking_applications list
// is running or, if there is no blocking_applications list, if any
// application in the installs list is running.
var appNames = [String]()
if let blockingApplications = pkginfo["blocking_applications"] as? [String] {
appNames = blockingApplications
} else {
// if no blocking_applications specified, get appnames
// from 'installs' list if it exists
if let installs = pkginfo["installs"] as? [PlistDict] {
let apps = installs.filter {
$0["type"] as? String ?? "" == "application"
}
appNames = apps.map {
($0["path"] as? NSString)?.lastPathComponent ?? ""
}.filter { !$0.isEmpty }
}
}
displayDebug1("Checking for \(appNames)")
let runningApps = appNames.filter { isAppRunning($0) }
if !runningApps.isEmpty {
let itemName = pkginfo["name"] as? String ?? "<unknown>"
displayDetail("Blocking apps for \(itemName) are running:")
displayDetail(" \(runningApps)")
return true
}
return false
}
+11 -2
View File
@@ -34,12 +34,21 @@ class Report {
report[key] = value
}
func add(_ newValue: String, to key: String) {
func add(string newValue: String, to key: String) {
if var value = report[key] as? [String] {
value.append(newValue)
report[key] = value
} else {
report[key] = newValue
report[key] = [newValue]
}
}
func add(dict newValue: PlistDict, to key: String) {
if var value = report[key] as? [PlistDict] {
value.append(newValue)
report[key] = value
} else {
report[key] = [newValue]
}
}
+105
View File
@@ -0,0 +1,105 @@
//
// scriptutils.swift
// munki
//
// Created by Greg Neagle on 8/5/24.
//
import Foundation
func createExecutableFile(atPath path: String, withStringContents stringContents: String) -> Bool {
// Writes string data to path.
// Returns success or failure as a boolean.
let data = stringContents.data(using: .utf8)
return FileManager.default.createFile(
atPath: path,
contents: data,
attributes: [FileAttributeKey.posixPermissions: 0o700]
)
}
class ScriptRunner: AsyncProcessRunner {
var remainingOutput = ""
func linesAndRemainderOf(_ str: String) -> ([String], String) {
var lines = str.components(separatedBy: "\n")
var remainder = ""
if lines.count > 0, !str.hasSuffix("\n") {
// last line of string did not end with a newline; might be a partial
remainder = lines.last ?? ""
lines.removeLast()
}
return (lines, remainder)
}
override func processOutput(_ str: String) {
super.processOutput(str)
let (lines, remainder) = linesAndRemainderOf(remainingOutput + str)
remainingOutput = remainder
for line in lines {
displayInfo(line)
}
}
}
func runScript(_ path: String, itemName: String, scriptName: String, suppressError: Bool = false) async -> Int {
// Runs a script, Returns return code.
if suppressError {
displayDetail("Running \(scriptName) for \(itemName)")
} else {
displayMinorStatus("Running \(scriptName) for \(itemName)")
}
if DisplayOptions.shared.munkistatusoutput {
// TODO: munkistatus
// set indeterminate progress bar
// munkistatus.percent(-1)
}
let proc = ScriptRunner(path)
await proc.run()
let result = proc.results
if result.exitcode != 0, !suppressError {
displayError("Running \(scriptName) for \(itemName) failed.")
displayError(String(repeating: "-", count: 78))
for line in result.output.components(separatedBy: .newlines) {
displayError(" " + line)
}
displayError(String(repeating: "-", count: 78))
} else if !suppressError {
munkiLog("Running \(scriptName) for \(itemName) was successful.")
}
if DisplayOptions.shared.munkistatusoutput {
// TODO: munkistatus
// clear indeterminate progress bar
// munkistatus.percent(0)
}
return result.exitcode
}
func runEmbeddedScript(name: String, pkginfo: PlistDict, suppressError: Bool = false) async -> Int {
// Runs a script embedded in the pkginfo.
// Returns the result code.
// get the script text
let itemName = pkginfo["name"] as? String ?? "<unknown>"
guard let scriptText = pkginfo[name] as? String else {
displayError("Missing script \(name) for \(itemName)")
return -1
}
// write the script to a temp file
guard let tempdir = TempDir.shared.makeTempDir() else {
displayError("Could not create a temporary directory for \(name)")
return -1
}
let scriptPath = (tempdir as NSString).appendingPathComponent(name)
if createExecutableFile(atPath: scriptPath, withStringContents: scriptText) {
return await runScript(scriptPath, itemName: itemName, scriptName: name, suppressError: suppressError)
} else {
displayError("Failed to create executable file for \(name)")
return -1
}
}
@@ -1,5 +1,5 @@
//
// socketclient.swift
// socket/client.swift
//
// Created by Greg Neagle on 7/23/18.
// Copyright © 2018-2024 The Munki Project. All rights reserved.
@@ -40,26 +40,6 @@ class UNIXDomainSocketClient {
}
}
private func addrRefCreate(_ path: String) -> CFData? {
// make a sockaddr struct (this is ugly), wrap it in a CFData obj
var socketAdr = sockaddr_un()
socketAdr.sun_family = sa_family_t(AF_UNIX)
socketAdr.sun_len = __uint8_t(MemoryLayout<sockaddr_un>.size)
if var cstring_path = path.cString(using: .utf8) {
if cstring_path.count > MemoryLayout.size(
ofValue: socketAdr.sun_path)
{
// path is too long for this 1970s era struct
return nil
}
memcpy(&socketAdr.sun_path, &cstring_path, cstring_path.count)
} else {
return nil
}
return NSData(bytes: &socketAdr,
length: MemoryLayout.size(ofValue: socketAdr)) as CFData
}
func connect(to path: String) {
// Create a UNIX domain socket object and connect
//
+29
View File
@@ -0,0 +1,29 @@
//
// socket/common.swift
// munki
//
// Created by Greg Neagle on 8/3/24.
//
import Darwin
import Foundation
func addrRefCreate(_ path: String) -> CFData? {
// make a sockaddr struct (this is ugly), wrap it in a CFData obj
var socketAdr = sockaddr_un()
socketAdr.sun_family = sa_family_t(AF_UNIX)
socketAdr.sun_len = __uint8_t(MemoryLayout<sockaddr_un>.size)
if var cstring_path = path.cString(using: .utf8) {
if cstring_path.count > MemoryLayout.size(
ofValue: socketAdr.sun_path)
{
// path is too long for this 1970s era struct
return nil
}
memcpy(&socketAdr.sun_path, &cstring_path, cstring_path.count)
} else {
return nil
}
return NSData(bytes: &socketAdr,
length: MemoryLayout.size(ofValue: socketAdr)) as CFData
}
+19
View File
@@ -0,0 +1,19 @@
//
// socket/server.swift
// munkitester
//
// Created by Greg Neagle on 8/3/24.
//
import CoreFoundation
import Darwin
import Foundation
class BaseServer {
var timeout = 0
var socketRef: CFSocket?
init(_: CInt) {
let socket =
}
}
+38
View File
@@ -0,0 +1,38 @@
//
// xattr.swift
// munki
//
// Created by Greg Neagle on 8/4/24.
//
import Darwin
import Foundation
func listXattrs(atPath path: String) throws -> [String] {
// A simple implementation sufficient for Munki's needs
// Inspired by https://github.com/okla/swift-xattr/blob/master/xattr.swift
let bufLength = listxattr(path, nil, 0, XATTR_NOFOLLOW)
guard bufLength != -1 else {
let errString = String(utf8String: strerror(errno)) ?? String(errno)
throw MunkiError("Could not get buffer length for xattrs for \(path): \(errString)")
}
let buf = UnsafeMutablePointer<Int8>.allocate(capacity: bufLength)
guard listxattr(path, buf, bufLength, 0) != -1 else {
let errString = String(utf8String: strerror(errno)) ?? String(errno)
throw MunkiError("Could not get list of xattrs for \(path): \(errString)")
}
var names = NSString(bytes: buf, length: bufLength, encoding: String.Encoding.utf8.rawValue)?.components(separatedBy: "\0").filter { !$0.isEmpty }
return names ?? [String]()
}
func removeXattr(_ name: String, atPath path: String) throws {
// A simple implementation sufficient for Munki's needs
// Inspired by https://github.com/okla/swift-xattr/blob/master/xattr.swift
if removexattr(path, name, XATTR_NOFOLLOW) == -1 {
let errString = String(utf8String: strerror(errno)) ?? String(errno)
throw MunkiError("Failed to remove xattr \(name) from \(path): \(errString)")
}
}