mirror of
https://github.com/munki/munki.git
synced 2026-05-14 16:28:58 -05:00
So much new code to support software installation
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// main.swift
|
||||
// appusaged
|
||||
//
|
||||
// Created by Greg Neagle on 8/3/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
print("Hello, World!")
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ?? ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
-21
@@ -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
|
||||
//
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 =
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user