xolo-server 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +42 -4
- data/bin/xoloserver +3 -0
- data/data/client/xolo +1160 -0
- data/lib/optimist_with_insert_blanks.rb +1216 -0
- data/lib/xolo/core/base_classes/configuration.rb +238 -0
- data/lib/xolo/core/base_classes/server_object.rb +112 -0
- data/lib/xolo/core/base_classes/title.rb +648 -0
- data/lib/xolo/core/base_classes/version.rb +601 -0
- data/lib/xolo/core/constants.rb +81 -0
- data/lib/xolo/core/exceptions.rb +52 -0
- data/lib/xolo/core/json_wrappers.rb +43 -0
- data/lib/xolo/core/loading.rb +59 -0
- data/lib/xolo/core/output.rb +292 -0
- data/lib/xolo/core/version.rb +21 -0
- data/lib/xolo/core.rb +46 -0
- data/lib/xolo/server/configuration.rb +1 -2
- data/lib/xolo/server/helpers/jamf_pro.rb +1 -0
- data/lib/xolo/server/mixins/title_jamf_access.rb +7 -12
- data/lib/xolo/server/mixins/title_ted_access.rb +21 -5
- data/lib/xolo/server/mixins/version_jamf_access.rb +23 -17
- data/lib/xolo/server/mixins/version_ted_access.rb +9 -4
- data/lib/xolo/server/routes.rb +6 -23
- data/lib/xolo/server/version.rb +1 -3
- metadata +21 -10
data/data/client/xolo
ADDED
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
#!/bin/zsh
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# Copyright 2025 Pixar
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
7
|
+
# at the root of this project.
|
|
8
|
+
|
|
9
|
+
# SHELL SETUP
|
|
10
|
+
###############################
|
|
11
|
+
###############################
|
|
12
|
+
|
|
13
|
+
# Load the zsh/zutil module for several utility functions
|
|
14
|
+
zmodload zsh/zutil
|
|
15
|
+
|
|
16
|
+
# for perl-style regexps
|
|
17
|
+
# DISABLED FOR NOW - some versions of macos don't seem to have
|
|
18
|
+
# /usr/lib/zsh/5.9/zsh/pcre.so
|
|
19
|
+
# All our regexps now use posix style
|
|
20
|
+
#
|
|
21
|
+
# setopt RE_MATCH_PCRE
|
|
22
|
+
|
|
23
|
+
# ENVIRONMENT
|
|
24
|
+
###############################
|
|
25
|
+
###############################
|
|
26
|
+
|
|
27
|
+
# Needed for the UTF-8 chars in the lsreg output
|
|
28
|
+
export LANG="en_US.UTF-8"
|
|
29
|
+
export LC_ALL="en_US.UTF-8"
|
|
30
|
+
|
|
31
|
+
# CONSTANTS
|
|
32
|
+
###############################
|
|
33
|
+
###############################
|
|
34
|
+
|
|
35
|
+
# The version of xolo
|
|
36
|
+
# This should be updated automatically when this script is packaged
|
|
37
|
+
XOLO_VERSION="0.0.0"
|
|
38
|
+
|
|
39
|
+
# The usage message for xolo
|
|
40
|
+
USAGE='xolo [options] <command> [<title> [<version>]]'
|
|
41
|
+
|
|
42
|
+
# The URL for more information about xolo
|
|
43
|
+
XOLO_DOX_URL='<URL TO BE DETERMINED>'
|
|
44
|
+
|
|
45
|
+
# The long path to the lsregister command
|
|
46
|
+
LSREG='/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister'
|
|
47
|
+
|
|
48
|
+
# The path to the jamf binary
|
|
49
|
+
JAMF='/usr/local/bin/jamf'
|
|
50
|
+
|
|
51
|
+
# The path to the client data json file
|
|
52
|
+
CLIENT_DATA_JSON_FILE="/Library/Application Support/xolo/client-data.json"
|
|
53
|
+
|
|
54
|
+
# The jamf policy trigger to update the client data
|
|
55
|
+
UPDATE_CLIENT_DATA_TRIGGER='update-xolo-client-data'
|
|
56
|
+
|
|
57
|
+
# GLOBAL VARIABLES
|
|
58
|
+
###############################
|
|
59
|
+
###############################
|
|
60
|
+
|
|
61
|
+
# From the command line
|
|
62
|
+
|
|
63
|
+
# options
|
|
64
|
+
show_help=
|
|
65
|
+
be_verbose=
|
|
66
|
+
be_quiet=
|
|
67
|
+
debugging_on=
|
|
68
|
+
no_versions=
|
|
69
|
+
show_xolo_version=
|
|
70
|
+
|
|
71
|
+
# arguments
|
|
72
|
+
command=
|
|
73
|
+
title=
|
|
74
|
+
version=
|
|
75
|
+
|
|
76
|
+
# An Associative Array to hold the installed apps
|
|
77
|
+
# is populated by get_installed_apps()
|
|
78
|
+
typeset -A installed_apps
|
|
79
|
+
|
|
80
|
+
# FUNCTIONS
|
|
81
|
+
###############################
|
|
82
|
+
###############################
|
|
83
|
+
|
|
84
|
+
# Print a message to stderr
|
|
85
|
+
###############################
|
|
86
|
+
function echoerr() { echo "$@" 1>&2; }
|
|
87
|
+
|
|
88
|
+
# Die with an error message and status
|
|
89
|
+
###############################
|
|
90
|
+
function die() {
|
|
91
|
+
local msg="$1"
|
|
92
|
+
local mystat=1
|
|
93
|
+
|
|
94
|
+
[[ -n "$2" ]] && mystat=$2
|
|
95
|
+
echoerr "$msg"
|
|
96
|
+
exit $mystat
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Show the help message
|
|
101
|
+
###############################
|
|
102
|
+
function show_help() {
|
|
103
|
+
cat <<ENDHELP
|
|
104
|
+
xolo: manage software titles on this computer
|
|
105
|
+
|
|
106
|
+
Usage:
|
|
107
|
+
$USAGE
|
|
108
|
+
|
|
109
|
+
Options:
|
|
110
|
+
-h, -H, --help: Show this help message.
|
|
111
|
+
-v, --verbose: Enable verbose mode, extra information will be printed.
|
|
112
|
+
-q, --quiet: Enable quiet mode, only errors will be printed to stderr.
|
|
113
|
+
-d, --debug: Enable debug mode, extra debug information will be printed.
|
|
114
|
+
-r, --recon: With 'install' or 'uninstall' run a 'jamf recon' after the
|
|
115
|
+
operation completes successfully.
|
|
116
|
+
-n, --no-versions: With the 'list-titles' and 'list-installed' commands,
|
|
117
|
+
do not show the versions available, or the version
|
|
118
|
+
installed, respectively.
|
|
119
|
+
-V, --version: Show the version of xolo.
|
|
120
|
+
|
|
121
|
+
NOTE: Debug mode will also enable verbose mode. Both debug and verbose modes
|
|
122
|
+
override quiet mode.
|
|
123
|
+
|
|
124
|
+
Commands:
|
|
125
|
+
install, i <title> [<version>]
|
|
126
|
+
Install a title, or specific version thereof (e.g. a version currently in pilot)
|
|
127
|
+
If no version is specified, the currently released version will be installed.
|
|
128
|
+
|
|
129
|
+
uninstall, u <title>
|
|
130
|
+
Uninstall a title, if possible. Not all titles are uninstallable via xolo.
|
|
131
|
+
|
|
132
|
+
update, U
|
|
133
|
+
Run any pending updates to currently installed titles, or install new titles
|
|
134
|
+
that are scoped to auto-install on this computer. The exact behavior depends on
|
|
135
|
+
what's installed and the title's configuration. This also happens automatically
|
|
136
|
+
every time this computer checks in with Jamf Pro.
|
|
137
|
+
Note: this just runs the 'jamf policy' command, which will execute any pending
|
|
138
|
+
Jamf policies triggered by "recurring check-in".
|
|
139
|
+
|
|
140
|
+
refresh, r
|
|
141
|
+
Update xolo's information about current titles and versions.
|
|
142
|
+
This also happens automatically at least daily, when this computer is online.
|
|
143
|
+
|
|
144
|
+
list-titles, lt
|
|
145
|
+
List all known titles and versions. Not all may be available for install,
|
|
146
|
+
e.g., if this computer is in an excluded computer-group
|
|
147
|
+
|
|
148
|
+
list-installed, li
|
|
149
|
+
List titles installed on this computer, that are known to xolo. This may take
|
|
150
|
+
a few moments, since all titles with version scripts will run those scripts
|
|
151
|
+
to see if they are installed.
|
|
152
|
+
|
|
153
|
+
details, d <title> [<version>]
|
|
154
|
+
Show detailed information about a title, or a specific version thereof.
|
|
155
|
+
|
|
156
|
+
expire, e
|
|
157
|
+
Expire the given title if it has not been used in its defined expiration period.
|
|
158
|
+
Does nothing if the title is not expirable.
|
|
159
|
+
This also happens automatically at least daily, when this computer is online.
|
|
160
|
+
|
|
161
|
+
help, h
|
|
162
|
+
Show this help message. The same as -h or --help.
|
|
163
|
+
|
|
164
|
+
For more information about xolo, see $XOLO_DOX_URL
|
|
165
|
+
ENDHELP
|
|
166
|
+
} # end show_help
|
|
167
|
+
|
|
168
|
+
# Parse the command line arguments
|
|
169
|
+
###############################
|
|
170
|
+
function parse_cli() {
|
|
171
|
+
zparseopts -D -F -E -- \
|
|
172
|
+
{h,H,-help}=show_help \
|
|
173
|
+
{v,-verbose}=be_verbose \
|
|
174
|
+
{q,-quiet}=be_quiet \
|
|
175
|
+
{d,-debug}=debugging_on \
|
|
176
|
+
{n,-no-versions}=no_versions \
|
|
177
|
+
{r,-recon}=do_recon \
|
|
178
|
+
{V,-version}=show_xolo_version || \
|
|
179
|
+
die 'Unknonwn Options or Args.'
|
|
180
|
+
|
|
181
|
+
# they are in arrays, but we want regular vars
|
|
182
|
+
show_help=$show_help[-1]
|
|
183
|
+
be_verbose=$be_verbose[-1]
|
|
184
|
+
be_quiet=$be_quiet[-1]
|
|
185
|
+
debugging_on=$debugging_on[-1]
|
|
186
|
+
no_versions=$no_versions[-1]
|
|
187
|
+
do_recon=$do_recon[-1]
|
|
188
|
+
show_xolo_version=$show_xolo_version[-1]
|
|
189
|
+
|
|
190
|
+
# if debugging is on, verbose is also on
|
|
191
|
+
[[ -n $debugging_on ]] && be_verbose=1
|
|
192
|
+
|
|
193
|
+
# if we're in verbose mode, we're not in quiet mode
|
|
194
|
+
[[ -n $be_verbose ]] && unset be_quiet
|
|
195
|
+
|
|
196
|
+
command=$1
|
|
197
|
+
[[ ${#@} -gt 0 ]] && shift
|
|
198
|
+
|
|
199
|
+
title=$1
|
|
200
|
+
[[ ${#@} -gt 0 ]] && shift
|
|
201
|
+
|
|
202
|
+
version=$1
|
|
203
|
+
|
|
204
|
+
# echo "show_help is: $show_help"
|
|
205
|
+
# echo "be_verbose is: $be_verbose"
|
|
206
|
+
# echo "be_quiet is: $be_quiet"
|
|
207
|
+
# echo "debugging_on is: $debugging_on"
|
|
208
|
+
# echo "command is: $command"
|
|
209
|
+
# echo "title is: $title"
|
|
210
|
+
# echo "version is: $version"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
debug "Parsed command line:"
|
|
214
|
+
debug "..show_help is: $show_help"
|
|
215
|
+
debug "..be_verbose is: $be_verbose"
|
|
216
|
+
debug "..be_quiet is: $be_quiet"
|
|
217
|
+
debug "..debugging_on is: $debugging_on"
|
|
218
|
+
debug "..no_versions is: $no_versions"
|
|
219
|
+
debug "..show_xolo_version is: $show_xolo_version"
|
|
220
|
+
|
|
221
|
+
debug "..command is: $command"
|
|
222
|
+
debug "..title is: $title"
|
|
223
|
+
debug "..version is: $version"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# quick test for debugging_on
|
|
227
|
+
###############################
|
|
228
|
+
function debugging_on() {
|
|
229
|
+
[[ -n "$debugging_on" ]]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# quick test for be_verbose
|
|
233
|
+
###############################
|
|
234
|
+
function be_verbose() {
|
|
235
|
+
[[ -n "$be_verbose" ]]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# quick test for be_quiet
|
|
239
|
+
###############################
|
|
240
|
+
function be_quiet() {
|
|
241
|
+
[[ -n "$be_quiet" ]]
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Print a message to stout if we're not in quiet mode
|
|
245
|
+
###############################
|
|
246
|
+
function say() {
|
|
247
|
+
be_quiet || echo "$*"
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Print a message to stdout if we're in verbose or debug mode
|
|
251
|
+
###############################
|
|
252
|
+
function verbose() {
|
|
253
|
+
be_verbose && echo "$*"
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Print a message to stderr if we're in debug mode
|
|
257
|
+
###############################
|
|
258
|
+
function debug() {
|
|
259
|
+
debugging_on && echo "DEBUG: $*" 1>&2
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# Gather all installed bundle ids and installed versions from lsreg
|
|
263
|
+
# store them in an associative array in $installed_apps.
|
|
264
|
+
# keys are bundle ids, values are app name & installed version
|
|
265
|
+
# separated by a semicolon
|
|
266
|
+
# e.g. 'com.apple.Safari => Safari.app;14.0.3'
|
|
267
|
+
###############################
|
|
268
|
+
function get_installed_apps() {
|
|
269
|
+
debug "Getting all installed .apps on this Mac..."
|
|
270
|
+
local app_name
|
|
271
|
+
local app_bundle_id
|
|
272
|
+
local app_vers
|
|
273
|
+
|
|
274
|
+
# loop thru lsreg output
|
|
275
|
+
"$LSREG" -dump Bundle | while IFS= read -r line ; do
|
|
276
|
+
# a line if ------ starts a new record
|
|
277
|
+
if [[ "$line" =~ '^-+$' ]] ; then
|
|
278
|
+
unset app_name
|
|
279
|
+
unset app_bundle_id
|
|
280
|
+
unset app_vers
|
|
281
|
+
continue
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
# $MATCH is the whole matched string, $match is the array of captures
|
|
285
|
+
# NOTE this only works if the path is listed before the CFBundleIdentifier
|
|
286
|
+
# which seems to be the case in the lsreg output
|
|
287
|
+
[[ "$line" =~ '^path:[[:space:]]+/.*/(.+\.app) \(' ]] && app_name=$match[1]
|
|
288
|
+
[[ "$line" =~ '^[[:space:]]+CFBundleIdentifier = \"(.+)\";' ]] && app_bundle_id=$match[1]
|
|
289
|
+
[[ "$line" =~ '^[[:space:]]+CFBundleShortVersionString = \"(.+)\";' ]] && app_vers=$match[1]
|
|
290
|
+
|
|
291
|
+
if [[ -n "$app_name" && -n "$app_bundle_id" && -n "$app_vers" ]] ; then
|
|
292
|
+
installed_apps[$app_bundle_id]="$app_name;$app_vers"
|
|
293
|
+
unset app_name
|
|
294
|
+
unset app_bundle_id
|
|
295
|
+
unset app_vers
|
|
296
|
+
fi
|
|
297
|
+
done
|
|
298
|
+
# installed_apps is now an associative array with bundle ids as keys and app names as values
|
|
299
|
+
|
|
300
|
+
# print the installed apps - this is a long list
|
|
301
|
+
if debugging_on ; then
|
|
302
|
+
debug "All installed .apps on this Mac:"
|
|
303
|
+
for bundle app in ${(kv)installed_apps}; do
|
|
304
|
+
debug "'$bundle' -> '$app'"
|
|
305
|
+
done
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
} # end installed_apps
|
|
309
|
+
|
|
310
|
+
# Extract data from the client-data.json file
|
|
311
|
+
#
|
|
312
|
+
# TODO: Once we only support macOS 15 (Sequoia) and higher, we
|
|
313
|
+
# can use /usr/bin/jq to parse the JSON file instead of using JXA.
|
|
314
|
+
#
|
|
315
|
+
# $1 = a string of javascript that extracts your desired info from
|
|
316
|
+
# the client-data.json file, which has already been parsed into
|
|
317
|
+
# the variable 'parsed_data'
|
|
318
|
+
#
|
|
319
|
+
# your code should process parsed_data however it needs, and then
|
|
320
|
+
# put the desired output string into the 'result' var, which has already
|
|
321
|
+
# been declared.
|
|
322
|
+
#
|
|
323
|
+
# The result of your processing will be sent to stdout of this function
|
|
324
|
+
#######################################
|
|
325
|
+
function extract_json_data() {
|
|
326
|
+
debug "Extracting data from client-data.json..."
|
|
327
|
+
local processing_code="$1"
|
|
328
|
+
local CLIENT_DATA_PROCESSING_JS
|
|
329
|
+
|
|
330
|
+
read -r -d '' CLIENT_DATA_PROCESSING_JS <<ENDJAVASCRIPT
|
|
331
|
+
// this gives us access to the local file system
|
|
332
|
+
var app = Application.currentApplication();
|
|
333
|
+
app.includeStandardAdditions = true;
|
|
334
|
+
|
|
335
|
+
// run() is automatically executed when the program is called, and will print any output returned.
|
|
336
|
+
function run() {
|
|
337
|
+
var parsed_data = JSON.parse(app.read("${CLIENT_DATA_JSON_FILE}"));
|
|
338
|
+
var result;
|
|
339
|
+
|
|
340
|
+
// the line below will sub-in whatever code we were passed
|
|
341
|
+
// to process the parsed data and store the result in 'result'
|
|
342
|
+
|
|
343
|
+
${processing_code}
|
|
344
|
+
|
|
345
|
+
return result
|
|
346
|
+
}
|
|
347
|
+
ENDJAVASCRIPT
|
|
348
|
+
|
|
349
|
+
# in debug mode, show the javascript that will be run
|
|
350
|
+
debug '============= EXECUTING JAVASCRIPT ==========='
|
|
351
|
+
debug "${CLIENT_DATA_PROCESSING_JS}"
|
|
352
|
+
debug '========================'
|
|
353
|
+
|
|
354
|
+
# run the javascript to send the result to stdout, which is the default
|
|
355
|
+
# behavior of of the 'run' function in JXA
|
|
356
|
+
osascript -l "JavaScript" <<< "${CLIENT_DATA_PROCESSING_JS}"
|
|
357
|
+
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# Outputs all known titles, one per line
|
|
361
|
+
#######################################
|
|
362
|
+
function all_xolo_titles() {
|
|
363
|
+
debug "Getting all known titles..."
|
|
364
|
+
local jscode
|
|
365
|
+
|
|
366
|
+
read -r -d '' jscode <<ENDJAVASCRIPT
|
|
367
|
+
result = Object.keys(parsed_data.titles).join('\n');
|
|
368
|
+
ENDJAVASCRIPT
|
|
369
|
+
extract_json_data "$jscode"
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
# Outputs all known titles, one per line, with versions and statuses
|
|
373
|
+
#######################################
|
|
374
|
+
function all_xolo_titles_with_versions() {
|
|
375
|
+
debug "Getting all known titles with versions and statuses..."
|
|
376
|
+
local jscode
|
|
377
|
+
|
|
378
|
+
read -r -d '' jscode <<ENDJAVASCRIPT
|
|
379
|
+
|
|
380
|
+
result = '';
|
|
381
|
+
|
|
382
|
+
Object.entries(parsed_data.titles).forEach(([title, title_data]) => {
|
|
383
|
+
result += \`\${title}: \`;
|
|
384
|
+
|
|
385
|
+
if (title_data.versions.length > 0) {
|
|
386
|
+
vers_array = title_data.versions.map(
|
|
387
|
+
function (version) {
|
|
388
|
+
return \`\${version.version} (\${version.status})\`;
|
|
389
|
+
} // end function
|
|
390
|
+
); // end map
|
|
391
|
+
|
|
392
|
+
result += \`\${vers_array.join(', ')}\`;
|
|
393
|
+
} // end if
|
|
394
|
+
|
|
395
|
+
result += "\n";
|
|
396
|
+
}); // end forEach
|
|
397
|
+
|
|
398
|
+
// remove the last newline
|
|
399
|
+
result = result.slice(0, -1);
|
|
400
|
+
|
|
401
|
+
ENDJAVASCRIPT
|
|
402
|
+
extract_json_data "$jscode"
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# $1 = the name of a title
|
|
407
|
+
# Outputs all the versions for the title, one per line
|
|
408
|
+
#######################################
|
|
409
|
+
function versions_for_title() {
|
|
410
|
+
debug "Getting versions for title $1..."
|
|
411
|
+
local desired_title=$1
|
|
412
|
+
local jscode
|
|
413
|
+
|
|
414
|
+
read -r -d '' jscode <<ENDJAVASCRIPT
|
|
415
|
+
if (parsed_data.titles["${desired_title}"]) {
|
|
416
|
+
result = parsed_data.titles["${desired_title}"]["version_order"].join('\n');
|
|
417
|
+
} else {
|
|
418
|
+
result = \`Unknown title \${desired_title}\n\`;
|
|
419
|
+
}
|
|
420
|
+
ENDJAVASCRIPT
|
|
421
|
+
extract_json_data "$jscode"
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# $1 = the name of a title
|
|
425
|
+
# Output the text to be displayed for the 'details' command for a title
|
|
426
|
+
#######################################
|
|
427
|
+
function details_for_title() {
|
|
428
|
+
debug "Getting details for title $1..."
|
|
429
|
+
local desired_title=$1
|
|
430
|
+
local jscode
|
|
431
|
+
|
|
432
|
+
read -r -d '' jscode <<ENDJAVASCRIPT
|
|
433
|
+
if (parsed_data.titles["${desired_title}"]) {
|
|
434
|
+
var title_data = parsed_data.titles["${desired_title}"];
|
|
435
|
+
|
|
436
|
+
result = \`Details of title \${title_data.title}\n\`;
|
|
437
|
+
result += \`..Display Name: \${title_data.display_name}\n\`;
|
|
438
|
+
result += \`..Description: \${title_data.description}\n\`;
|
|
439
|
+
result += \`..Publisher: \${title_data.publisher}\n\`;
|
|
440
|
+
result += \`..Release Groups: \${title_data.release_groups}\n\`;
|
|
441
|
+
result += \`..Excluded Groups: \${title_data.excluded_groups}\n\`;
|
|
442
|
+
|
|
443
|
+
if (title_data.uninstall_script || title_data.uninstall_ids.length > 0) {
|
|
444
|
+
var uninstallable = 'true';
|
|
445
|
+
} else {
|
|
446
|
+
var uninstallable = 'false';
|
|
447
|
+
}
|
|
448
|
+
result += \`..Uninstallable: \${uninstallable}\n\`;
|
|
449
|
+
|
|
450
|
+
if ( uninstallable == 'true' && title_data.expiration && title_data.expire_paths.length > 0) {
|
|
451
|
+
result += \`..Expires after days of disuse: \${title_data.expiration}\n\`;
|
|
452
|
+
result += \`..Expire Paths: \${title_data.expire_paths}\n\`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if ( title_data.self_service ) {
|
|
456
|
+
result += \`..In Self Service: true\n\`;
|
|
457
|
+
result += \`..Self Service Category: \${title_data.self_service_category}\n\`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
result += \`..Contact: \${title_data.contact_email}\n\`;
|
|
461
|
+
result += \`..Added to Xolo: \${title_data.creation_date} by \${title_data.created_by}\n\`;
|
|
462
|
+
|
|
463
|
+
result += \`..Versions:\n\`;
|
|
464
|
+
if (title_data.versions.length > 0) {
|
|
465
|
+
title_data.versions.forEach(
|
|
466
|
+
function (arrayItem) {
|
|
467
|
+
result += \`....\${arrayItem.version}: \${arrayItem.status}\n\`;
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
} else {
|
|
471
|
+
result += \`....No versions added yet\n\`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
} else {
|
|
475
|
+
result = \`Unknown title ${desired_title}\n\`;
|
|
476
|
+
}
|
|
477
|
+
ENDJAVASCRIPT
|
|
478
|
+
|
|
479
|
+
extract_json_data "$jscode"
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# $1 = the name of a title
|
|
484
|
+
# $2 = the name of a version
|
|
485
|
+
# Output the text to be displayed for the 'details' command for a version
|
|
486
|
+
#######################################
|
|
487
|
+
function details_for_version() {
|
|
488
|
+
debug "Getting details for version $2 of title $1 ..."
|
|
489
|
+
local desired_title=$1
|
|
490
|
+
local desired_version=$2
|
|
491
|
+
local jscode
|
|
492
|
+
|
|
493
|
+
read -r -d '' jscode <<ENDJAVASCRIPT
|
|
494
|
+
if (parsed_data.titles["${desired_title}"]) {
|
|
495
|
+
var title_data = parsed_data.titles["${desired_title}"];
|
|
496
|
+
var version_data = title_data.versions.find(
|
|
497
|
+
function(value, index, array) {
|
|
498
|
+
return value.version == "${desired_version}";
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
if (version_data) {
|
|
503
|
+
result = \`Details of version '\${version_data.version}' of title \${title_data.title}\n\`;
|
|
504
|
+
result += \`..Publish Date: \${version_data.publish_date}\n\`;
|
|
505
|
+
result += \`..Added to Xolo: \${version_data.creation_date} by \${version_data.created_by}\n\`;
|
|
506
|
+
if (version_data.pilot_groups.length > 0) result += \`..Pilot Groups: \${version_data.pilot_groups}\n\`;
|
|
507
|
+
|
|
508
|
+
result += \`..Minimum OS Version: \${version_data.min_os}\n\`;
|
|
509
|
+
if (version_data.max_os) result += \`..Maximum OS Version: \${version_data.max_os}\n\`;
|
|
510
|
+
result += \`..Requires Reboot: \${version_data.reboot}\n\`;
|
|
511
|
+
result += \`..Standalone: \${version_data.standalone}\n\`;
|
|
512
|
+
if (version_data.killapps.length > 0) result += \`..KillApps: \${version_data.killapps}\n\`;
|
|
513
|
+
|
|
514
|
+
result += \`..Status: \${version_data.status}\n\`;
|
|
515
|
+
if (version_data.status == 'released') {
|
|
516
|
+
result += \`..Released: \${version_data.release_date} by \${version_data.released_by}\n\`;
|
|
517
|
+
if (title_data.release_groups.length > 0) result += \`..Release Groups: \${title_data.release_groups}\n\`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (version_data.status == 'deprecated') {
|
|
521
|
+
result += \`..Deprecated: \${version_data.deprecation_date} by \${version_data.deprecated_by}\n\`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (version_data.status == 'skipped') {
|
|
525
|
+
result += \`..Skipped: \${version_data.skipped_date} by \${version_data.skipped_by}\n\`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
} else {
|
|
531
|
+
result = \`Unknown version '${desired_version}' for title ${desired_title}\`;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
} else {
|
|
535
|
+
result = \`Unknown title ${desired_title}\`;
|
|
536
|
+
}
|
|
537
|
+
ENDJAVASCRIPT
|
|
538
|
+
|
|
539
|
+
extract_json_data "$jscode"
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
# $1 = the name of a title
|
|
543
|
+
# Outputs the version script for the title
|
|
544
|
+
# or empty string if not set, or if no matching title
|
|
545
|
+
#######################################
|
|
546
|
+
function version_script_for_title() {
|
|
547
|
+
debug "Getting version script for title $1..."
|
|
548
|
+
local desired_title=$1
|
|
549
|
+
local jscode
|
|
550
|
+
|
|
551
|
+
read -r -d '' jscode <<ENDJAVASCRIPT
|
|
552
|
+
if (parsed_data.titles["${desired_title}"]) {
|
|
553
|
+
var vscript = parsed_data.titles["${desired_title}"]["version_script"];
|
|
554
|
+
result = vscript ? vscript : ''
|
|
555
|
+
} else {
|
|
556
|
+
result = '';
|
|
557
|
+
}
|
|
558
|
+
ENDJAVASCRIPT
|
|
559
|
+
extract_json_data "$jscode"
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
# $1 = the name of a title
|
|
563
|
+
# Outputs the app_name and app_bundle_id for the title, semi-colon separated
|
|
564
|
+
# or empty string if not set, or if no matching title
|
|
565
|
+
#######################################
|
|
566
|
+
function app_data_for_title() {
|
|
567
|
+
debug "Getting app data for title $1..."
|
|
568
|
+
local desired_title=$1
|
|
569
|
+
local jscode
|
|
570
|
+
|
|
571
|
+
read -r -d '' jscode <<ENDJAVASCRIPT
|
|
572
|
+
if (parsed_data.titles["${desired_title}"]) {
|
|
573
|
+
var appname = parsed_data.titles["${desired_title}"]["app_name"];
|
|
574
|
+
var bundleid = parsed_data.titles["${desired_title}"]["app_bundle_id"];
|
|
575
|
+
if (appname && bundleid) {
|
|
576
|
+
result = \`\${appname};\${bundleid}\`;
|
|
577
|
+
} else {
|
|
578
|
+
result = '';
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
result = '';
|
|
582
|
+
}
|
|
583
|
+
ENDJAVASCRIPT
|
|
584
|
+
extract_json_data "$jscode"
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
# $1 = the name of a title
|
|
588
|
+
# Outputs the expiration period (integer of days) for the title
|
|
589
|
+
# or empty string if not set, or if no matching title
|
|
590
|
+
#######################################
|
|
591
|
+
function expiration_for_title() {
|
|
592
|
+
debug "Getting expiration for title $1..."
|
|
593
|
+
local desired_title=$1
|
|
594
|
+
local jscode
|
|
595
|
+
|
|
596
|
+
read -r -d '' jscode <<ENDJAVASCRIPT
|
|
597
|
+
if (parsed_data.titles["${desired_title}"]) {
|
|
598
|
+
var expiration = parsed_data.titles["${desired_title}"]["expiration"];
|
|
599
|
+
result = expiration ? expiration : ''
|
|
600
|
+
} else {
|
|
601
|
+
result = '';
|
|
602
|
+
}
|
|
603
|
+
ENDJAVASCRIPT
|
|
604
|
+
extract_json_data "$jscode"
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
# $1 = the name of a title
|
|
608
|
+
# Outputs the expire paths for the title, one per line
|
|
609
|
+
# or empty string if not set, or if no matching title
|
|
610
|
+
#######################################
|
|
611
|
+
function expire_paths_for_title() {
|
|
612
|
+
debug "Getting expire paths for title $1..."
|
|
613
|
+
local desired_title=$1
|
|
614
|
+
local jscode
|
|
615
|
+
|
|
616
|
+
read -r -d '' jscode <<ENDJAVASCRIPT
|
|
617
|
+
if (parsed_data.titles["${desired_title}"]) {
|
|
618
|
+
var expire_paths = parsed_data.titles["${desired_title}"]["expire_paths"];
|
|
619
|
+
result = expire_paths.length > 0 ? expire_paths.join("\n") : ''
|
|
620
|
+
} else {
|
|
621
|
+
result = '';
|
|
622
|
+
}
|
|
623
|
+
ENDJAVASCRIPT
|
|
624
|
+
extract_json_data "$jscode"
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
# $1 = the name of a title
|
|
628
|
+
# Outputs the currently released version for the title
|
|
629
|
+
# or empty string if not set, or if no matching title
|
|
630
|
+
#######################################
|
|
631
|
+
function released_version_for_title() {
|
|
632
|
+
debug "Getting released version for title $1..."
|
|
633
|
+
local desired_title=$1
|
|
634
|
+
local jscode
|
|
635
|
+
|
|
636
|
+
read -r -d '' jscode <<ENDJAVASCRIPT
|
|
637
|
+
if (parsed_data.titles["${desired_title}"]) {
|
|
638
|
+
var released_version = parsed_data.titles["${desired_title}"]["released_version"];
|
|
639
|
+
result = released_version ? released_version : ''
|
|
640
|
+
} else {
|
|
641
|
+
result = '';
|
|
642
|
+
}
|
|
643
|
+
ENDJAVASCRIPT
|
|
644
|
+
extract_json_data "$jscode"
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
# $1 = the name of a title
|
|
648
|
+
# $2 = the name of a version
|
|
649
|
+
# Output status of a version
|
|
650
|
+
#######################################
|
|
651
|
+
function status_for_version() {
|
|
652
|
+
debug "Getting status for version $2 of title $1 ..."
|
|
653
|
+
local desired_title=$1
|
|
654
|
+
local desired_version=$2
|
|
655
|
+
local jscode
|
|
656
|
+
|
|
657
|
+
read -r -d '' jscode <<ENDJAVASCRIPT
|
|
658
|
+
if (parsed_data.titles["${desired_title}"]) {
|
|
659
|
+
var title_data = parsed_data.titles["${desired_title}"];
|
|
660
|
+
var version_data = title_data.versions.find(
|
|
661
|
+
function(value, index, array) {
|
|
662
|
+
return value.version == "${desired_version}";
|
|
663
|
+
}
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
if (version_data) {
|
|
667
|
+
result = version_data.status;
|
|
668
|
+
} else {
|
|
669
|
+
result = \`Unknown version '${desired_version}' for title ${desired_title}\`;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
} else {
|
|
673
|
+
result = \`Unknown title ${desired_title}\`;
|
|
674
|
+
}
|
|
675
|
+
ENDJAVASCRIPT
|
|
676
|
+
|
|
677
|
+
extract_json_data "$jscode"
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
# Run a Jamf policy trigger
|
|
681
|
+
# $1 = the policy trigger to run
|
|
682
|
+
# $2 = any extra options to pass to the jamf command
|
|
683
|
+
# be_verbose() and be_quiet() will automatically be honored
|
|
684
|
+
#######################################
|
|
685
|
+
function run_jamf_policy_trigger() {
|
|
686
|
+
local policy_trigger=$1
|
|
687
|
+
local jamf_options=$2
|
|
688
|
+
local jamf_verbose=''
|
|
689
|
+
local jamf_quiet=''
|
|
690
|
+
local cmd
|
|
691
|
+
|
|
692
|
+
# debug means verbose
|
|
693
|
+
if be_verbose ; then
|
|
694
|
+
jamf_verbose='-verbose'
|
|
695
|
+
else
|
|
696
|
+
# if told to be quiet, do so
|
|
697
|
+
if be_quiet ; then
|
|
698
|
+
jamf_quiet=1
|
|
699
|
+
|
|
700
|
+
# but if we're refreshing client data as part of
|
|
701
|
+
# another command, always be quiet.
|
|
702
|
+
elif [[ $policy_trigger == $UPDATE_CLIENT_DATA_TRIGGER && "$command" != 'refresh' ]] ; then
|
|
703
|
+
jamf_quiet=1
|
|
704
|
+
fi
|
|
705
|
+
fi
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
cmd="$JAMF policy -trigger $policy_trigger $jamf_options $jamf_verbose"
|
|
709
|
+
debug "Running jamf policy command: $cmd"
|
|
710
|
+
|
|
711
|
+
if [[ -n $jamf_quiet ]] ; then
|
|
712
|
+
policy_output=$( eval "$cmd" )
|
|
713
|
+
else
|
|
714
|
+
policy_output=$( eval "$cmd" | tee /dev/tty )
|
|
715
|
+
fi
|
|
716
|
+
|
|
717
|
+
[[ $policy_output =~ 'Submitting log to https://' ]] && return 0
|
|
718
|
+
|
|
719
|
+
# callers can examine $policy_output if they want to see what happened
|
|
720
|
+
return 1
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
# Refresh the client data
|
|
724
|
+
###############################
|
|
725
|
+
function refresh_client_data() {
|
|
726
|
+
# if we've already refreshed during this run, we're done
|
|
727
|
+
[[ -n "$client_data_refreshed" ]] && return
|
|
728
|
+
|
|
729
|
+
[[ "$command" == 'refresh' ]] && say "Refreshing client data..." || debug "Refreshing client data..."
|
|
730
|
+
|
|
731
|
+
run_jamf_policy_trigger $UPDATE_CLIENT_DATA_TRIGGER
|
|
732
|
+
|
|
733
|
+
client_data_refreshed=1
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
# validate that the title exists
|
|
737
|
+
###############################
|
|
738
|
+
function validate_title() {
|
|
739
|
+
# if we've already validated the title, we're done
|
|
740
|
+
[[ -n "$title_is_valid" ]] && return
|
|
741
|
+
|
|
742
|
+
debug "Validating title: $title"
|
|
743
|
+
|
|
744
|
+
# die if no title given
|
|
745
|
+
[[ -z "$title" ]] && die "No title given.\nUsage: $USAGE\nUse --help for more information."
|
|
746
|
+
|
|
747
|
+
# make sure the JSON data file is up to date
|
|
748
|
+
refresh_client_data
|
|
749
|
+
|
|
750
|
+
# die if no such title
|
|
751
|
+
# all titles in an array
|
|
752
|
+
IFS=$'\n' all_titles=($(all_xolo_titles))
|
|
753
|
+
|
|
754
|
+
# index will be zero if the title is not in the array
|
|
755
|
+
[[ ${all_titles[(Ie)$title]} -eq 0 ]] && die "No such title: $title"
|
|
756
|
+
title_is_valid=1
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
# validate that the version exists
|
|
760
|
+
###############################
|
|
761
|
+
function validate_version() {
|
|
762
|
+
# if we've already validated the version, we're done
|
|
763
|
+
[[ -n "$version_is_valid" ]] && return
|
|
764
|
+
|
|
765
|
+
validate_title
|
|
766
|
+
debug "Validating version: $version"
|
|
767
|
+
|
|
768
|
+
# die if no version given
|
|
769
|
+
[[ -z "$version" ]] && die "No version given.\nUsage: $USAGE\nUse --help for more information."
|
|
770
|
+
|
|
771
|
+
# die if no such version
|
|
772
|
+
all_versions=$(versions_for_title $title)
|
|
773
|
+
debug "All versions for title $title:\n$all_versions"
|
|
774
|
+
# [[ $all_versions =~ (^|\n)$version($|\n) ]] || die "No such version: $version"
|
|
775
|
+
echo "$all_versions" | grep -q "^$version$" || die "No such version: $version"
|
|
776
|
+
version_is_valid=1
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
# Install a title, or a specific version thereof
|
|
780
|
+
###############################
|
|
781
|
+
function install() {
|
|
782
|
+
validate_title
|
|
783
|
+
|
|
784
|
+
# if no version is given, we'll find the current release
|
|
785
|
+
if [[ -z "$version" ]] ; then
|
|
786
|
+
version=$(released_version_for_title $title)
|
|
787
|
+
[[ -z "$version" ]] && die "No current release for title: $title. Specify a version to install."
|
|
788
|
+
|
|
789
|
+
# This trigger always installs the current release via a single policy for the title
|
|
790
|
+
install_trigger="xolo-${title}-install"
|
|
791
|
+
else
|
|
792
|
+
# this is the trigger for a specific version of a title.
|
|
793
|
+
install_trigger="xolo-${title}-${version}-manual-install"
|
|
794
|
+
fi
|
|
795
|
+
|
|
796
|
+
validate_version
|
|
797
|
+
|
|
798
|
+
if [[ -n "$version" ]] ; then
|
|
799
|
+
thing_being_installed="version $version of title $title"
|
|
800
|
+
else
|
|
801
|
+
thing_being_installed="released version of title $title"
|
|
802
|
+
fi
|
|
803
|
+
say "Installing $thing_being_installed"
|
|
804
|
+
|
|
805
|
+
if run_jamf_policy_trigger "${install_trigger}" ; then
|
|
806
|
+
run_recon
|
|
807
|
+
say "Done, installed $thing_being_installed"
|
|
808
|
+
else
|
|
809
|
+
if [[ $policy_output =~ 'No policies were found for the' ]] ; then
|
|
810
|
+
die "Title $title is excluded for this computer or is not installable via xolo"
|
|
811
|
+
else
|
|
812
|
+
die "Install of title $title failed."
|
|
813
|
+
fi
|
|
814
|
+
fi
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
# Unnstall a title
|
|
818
|
+
# This might not do anything if the title is not uninstallable,
|
|
819
|
+
# or if the title is not installed
|
|
820
|
+
###############################
|
|
821
|
+
function uninstall() {
|
|
822
|
+
validate_title
|
|
823
|
+
say "Uninstalling title $title..."
|
|
824
|
+
|
|
825
|
+
if run_jamf_policy_trigger "xolo-${title}-uninstall" ; then
|
|
826
|
+
run_recon
|
|
827
|
+
say "Done, title $title uninstalled"
|
|
828
|
+
else
|
|
829
|
+
if [[ $policy_output =~ 'No policies were found for the' ]] ; then
|
|
830
|
+
die "Title $title is not uninstallable via xolo"
|
|
831
|
+
else
|
|
832
|
+
die "Uninstall of title $title failed."
|
|
833
|
+
fi
|
|
834
|
+
fi
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
# run a jamf recon if requested
|
|
838
|
+
#
|
|
839
|
+
#############################
|
|
840
|
+
function run_recon() {
|
|
841
|
+
[[ -n "$do_recon" ]] || return 0
|
|
842
|
+
|
|
843
|
+
say 'Running jamf recon...'
|
|
844
|
+
if be_verbose ; then
|
|
845
|
+
$JAMF recon -verbose
|
|
846
|
+
elif be_quiet ; then
|
|
847
|
+
$JAMF recon &> /dev/null
|
|
848
|
+
else
|
|
849
|
+
$JAMF recon
|
|
850
|
+
fi
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
# Update installed titles or install new ones
|
|
854
|
+
# scoped to this computer.
|
|
855
|
+
# This is just running 'jamf policy' with its default
|
|
856
|
+
# trigger "recurring check-in".
|
|
857
|
+
###############################
|
|
858
|
+
function update() {
|
|
859
|
+
say "Updating installed titles, or installing newly scoped ones..."
|
|
860
|
+
|
|
861
|
+
debug "Running jamf policy..."
|
|
862
|
+
if be_verbose ; then
|
|
863
|
+
$JAMF policy -verbose
|
|
864
|
+
elif be_quiet ; then
|
|
865
|
+
$JAMF policy &> /dev/null
|
|
866
|
+
else
|
|
867
|
+
$JAMF policy
|
|
868
|
+
fi
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
# List all known titles and their versions and statuses
|
|
872
|
+
###############################
|
|
873
|
+
function list_all_titles() {
|
|
874
|
+
say "All Titles known to Xolo..."
|
|
875
|
+
|
|
876
|
+
if [[ -n "$no_versions" ]] ; then
|
|
877
|
+
all_xolo_titles
|
|
878
|
+
return 0
|
|
879
|
+
fi
|
|
880
|
+
|
|
881
|
+
all_xolo_titles_with_versions
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
# Show detailed information about a title or a version thereof
|
|
885
|
+
###############################
|
|
886
|
+
function show_details() {
|
|
887
|
+
validate_title
|
|
888
|
+
|
|
889
|
+
if [[ -z "$version" ]] ; then
|
|
890
|
+
details_for_title $title
|
|
891
|
+
else
|
|
892
|
+
validate_version
|
|
893
|
+
details_for_version $title $version
|
|
894
|
+
fi
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
# List all installed titles
|
|
898
|
+
###############################
|
|
899
|
+
function list_installed_titles() {
|
|
900
|
+
local ttl
|
|
901
|
+
local app_data
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
verbose "Listing installed titles..."
|
|
905
|
+
|
|
906
|
+
# populate the installed_apps associative array
|
|
907
|
+
get_installed_apps
|
|
908
|
+
|
|
909
|
+
while IFS= read -r ttl; do
|
|
910
|
+
app_data=$(app_data_for_title $ttl)
|
|
911
|
+
|
|
912
|
+
if [[ -n "$app_data" ]] ; then
|
|
913
|
+
debug "App Data: '$app_data'"
|
|
914
|
+
display_title_if_installed_by_app_data $ttl "$app_data"
|
|
915
|
+
else
|
|
916
|
+
display_title_if_installed_by_version_script $ttl
|
|
917
|
+
fi
|
|
918
|
+
done <<<"$(all_xolo_titles)"
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
# given a title and some app data (name, bundle id)
|
|
922
|
+
# output a line if its installed, optionally with the version
|
|
923
|
+
# $1 is the semicolon separated app data, name:bundle_id
|
|
924
|
+
###############################
|
|
925
|
+
function display_title_if_installed_by_app_data() {
|
|
926
|
+
local ttl=$1
|
|
927
|
+
local app_data=$2
|
|
928
|
+
local app_name
|
|
929
|
+
local app_bundle_id
|
|
930
|
+
local inst_app_name
|
|
931
|
+
local inst_app_vers
|
|
932
|
+
local data
|
|
933
|
+
local parts
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
parts=(${(s/;/)app_data})
|
|
937
|
+
app_name=$parts[1]
|
|
938
|
+
app_bundle_id=$parts[2]
|
|
939
|
+
debug "Split App Data: '$app_bundle_id' -> '$app_name'"
|
|
940
|
+
|
|
941
|
+
# This shouldn't happen but...
|
|
942
|
+
[[ -n "$app_name" && -n "$app_bundle_id" ]] || return
|
|
943
|
+
|
|
944
|
+
# is this bundle id installed?
|
|
945
|
+
[[ -n $installed_apps[$app_bundle_id] ]] || return
|
|
946
|
+
|
|
947
|
+
data=$installed_apps[$app_bundle_id]
|
|
948
|
+
parts=(${(s/;/)data})
|
|
949
|
+
inst_app_name=$parts[1]
|
|
950
|
+
inst_app_vers=$parts[2]
|
|
951
|
+
debug "Split Installed Data: '$inst_app_name' -> '$inst_app_vers'"
|
|
952
|
+
|
|
953
|
+
# no go if the app name doesn't match
|
|
954
|
+
[[ "$inst_app_name" == "$app_name" ]] || return
|
|
955
|
+
|
|
956
|
+
if [[ -n "$no_versions" ]] ; then
|
|
957
|
+
echo $ttl
|
|
958
|
+
else
|
|
959
|
+
echo "$ttl ($inst_app_vers)"
|
|
960
|
+
fi
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
# Given a title, use its version script to see if its installed,
|
|
965
|
+
# and if so, output a line for it, optionally with the version
|
|
966
|
+
# $1 is the title
|
|
967
|
+
###############################
|
|
968
|
+
function display_title_if_installed_by_version_script() {
|
|
969
|
+
local title=$1
|
|
970
|
+
local version_script=$(version_script_for_title $title)
|
|
971
|
+
local app_vers=
|
|
972
|
+
|
|
973
|
+
[[ -n "$version_script" ]] || return
|
|
974
|
+
|
|
975
|
+
# Save it to a file, make it executable, run it capturing the output, then delete the file
|
|
976
|
+
tmp_file=$(mktemp -t xolo.$title)
|
|
977
|
+
touch "$tmp_file"
|
|
978
|
+
chmod 700 "$tmp_file"
|
|
979
|
+
echo "$version_script" > "$tmp_file"
|
|
980
|
+
|
|
981
|
+
debug "Running version script for $title from $tmp_file"
|
|
982
|
+
vsout=$("$tmp_file" 2>/dev/null)
|
|
983
|
+
debug "Version Script Output: '$vsout'"
|
|
984
|
+
|
|
985
|
+
rm -f "$tmp_file"
|
|
986
|
+
|
|
987
|
+
[[ -n "$vsout" ]] || return
|
|
988
|
+
|
|
989
|
+
# the output should be something like
|
|
990
|
+
# <result>version</result> if installed, and
|
|
991
|
+
# <result></result> if not installed
|
|
992
|
+
[[ "$vsout" =~ '<result>(.*)</result>' ]] && app_vers=$match[1]
|
|
993
|
+
[[ -n "$app_vers" ]] || return
|
|
994
|
+
|
|
995
|
+
if [[ -n "$no_versions" ]] ; then
|
|
996
|
+
echo $ttl
|
|
997
|
+
else
|
|
998
|
+
echo "$ttl ($app_vers)"
|
|
999
|
+
fi
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
# Expire the title given on the command line, if
|
|
1003
|
+
# - it has an expiration period set
|
|
1004
|
+
# - it has defined expire paths
|
|
1005
|
+
# - none of the paths are running right now
|
|
1006
|
+
# - none of the paths have been used in the defined period
|
|
1007
|
+
###############################
|
|
1008
|
+
function expire() {
|
|
1009
|
+
validate_title
|
|
1010
|
+
|
|
1011
|
+
local now=$(date '+%s')
|
|
1012
|
+
local expiration=$(expiration_for_title $title)
|
|
1013
|
+
local exp_secs=$(( $expiration * 24 * 3600 ))
|
|
1014
|
+
local expire_paths
|
|
1015
|
+
local victim_path
|
|
1016
|
+
local must_have_been_used_since=$(( $now - $exp_secs ))
|
|
1017
|
+
local last_use_epoch
|
|
1018
|
+
local last_use
|
|
1019
|
+
|
|
1020
|
+
# return if no expiration period
|
|
1021
|
+
if [[ -z "$expiration" ]] ;then
|
|
1022
|
+
say "Title $title is not expirable: No expiration period."
|
|
1023
|
+
return
|
|
1024
|
+
fi
|
|
1025
|
+
# this gives us an array of paths
|
|
1026
|
+
IFS=$'\n' expire_paths=($(expire_paths_for_title $title))
|
|
1027
|
+
# return if empty
|
|
1028
|
+
if [[ ${#expire_paths} -eq 0 ]] ; then
|
|
1029
|
+
say "Title $title is not expirable: no expire paths."
|
|
1030
|
+
return
|
|
1031
|
+
fi
|
|
1032
|
+
|
|
1033
|
+
# loop thru the paths and see if any have been used recently
|
|
1034
|
+
for victim_path in $expire_paths ; do
|
|
1035
|
+
|
|
1036
|
+
# don't expire if any expire path doesn't exist
|
|
1037
|
+
if ! [[ -e "$victim_path" ]] ; then
|
|
1038
|
+
say "Expiration Path: '$victim_path' doesn't exist, not expiring $title."
|
|
1039
|
+
return
|
|
1040
|
+
fi
|
|
1041
|
+
|
|
1042
|
+
# if any of the paths are running, we're done
|
|
1043
|
+
if [[ -n $(/usr/bin/pgrep -f "$victim_path") ]] ; then
|
|
1044
|
+
say "Title $title is not expirable right now: $victim_path is running."
|
|
1045
|
+
return
|
|
1046
|
+
fi
|
|
1047
|
+
|
|
1048
|
+
# if any of the paths have been used in the defined period, we're done
|
|
1049
|
+
# The Apple Developer Site says: kMDItemLastUsedDate is the date and time
|
|
1050
|
+
# that the file was last used. This value is updated automatically by
|
|
1051
|
+
# LaunchServices everytime a file is opened by double clicking, or by asking
|
|
1052
|
+
# LaunchServices to open a file.
|
|
1053
|
+
last_use=$(/usr/bin/mdls -name kMDItemLastUsedDate "$victim_path" -raw)
|
|
1054
|
+
# +> 2024-11-25 23:10:00 +0000
|
|
1055
|
+
|
|
1056
|
+
# use install time if no last use time
|
|
1057
|
+
if ! [[ "$last_use" =~ '^[[:digit:]]{4}-' ]] ; then
|
|
1058
|
+
debug "No last use time for $victim_path, using install time."
|
|
1059
|
+
last_use=$(/usr/bin/mdls -name kMDItemDateAdded "$victim_path" -raw)
|
|
1060
|
+
|
|
1061
|
+
# not installed if mdls fails or returns nothing
|
|
1062
|
+
# so don't expire it
|
|
1063
|
+
if [[ "$last_use" =~ '^[[:digit:]]{4}-' ]] || [[ -z "$last_use" ]] ; then
|
|
1064
|
+
say "Title $title is not expirable: $victim_path has no last use or install time."
|
|
1065
|
+
return
|
|
1066
|
+
fi
|
|
1067
|
+
fi
|
|
1068
|
+
|
|
1069
|
+
last_use_epoch=$(/bin/date -j -f "%Y-%m-%d %H:%M:%S %z" "$last_use" +"%s")
|
|
1070
|
+
|
|
1071
|
+
# if the last use time is more recent than the expiration period,
|
|
1072
|
+
# for any path, we're done
|
|
1073
|
+
if [[ "$last_use_epoch" -gt "$must_have_been_used_since" ]] ; then
|
|
1074
|
+
say "Title $title is not expirable right now: $victim_path was used recently."
|
|
1075
|
+
return
|
|
1076
|
+
fi
|
|
1077
|
+
done # for victim_path in $expire_paths
|
|
1078
|
+
|
|
1079
|
+
# if we're here, we can expire the title
|
|
1080
|
+
say "Expiring title $title..."
|
|
1081
|
+
uninstall
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
# MAIN
|
|
1085
|
+
###############################
|
|
1086
|
+
###############################
|
|
1087
|
+
|
|
1088
|
+
function main() {
|
|
1089
|
+
# Parse the command line
|
|
1090
|
+
parse_cli "$@"
|
|
1091
|
+
|
|
1092
|
+
# If the user asked for help, show it and exit
|
|
1093
|
+
if [[ -n "$show_help" ]] ; then
|
|
1094
|
+
show_help
|
|
1095
|
+
exit 0
|
|
1096
|
+
fi
|
|
1097
|
+
|
|
1098
|
+
# If the user asked for the xolo version, show it and exit
|
|
1099
|
+
if [[ -n "$show_xolo_version" ]] ; then
|
|
1100
|
+
echo "$XOLO_VERSION"
|
|
1101
|
+
exit 0
|
|
1102
|
+
fi
|
|
1103
|
+
|
|
1104
|
+
if [[ "$command" != "refresh" ]] ; then
|
|
1105
|
+
# If we're not refreshing, we need to have the client data file
|
|
1106
|
+
# and it needs to be up to date
|
|
1107
|
+
[[ -f "$CLIENT_DATA_JSON_FILE" ]] || die "No client data file found at $CLIENT_DATA_JSON_FILE.\nPlease run the 'refresh' command to update the client data."
|
|
1108
|
+
fi
|
|
1109
|
+
|
|
1110
|
+
[[ -z "$command" ]] && die "No command given.\nUsage: $USAGE\nUse --help for more information."
|
|
1111
|
+
|
|
1112
|
+
case "$command" in
|
|
1113
|
+
install|i)
|
|
1114
|
+
debug "processing command 'install'"
|
|
1115
|
+
install
|
|
1116
|
+
;;
|
|
1117
|
+
uninstall|u)
|
|
1118
|
+
debug "processing command 'uninstall'"
|
|
1119
|
+
uninstall
|
|
1120
|
+
;;
|
|
1121
|
+
update|U)
|
|
1122
|
+
debug "processing command 'update'"
|
|
1123
|
+
update
|
|
1124
|
+
;;
|
|
1125
|
+
refresh|r)
|
|
1126
|
+
debug "processing command 'refresh'"
|
|
1127
|
+
# if 'r' reset to 'refresh' so we can use it in the run_jamf_policy_trigger
|
|
1128
|
+
command=refresh
|
|
1129
|
+
refresh_client_data
|
|
1130
|
+
;;
|
|
1131
|
+
list-titles|lt)
|
|
1132
|
+
debug "processing command 'list_titles'"
|
|
1133
|
+
list_all_titles
|
|
1134
|
+
;;
|
|
1135
|
+
list-installed|li)
|
|
1136
|
+
debug "processing command 'list_installed'"
|
|
1137
|
+
list_installed_titles
|
|
1138
|
+
;;
|
|
1139
|
+
details|d)
|
|
1140
|
+
debug "processing command 'details'"
|
|
1141
|
+
show_details
|
|
1142
|
+
;;
|
|
1143
|
+
expire|e)
|
|
1144
|
+
debug "processing command 'expire'"
|
|
1145
|
+
expire
|
|
1146
|
+
;;
|
|
1147
|
+
help|h)
|
|
1148
|
+
debug "processing command 'help'"
|
|
1149
|
+
show_help
|
|
1150
|
+
;;
|
|
1151
|
+
*)
|
|
1152
|
+
die "Unknown command: $command\nUsage: $USAGE\nUse --help for more information."
|
|
1153
|
+
;;
|
|
1154
|
+
esac
|
|
1155
|
+
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
# RUN!
|
|
1159
|
+
###########################
|
|
1160
|
+
main "$@"
|