From c260e68a58af3596b5d56b33c073bc3ef7955428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Theilgaard?= Date: Sun, 28 Mar 2021 14:10:13 +0200 Subject: [PATCH] v0.5.0 --- CHANGELOG.md | 15 ++++++-- README.md | 80 +++++++++++++++++++++++++++++++++++-------- buildCaseStatement.sh | 77 +++++++++++++++++++++++++++++++++-------- 3 files changed, 141 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca2ddc9..9246034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ -## v0.5 - 2020- +## v0.5 - 2021-03-28 -- when Installomator cannot find an app in a dmg with the given appName it will now use the first .app in the root dir. This is for applications which contain the version number in the application name. Note: this might lead to multiple versions of the app in the /Applications directory. You will have to find a different means to clean these up when necessary +- Major update and now with help from @Theile and @Isaac +- Added additional `BLOCKING_PROCESS_ACTION` handlings +- Added additional `NOTIFY=all`. Usuful if used in Self Service, as the user will be notified before download, before install as well as when it is done. +- Added variable `LOGO` for icons i dialogs, use `LOGO=appstore` (or `jamf` or `mosyleb` or `mosylem` or `addigy`). It's also possible to set it to a direct path to a specific icon. Default is `appstore`. +- Added variable `INSTALL` that can be set to `INSTALL=force` if software needs to be installed even though latest version is already installed (it will be a reinstall). +- Version control now included. The variable `appNewVersion` in a label can be used to tell what the latest version from the web is. If this is not given, version checking is done after download. +- For a label that only installs a pkg without an app in it, a variable `packageID` can be used for version checking. +- Labels now sorted alphabetically, except for the Microsoft ones (that are at the end of the list). A bunch of new labels added, and lots of them have either been changed or improved (with `appNewVersion` og `packageID`). +- If an app is asked to be closed down, it will now be opened again after the update. +- If your MDM cannot call a script with parameters, the label can be set in the top of the script. +- If your MDM is not Jamf Pro, and you need the script to be installed locally on your managed machines, then take a look at [Theiles fork](https://github.com/Theile/Installomator/). This fork can be called from the MDM using a small script. +- Script `buildCaseStatement.sh` to help with creating labels have been improved. ## v0.4 - 2020-10-19 diff --git a/README.md b/README.md index 4482e31..2b68acc 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ There are a few interesting post on Installomator on my weblog: - [Introducing Installomator](https://scriptingosx.com/2020/05/introducing-installomator/) - [Using Installomator with Jamf Pro](https://scriptingosx.com/2020/06/using-installomator-with-jamf-pro/) by Mischa van der Bent +- [Using another MDM than Jamf and you might want a local installation](https://github.com/Theile/Installomator/) By Søren Theilgaard ## Background @@ -102,29 +103,42 @@ When used to install software, Installomator has a single argument: the label or ``` ./Installomator.sh firefox +./Installomator.sh firefox LOGO=jamf BLOCKING_PROCESS_ACTION=tell_user_then_kill NOTIFY=all ``` There is a debug mode and one other setting that can be controlled with variables in the code. This simplifies the actual use of the script from within a management system. ### Extensible -As of this writing, Installomator knows how to download and install more than 50 different applications. You can add more by adding a block to the _long_ `case` statement starting on line 160. Some of them are more elaborate, but most of them just need this information: +As of this writing, Installomator knows how to download and install more than 238 different applications. You can add more by adding a block to the _long_ `case` statement starting on line 758. Some of them are more elaborate, but most of them (just) need this information (not really "just" in this case, as we have to differentiate between arm64 and i386 versions for both `downloadURL` and `appNewVersion`): ``` googlechrome) name="Google Chrome" type="dmg" - downloadURL="https://dl.google.com/chrome/mac/stable/GGRO/googlechrome.dmg" + if [[ $(arch) != "i386" ]]; then + printlog "Architecture: arm64 (not i386)" + downloadURL="https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg" + appNewVersion=$(curl -s https://omahaproxy.appspot.com/history | awk -F',' '/mac_arm64,stable/{print $3; exit}') # Credit: William Smith (@meck) + else + printlog "Architecture: i386" + downloadURL="https://dl.google.com/chrome/mac/stable/GGRO/googlechrome.dmg" + appNewVersion=$(curl -s https://omahaproxy.appspot.com/history | awk -F',' '/mac,stable/{print $3; exit}') # Credit: William Smith (@meck) + fi expectedTeamID="EQHXZ8M8AV" ;; ``` When you know how to extract these pieces of information from the application and/or download, then you can add an application to Installomator. +The script buildCaseStatement.sh can help with the label creation. + ### Not specific to a management system I wrote this script mainly for use with Jamf Pro, because that is what we use. For testing, you can run the script interactively from the command line. However, I have tried to keep anything that is specific to Jamf optional, or so flexible that it will work anywhere. Even if it does not work with your management system 'out of the box,' the adaptations should be straightforward. +Not all MDMs can include the full script, for those MDMs it might be more useful to install it on the client machines, and run it from there. See [Using another MDM than Jamf and you might want a local installation](https://github.com/Theile/Installomator/) By Søren Theilgaard. + ### No dependencies The script started out as a pure `sh` script, and when I needed arrays I 'switched' to `zsh`, because that is what [we can rely on being in macOS for the foreseeable future](https://scriptingosx.com/zsh). There are quite a few places where using python would have been easier and safer, but with the python 2 run-time being deprecated, that would have added a requirement for a Python 3 run-time to be installed. XML and JSON parsing would have been better with a tool like [scout](https://github.com/ABridoux/scout) or [jq](https://stedolan.github.io/jq/), but those would again require additional installations on the client before the script can run. @@ -141,15 +155,19 @@ The argument can be `version` or `longversion` which will print the script's ver ``` > ./Installomator.sh version -0.1 +2021-03-28 10:03:42 version ################## Start Installomator v. 0.5.0 +2021-03-28 10:03:42 version ################## version +2021-03-28 10:03:42 version 0.5.0 > ./Installomator.sh longversion -Installomater: version 0.1 (20200506) +2021-03-28 10:04:16 longversion ################## Start Installomator v. 0.5.0 +2021-03-28 10:04:16 longversion ################## longversion +2021-03-28 10:04:16 longversion Installomater: version 0.5.0 (2021-03-28) ``` Other than the version arguments, the argument can be any of the labels listed in the Labels.txt file. Each of the labels will download and install the latest version of the application, or suite of applications. Since the script will have to run the `installer` command or copy the application to the `/Applications` folder, it will have to be run as root. ``` -> sudo ./Installomator.sh desktoppr +> sudo ./Installomator.sh desktoppr DEBUG=0 ``` (Since Jamf Pro always provides the mount point, computer name, and user name as the first three arguments for policy scripts, the script will use argument `$4` when there are more than three arguments.) @@ -174,8 +192,9 @@ Then you can use the Installomator script in a policy and choose the application When it runs with a known label, the script will perform the following: -- when the application is running, prompt the user to quit or cancel +- Check the version installed with the version online. Only continue if it's different - download the latest version from the vendor +- when the application is running, prompt the user to quit or cancel - dmg or zip archives: - extract the application and copy it to /Applications - change the owner of the application to the current user @@ -203,20 +222,42 @@ The `BLOCKING_PROCESS_ACTION` variable controls the behavior of the script when There are five options: -- `ignore`: continue even when blocking processes are found -- `silent_fail`: exit script without prompt or installation -- `prompt_user`: show a user dialog for each blocking process found abort after three attempts to quit -- `prompt_user_then_kill`: show a user dialog for each blocking process found, attempt to quit two times, kill the process finally -- `kill`: kill process without prompting or giving the user a chance to save +- `ignore`: continue even when blocking processes are found. +- `silent_fail`: exit script without prompt or installation. +- `prompt_user`: (default) show a user dialog for each blocking process found abort after three attempts to quit (only if user accepts to quit the apps, otherwise the update is cancelled). +- `prompt_user_then_kill`: show a user dialog for each blocking process found, attempt to quit two times, kill the process finally. +- `prompt_user_loop`: Like prompt-user, but clicking "Not Now", will just wait an hour, and then it will ask again. +- `tell_user`: User will be showed a notification about the important update, but user is only allowed to quit and continue, and then we ask the app to quit. +- `tell_user_then_kill`: Show dialog 2 times, and if the quitting fails, the blocking processes will be killed. +- `kill`: kill process without prompting or giving the user a chance to save. -The default is `prompt_user`. +If any process was closed, Installomator will try to open the app again, after the update process is done. ### Notification -The `NOTIFY` variable controls the notifications shown to the user. As of now, there are two options: `success` (default) and `silent`. +The `NOTIFY` variable controls the notifications shown to the user. As of now, there are three options: -- `success`: notify the user after a successful install +- `success`: (default) notify the user after a successful install - `silent`: no notifications +- `all`: all notifications (great for Self Service installation) + +### Logo + +The `LOGO` variable is used for the icon shown in dialog boxes. There are these options: + +- `appstore`: Icon is Apple App Store (default) +- `jamf`: JAMF Pro +- `mosyleb`: Mosyle Business +- `mosylem`: Mosyle Manager (Education) +- `addigy`: Addigy +Path can also be set in the command call, and if file exists, it will be used, like `LOGO="/System/Applications/App\ Store.app/Contents/Resources/AppIcon.icns"` (spaces are escaped). + +### Install behavior (force installation) + +Since we now make a version checking, and only installs the software if the version is different, an `INSTALL` variable can be used to force the installation: + +- ``: When not set, software is only installed if it is newer/different in version (default) +- `force`: Install even if it’s the same version ### Adding applications/label blocks @@ -246,10 +287,21 @@ The type of installation. Possible values: - `zip`: application in zip archive (`zip` or `tbz` extension) - `pkgInDmg`: a pkg file inside a disk image - `pkgInZip`: a pkg file inside a zip + - `appInDmgInZip`: an app in a dmg file that has been zip'ed - `downloadURL`: The URL from which to download the archive. The URL can be generated by a series of commands, for example when you need to parse an xml file for the latest URL. (See `bbedit`, `desktoppr`, or `omnigraffle` for examples.) +Sometimes version differs between Intel and Apple Silicon versions. (See `brave`, `obsidian`, `omnidisksweeper`, or `notion`). + +- `appNewVersion` (optional, but recommended): +Version of the downloaded software. +If given, it will be compared to installed version, to see if download is different. +It does not check for newer or not, only different. +Not always easy to figure out how to make this. Sometimes this is listed on the downloads page, sometimes in other places. And how can we isolate it in a genral manner? (See `abstract`, `bbedit`, `brave`, `desktoppr`, `googlechrome`, or `omnidisksweeper`). + +- `packageID` (optional, but recommended for pkgs without an app) +This variable is for pkg bundle IDs. Very usefull if a pkg only install command line tools, or the like that does not install an app. (See label `desktoppr`, `golang`, `installomator_st`, `odrive`, or `teamviewerhost`). - `expectedTeamID`: The 10-character Developer Team ID with which the application or pkg is signed and notarized. diff --git a/buildCaseStatement.sh b/buildCaseStatement.sh index 4087013..b2ad2b1 100755 --- a/buildCaseStatement.sh +++ b/buildCaseStatement.sh @@ -20,7 +20,11 @@ fi # download the URL echo "Downloading $downloadURL" +<<<<<<< Updated upstream if ! archivePath=$(curl -fsL "$downloadURL" --remote-header-name --remote-name -w "%{filename_effective}"); then +======= +if ! downloadOut="$(curl -fsL "$downloadURL" --remote-header-name --remote-name -w "%{filename_effective}\n%{url_effective}\n")"; then +>>>>>>> Stashed changes echo "error downloading $downloadURL" exit 2 fi @@ -36,10 +40,43 @@ xpath() { fi } +<<<<<<< Updated upstream #archivePath=$(find $tmpDir -print ) echo "archivePath: $archivePath" archiveName=${archivePath##*/} echo "archiveName: $archiveName" +======= +pkgInvestigation() { + echo "Package found" + teamID=$(spctl -a -vv -t install "$archiveName" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()' ) + echo "For PKGs it's advised to find packageID for version checking" + + pkgutil --expand "$pkgPath" "$archiveName"_pkg + cat "$archiveName"_pkg/Distribution | xpath '//installer-gui-script/pkg-ref[@id][@version]' 2>/dev/null + packageID="$(cat "$archiveName"_pkg/Distribution | xpath '//installer-gui-script/pkg-ref[@id][@version]' 2>/dev/null | tr ' ' '\n' | grep -i "id" | cut -d \" -f 2)" + rm -r "$archiveName"_pkg + echo "$packageID" + echo "Above is the possible packageIDs that can be used, and the correct one is probably one of those with a version number. More investigation might be needed to figure out correct packageID if several are displayed." +} +appInvestigation() { + appName=${appPath##*/} + + # verify with spctl + echo "Verifying: $appPath" + if ! teamID=$(spctl -a -vv "$appPath" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()' ); then + echo "Error verifying $appPath" + exit 4 + fi +} +echo "downloadOut: ${downloadOut}" +archiveTempName=$( echo "${downloadOut}" | head -1 ) +echo "archiveTempName: $archiveTempName" +archivePath=$( echo "${downloadOut}" | tail -1 ) +echo "archivePath: $archivePath" +archiveName=${archivePath##*/} +echo "archiveName: $archiveName" +mv $archiveTempName $archiveName +>>>>>>> Stashed changes name=${archiveName%.*} echo "name: $name" archiveExt=${archiveName##*.} @@ -48,6 +85,7 @@ identifier=$(echo $name | tr '[:upper:]' '[:lower:]') echo "identifier: $identifier" if [ "$archiveExt" = "pkg" ]; then +<<<<<<< Updated upstream echo "Package found" teamID=$(spctl -a -vv -t install "$archiveName" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()' ) echo "For PKGs it's advised to find packageID for version checking" @@ -57,6 +95,10 @@ if [ "$archiveExt" = "pkg" ]; then rm -r "$archiveName"_pkg echo "$packageID" echo "Above is the possible packageIDs that can be used, and the correct one is probably one of those with a version number. More investigation might be needed to figure out correct packageID if several are displayed." +======= + pkgPath="$archiveName" + pkgInvestigation +>>>>>>> Stashed changes elif [ "$archiveExt" = "dmg" ]; then echo "Diskimage found" # mount the dmg @@ -66,16 +108,16 @@ elif [ "$archiveExt" = "dmg" ]; then exit 3 fi echo "Mounted: $dmgmount" - # check if app exists + # check if app og pkg exists appPath=$(find "$dmgmount" -name "*.app" -maxdepth 1 -print ) - appName=${appPath##*/} - - # verify with spctl - echo "Verifying: $appPath" - if ! teamID=$(spctl -a -vv "$appPath" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()' ); then - echo "Error verifying $appPath" - exit 4 + pkgPath=$(find "$dmgmount" -name "*.pkg" -maxdepth 1 -print ) + + if [[ $appPath != "" ]]; then + appInvestigation + elif [[ $pkgPath != "" ]]; then + archiveExt="pkgInDmg" + pkgInvestigation fi hdiutil detach "$dmgmount" @@ -84,20 +126,25 @@ elif [ "$archiveExt" = "zip" ] || [ "$archiveExt" = "tbz" ]; then # unzip the archive tar -xf "$archiveName" - # check if app exists + # check if app og pkg exists appPath=$(find "$tmpDir" -name "*.app" -maxdepth 2 -print ) - appName=${appPath##*/} - # verify with spctl - echo "Verifying: $appPath" - if ! teamID=$(spctl -a -vv "$appPath" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()' ); then - echo "Error verifying $appPath" - exit 4 + pkgPath=$(find "$tmpDir" -name "*.pkg" -maxdepth 2 -print ) + + if [[ $appPath != "" ]]; then + appInvestigation + elif [[ $pkgPath != "" ]]; then + archiveExt="pkgInZip" + pkgInvestigation fi fi echo +<<<<<<< Updated upstream echo "appNewVersion is often difficult to find. Can sometimes be found in the filename, but also on a web page." +======= +echo "appNewVersion is often difficult to find. Can sometimes be found in the filename, but also on a web page. See archivePath above if link contains information about this." +>>>>>>> Stashed changes echo echo "$identifier)" echo " name=\"$name\""