depot3 0.0.0a1 → 3.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +55 -1
  3. data/bin/d3 +323 -0
  4. data/bin/d3admin +1011 -0
  5. data/bin/d3helper +354 -0
  6. data/bin/puppytime +334 -0
  7. data/data/d3/com.pixar.d3.RepoMan.plist +23 -0
  8. data/data/d3/d3.conf.example +507 -0
  9. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftAppKit.dylib +0 -0
  10. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftCore.dylib +0 -0
  11. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftCoreData.dylib +0 -0
  12. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftCoreGraphics.dylib +0 -0
  13. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftCoreImage.dylib +0 -0
  14. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftDarwin.dylib +0 -0
  15. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftDispatch.dylib +0 -0
  16. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftFoundation.dylib +0 -0
  17. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftObjectiveC.dylib +0 -0
  18. data/data/d3/d3RepoMan.app/Contents/Info.plist +56 -0
  19. data/data/d3/d3RepoMan.app/Contents/MacOS/d3RepoMan +0 -0
  20. data/data/d3/d3RepoMan.app/Contents/PkgInfo +1 -0
  21. data/data/d3/d3RepoMan.app/Contents/Resources/Base.lproj/MainMenu.nib +0 -0
  22. data/data/d3/d3RepoMan.app/Contents/Resources/last-foreground-times-template.plist +5 -0
  23. data/data/d3/d3RepoMan.app/Contents/_CodeSignature/CodeResources +214 -0
  24. data/data/d3/puppytime/ImageLicenses.txt +165 -0
  25. data/data/d3/puppytime/notification_image +1 -0
  26. data/data/d3/puppytime/opt_out_image +1 -0
  27. data/data/d3/puppytime/slideshow/2008-07-11_White_German_Shepherd_pup_chilling_at_the_Coker_Arboretum.jpg +0 -0
  28. data/data/d3/puppytime/slideshow/2009-04-21_APBT_pup_on_deck.jpg +0 -0
  29. data/data/d3/puppytime/slideshow/A_puppy_Yorkie.jpg +0 -0
  30. data/data/d3/puppytime/slideshow/Alert_Pug_Puppy.jpg +0 -0
  31. data/data/d3/puppytime/slideshow/Australian_Cattle_Dog_puppies_04.JPG +0 -0
  32. data/data/d3/puppytime/slideshow/Beagle_puppy_Cadet.jpg +0 -0
  33. data/data/d3/puppytime/slideshow/Bernese_Mountain_Dog.jpg +0 -0
  34. data/data/d3/puppytime/slideshow/Bloodhound_Puppy.jpg +0 -0
  35. data/data/d3/puppytime/slideshow/Boston_terrier_with_toy.jpg +0 -0
  36. data/data/d3/puppytime/slideshow/Boxer_puppy_fawn_portrai.jpg +0 -0
  37. data/data/d3/puppytime/slideshow/Caracal_kitten.jpg +0 -0
  38. data/data/d3/puppytime/slideshow/Chihuahua_&_Doberman_Pup.jpg +0 -0
  39. data/data/d3/puppytime/slideshow/Cuccioli_di_Margot_a_35_gg_Basenjis.jpg +0 -0
  40. data/data/d3/puppytime/slideshow/Dalmatian_puppy_03.jpg +0 -0
  41. data/data/d3/puppytime/slideshow/GoldenRetrieverPuppyDaisyParker.JPG +0 -0
  42. data/data/d3/puppytime/slideshow/Green_eyed_beige_Chihuahua.jpg +0 -0
  43. data/data/d3/puppytime/slideshow/Let_Sleeping_Dogs_Lie.jpg +0 -0
  44. data/data/d3/puppytime/slideshow/Meatball_-_French_Bulldog_Puppy.jpg +0 -0
  45. data/data/d3/puppytime/slideshow/Oola_-_9_weeks.jpg +0 -0
  46. data/data/d3/puppytime/slideshow/Pancho0008.JPG +0 -0
  47. data/data/d3/puppytime/slideshow/Pomeranian_orange-sable_Coco.jpg +0 -0
  48. data/data/d3/puppytime/slideshow/Pug_puppy_001.jpg +0 -0
  49. data/data/d3/puppytime/slideshow/Puggle_puppy_6_weeks.JPG +0 -0
  50. data/data/d3/puppytime/slideshow/Puli_kan.jpg +0 -0
  51. data/data/d3/puppytime/slideshow/Puppy_French_Bulldog.jpg +0 -0
  52. data/data/d3/puppytime/slideshow/Rocco_the_Bulldog.jpg +0 -0
  53. data/data/d3/puppytime/slideshow/Rottweiler_Face.jpg +0 -0
  54. data/data/d3/puppytime/slideshow/Saint_Bernard_puppy.jpg +0 -0
  55. data/data/d3/puppytime/slideshow/Scottish_froment.jpg +0 -0
  56. data/data/d3/puppytime/slideshow/Shar_pei_puppy_(age_2_months).jpg +0 -0
  57. data/data/d3/puppytime/slideshow/Shiba-Inu_beim_Spielen_im_Schnee.JPG +0 -0
  58. data/data/d3/puppytime/slideshow/Smooth-coat_Border_Collie_puppy..jpg +0 -0
  59. data/data/d3/puppytime/slideshow/Smooth_Dachshund_puppies.jpg +0 -0
  60. data/data/d3/puppytime/slideshow/Snow_dog.jpg +0 -0
  61. data/data/d3/puppytime/slideshow/Taylor_the_Pembroke_Welsh_Corgi.png +0 -0
  62. data/data/d3/puppytime/slideshow/Weim_Pups_001.jpg +0 -0
  63. data/data/d3/puppytime/slideshow/Westie_pups.jpg +0 -0
  64. data/data/d3/puppytime/slideshow/Yellow_Labrador_puppies_(4165737325).jpg +0 -0
  65. data/lib/d3/admin/add.rb +451 -0
  66. data/lib/d3/admin/auth.rb +470 -0
  67. data/lib/d3/admin/edit.rb +297 -0
  68. data/lib/d3/admin/help.rb +396 -0
  69. data/lib/d3/admin/interactive.rb +972 -0
  70. data/lib/d3/admin/options.rb +454 -0
  71. data/lib/d3/admin/prefs.rb +204 -0
  72. data/lib/d3/admin/report.rb +727 -0
  73. data/lib/d3/admin/state.rb +42 -0
  74. data/lib/d3/admin/validate.rb +413 -0
  75. data/lib/d3/admin.rb +42 -0
  76. data/lib/d3/basename.rb +217 -0
  77. data/lib/d3/client/auth.rb +108 -0
  78. data/lib/d3/client/class_methods.rb +766 -0
  79. data/lib/d3/client/class_variables.rb +47 -0
  80. data/lib/d3/client/cli.rb +187 -0
  81. data/lib/d3/client/environment.rb +134 -0
  82. data/lib/d3/client/help.rb +110 -0
  83. data/lib/d3/client/lists.rb +314 -0
  84. data/lib/d3/client/receipt.rb +1173 -0
  85. data/lib/d3/client.rb +45 -0
  86. data/lib/d3/configuration.rb +319 -0
  87. data/lib/d3/constants.rb +60 -0
  88. data/lib/d3/database.rb +488 -0
  89. data/lib/d3/exceptions.rb +44 -0
  90. data/lib/d3/log.rb +271 -0
  91. data/lib/d3/package/aliases.rb +80 -0
  92. data/lib/d3/package/attributes.rb +97 -0
  93. data/lib/d3/package/class_methods.rb +817 -0
  94. data/lib/d3/package/class_variables.rb +46 -0
  95. data/lib/d3/package/client_actions.rb +293 -0
  96. data/lib/d3/package/constants.rb +58 -0
  97. data/lib/d3/package/constructor.rb +191 -0
  98. data/lib/d3/package/getters.rb +164 -0
  99. data/lib/d3/package/mixins.rb +39 -0
  100. data/lib/d3/package/private_methods.rb +227 -0
  101. data/lib/d3/package/questions.rb +95 -0
  102. data/lib/d3/package/server_actions.rb +683 -0
  103. data/lib/d3/package/setters.rb +326 -0
  104. data/lib/d3/package/validate.rb +448 -0
  105. data/lib/d3/package.rb +51 -0
  106. data/lib/d3/puppytime/pending_puppy.rb +108 -0
  107. data/lib/d3/puppytime/puppy_queue.rb +274 -0
  108. data/lib/d3/puppytime.rb +68 -0
  109. data/lib/d3/state.rb +105 -0
  110. data/lib/d3/utility.rb +325 -0
  111. data/lib/d3/version.rb +1 -1
  112. metadata +162 -9
@@ -0,0 +1,1173 @@
1
+ ### Copyright 2016 Pixar
2
+ ###
3
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
4
+ ### with the following modification; you may not use this file except in
5
+ ### compliance with the Apache License and the following modification to it:
6
+ ### Section 6. Trademarks. is deleted and replaced with:
7
+ ###
8
+ ### 6. Trademarks. This License does not grant permission to use the trade
9
+ ### names, trademarks, service marks, or product names of the Licensor
10
+ ### and its affiliates, except as required to comply with Section 4(c) of
11
+ ### the License and to reproduce the content of the NOTICE file.
12
+ ###
13
+ ### You may obtain a copy of the Apache License at
14
+ ###
15
+ ### http://www.apache.org/licenses/LICENSE-2.0
16
+ ###
17
+ ### Unless required by applicable law or agreed to in writing, software
18
+ ### distributed under the Apache License with the above modification is
19
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20
+ ### KIND, either express or implied. See the Apache License for the specific
21
+ ### language governing permissions and limitations under the Apache License.
22
+ ###
23
+ ###
24
+
25
+ module D3
26
+ class Client < JSS::Client
27
+
28
+ ###
29
+ ### Receipt - a d3 package that is currently installed on this machine.
30
+ ###
31
+ ### D3 receipts are stored as their native ruby objects in a YAML file located at D3::Client::Receipt::DATASTORE
32
+ ###
33
+ ### When the module loads, the file is read if it exists and all receipts are available in
34
+ ###
35
+ ### The datastore contains a Hash of D3::Client::Receipt objects, keyed by their basenames (only one
36
+ ### installation of a basename can be on a machine at a time)
37
+ ###
38
+ ###
39
+ ###
40
+ class Receipt
41
+
42
+ ################# Mixin Modules #################
43
+
44
+ include D3::Basename
45
+
46
+ ################# Class Constants #################
47
+
48
+ # This YAML file stores all D3::Client::Receipts on this machine
49
+ DATASTORE = D3::SUPPORT_DIR + "receipts.yaml"
50
+
51
+ # This locks the loading of receipts when there's a potential to
52
+ # write them back ou. See Receipt.load_receipts.
53
+ DATASTORE_LOCKFILE = D3::SUPPORT_DIR + "receipts.lock"
54
+
55
+ # How many seconds by default to keep trying to get the datastore lockfile.
56
+ DATASTORE_LOCK_TIMEOUT = 10
57
+
58
+ # If a lockfile is this many seconds old, warn that it might be stale and need
59
+ # manual cleanup. 600 secs = 10 min
60
+ DATASTORE_STALE_LOCK_AGE = 600
61
+
62
+ # This plist contains the last time any app was brought to the
63
+ # foreground. It's updated by the helper app d3RepoMan.app
64
+ # which should always be running if expiration is turned on.
65
+ #LAST_APP_USAGE_FILE = D3::SUPPORT_DIR + "last-foreground-times.plist"
66
+
67
+ # This dir contains a plist for each GUI user, containing
68
+ # the last time any app was brought to the foreground for that user
69
+ # It's updated by the helper app d3RepoMan.app
70
+ # which should always be running while a GUI user is logged in
71
+ # if expiration is turned on.
72
+ LAST_APP_USAGE_DIR = D3::SUPPORT_DIR + "Usage"
73
+
74
+ # This is the process (as listed in the output of '/bin/ps -A -c -o comm')
75
+ # that updates the LAST_APP_USAGE_FILE. If it isn't running as root
76
+ # when expiration is attempted, then expiration won't happen.
77
+ APP_USAGE_MONITOR_PROC = "d3RepoMan"
78
+
79
+ # The newest of the plists in the LAST_APP_USAGE_DIR must have been
80
+ # updated within the last X number of seconds, or else we assume
81
+ # either no one's logged in for a while, or something's wrong with the
82
+ # usage monitoring, since nothing new has come to the foreground
83
+ # in that long. If so, nothing will be expired.
84
+ # Default is 24 hours
85
+ MAX_APP_USAGE_UPDATE_AGE = 60 * 60 * 24
86
+
87
+ # These args are required when creating a new D3::Client::Receipt
88
+ REQUIRED_INIT_ARGS = [
89
+ :basename,
90
+ :version,
91
+ :revision,
92
+ :admin,
93
+ :id,
94
+ :jamf_rcpt_file,
95
+ :status
96
+ ]
97
+
98
+ # Only these attributes can be changed after a receipt is created
99
+ CHANGABLE_ATTRIBS = [
100
+ :status,
101
+ :removable,
102
+ :pre_remove_script_id,
103
+ :post_remove_script_id,
104
+ :expiration,
105
+ :expiration_path,
106
+ :prohibiting_process
107
+ ]
108
+
109
+ ################## Class Variables #################
110
+
111
+ ### The current receipts.
112
+ ### See D3::Client::Receipt.load_receipts and D3::Client::Receipt.all
113
+ @@installed_rcpts = nil
114
+
115
+ ### Do we currently have the rw lock?
116
+ @@got_lock = nil
117
+
118
+ ################# Class Methods #################
119
+
120
+ ### Load in the existing rcpt database if it exists.
121
+ ### This makes them available in @@installed_rcpts and from
122
+ ### D3::Client::Receipt.all
123
+ ###
124
+ ### When loading read-write, if another process has loaded them read-write,
125
+ ### and hasn't saved them yet, a lock file will be present and this load
126
+ ### will retry for lock_timeout seconds before raising an exception
127
+ ###
128
+ ### @param rw[Boolean] Load the receipts read-write, meaning that a lock file
129
+ ### is created and changes can be saved. Defaults to false.
130
+ ###
131
+ ### @param lock_timeout[Integer] How many seconds to keep trying to get the
132
+ ### read-write lock, when loading read-write.
133
+ ###
134
+ ### @return [void]
135
+ ###
136
+ def self.load_receipts(rw = false, lock_timeout = DATASTORE_LOCK_TIMEOUT)
137
+
138
+ # have we already loaded them?
139
+ # (use self.reload if needed)
140
+ return if @@installed_rcpts
141
+
142
+ D3.log "Loading receipts, #{rw ? 'read-write' : 'read-only'}", :debug
143
+
144
+ # get the lock if needed
145
+ self.get_datastore_lock(lock_timeout) if rw
146
+
147
+ @@installed_rcpts = DATASTORE.file? ? YAML.load(DATASTORE.read) : {}
148
+
149
+ D3.log "Receipts loaded", :debug
150
+ end # seld.load_receipts
151
+
152
+ ### Reload the existing rcpt database
153
+ ###
154
+ ### @param rw[Boolean] Load the receipts read-write, meaning that a lock file
155
+ ### is created and changes can be saved. Defaults to false.
156
+ ###
157
+ ### @param lock_timeout[Integer] How many seconds to keep trying to get the
158
+ ### read-write lock, when loading read-write.
159
+ ###
160
+ ### @return [void]
161
+ ###
162
+ def self.reload_receipts(rw = false, lock_timeout = DATASTORE_LOCK_TIMEOUT)
163
+
164
+ # if we haven't loaded them at all yet, just do that.
165
+ unless @@installed_rcpts
166
+ self.load_receipts rw, lock_timeout
167
+ return
168
+ end # unless @@installed_rcpts
169
+
170
+ D3.log "Reloading receipts, #{rw ? 'read-write' : 'read-only'}", :debug
171
+
172
+ # Are we trying to re-load with rw?
173
+ if rw
174
+ # if we already have the lock, then we don't need to get it again
175
+ self.get_datastore_lock(lock_timeout) unless @@got_lock
176
+ else
177
+ # not reloading rw, so release the lock if we have it
178
+ self.release_datastore_lock if @@got_lock
179
+ end
180
+
181
+ # reload it
182
+ @@installed_rcpts = DATASTORE.file? ? YAML.load(DATASTORE.read) : {}
183
+ D3.log "Receipts reloaded", :debug
184
+ end # self.reload_receipts
185
+
186
+ ### Write existing rcpt database to disk
187
+ ###
188
+ ### @return [void]
189
+ ###
190
+ def self.save_receipts(release_lock = true)
191
+ raise JSS::MissingDataError, "Receipts not loaded, can't save." unless @@installed_rcpts
192
+ D3.log "Saving receipts", :debug
193
+
194
+ unless @@got_lock
195
+ D3.log "Receipts were loaded read-only, can't save", :error
196
+ raise JSS::UnsupportedError,"Receipts were loaded read-only, can't save"
197
+ end
198
+
199
+ # ensure any deleted rcpts are gone
200
+ @@installed_rcpts.delete_if{|basename, rcpt| rcpt.deleted? }
201
+
202
+ DATASTORE.parent.mktree unless DATASTORE.parent.directory?
203
+ DATASTORE.jss_save YAML.dump(@@installed_rcpts)
204
+ D3.log "Receipts saved", :debug
205
+ if release_lock
206
+ self.release_datastore_lock
207
+ end
208
+ end #self.save_receipts
209
+
210
+ ### Try to get the lock for read-write access to the datastore.
211
+ ### Raise an exception if we fail after the timeout
212
+ ###
213
+ ### @param lock_timeout[Integer] How many seconds to keep trying to get the lock?
214
+ ###
215
+ ### @return [void]
216
+ ###
217
+ def self.get_datastore_lock (lock_timeout = DATASTORE_LOCK_TIMEOUT)
218
+ D3.log "Attempting to get receipt datastore write lock.", :debug
219
+ # try to get it 10x per second...
220
+ if DATASTORE_LOCKFILE.exist?
221
+ D3.log "Lock in use, retrying for #{lock_timeout} secs", :debug
222
+ max_tries = lock_timeout * 10
223
+ tries = 0
224
+ while tries < max_tries do
225
+ sleep 0.1
226
+ tries += 1 if DATASTORE_LOCKFILE.exist?
227
+ end # while
228
+ end # if DATASTORE_LOCKFILE.exist?
229
+
230
+ if DATASTORE_LOCKFILE.exist?
231
+ errmsg = "Couldn't get receipt write lock after #{lock_timeout} seconds."
232
+ lockfile_age = (Time.now - DATASTORE_LOCKFILE.ctime).to_i
233
+
234
+ # if its stale, warn that it might need manual fixing
235
+ errmsg += " Potentially stale. Please investigate manually." if lockfile_age > DATASTORE_STALE_LOCK_AGE
236
+ D3.log errmsg, :error
237
+ raise JSS::TimeoutError, errmsg
238
+ else
239
+ DATASTORE_LOCKFILE.parent.mkpath
240
+ DATASTORE_LOCKFILE.jss_save $$.to_s
241
+ D3.log "Acquired write lock on receipt datastore.", :debug
242
+ @@got_lock = true
243
+ end
244
+ end #self.get_datastore_lock
245
+
246
+ ### Release the rw lock on the datastore, if we have it.
247
+ ###
248
+ def self.release_datastore_lock
249
+ return nil unless @@got_lock
250
+ DATASTORE_LOCKFILE.delete if DATASTORE_LOCKFILE.exist?
251
+ D3.log "Receipt datastore write lock released", :debug
252
+ @@got_lock = false
253
+ end # self.release_datastore_lock
254
+
255
+ ### Force the release of the lock, regardless of who has it
256
+ ### Useful for testing, but very dangerous - could cause data loss.
257
+ ###
258
+ def self.force_clear_datastore_lock
259
+ D3.log "Force-clearing the receipt write lock", :debug
260
+ DATASTORE_LOCKFILE.delete if DATASTORE_LOCKFILE.exist?
261
+ @@got_lock = false
262
+ end
263
+
264
+ ### Do we currently have the rw lock on the rcpt file?
265
+ ###
266
+ ### @return [boolean]
267
+ ###
268
+ def self.got_lock?
269
+ @@got_lock
270
+ end
271
+
272
+ ### Add a D3::Client::Receipt to the local rcpt database
273
+ ###
274
+ ### @param receipt[D3::Client::Receipt] the receipt to add
275
+ ###
276
+ ### @pararm replace[Boolean] overwrite the existing receipt for this basename?
277
+ ###
278
+ ### @return [void]
279
+ ###
280
+ def self.add_receipt(receipt, replace = false)
281
+ raise JSS::InvalidDataError, "Argument must be a D3::Client::Receipt" unless receipt.is_a? D3::Client::Receipt
282
+ D3.log "Attempting to #{replace ? "replace" : "add"} receipt for #{receipt.edition}.", :debug
283
+ self.reload_receipts :rw
284
+ begin
285
+ unless replace
286
+ if @@installed_rcpts.member? receipt.basename
287
+ raise JSS::AlreadyExistsError, "There's already a receipt on this machine for basemame '#{receipt.basename}'"
288
+ end # if
289
+ end # unless replace
290
+
291
+ @@installed_rcpts[receipt.basename] = receipt
292
+ self.save_receipts
293
+ D3.log "#{replace ? "Replaced" : "Added"} receipt for #{receipt.edition}", :info
294
+
295
+ ensure
296
+ # always release the rw lock even after an error
297
+ self.release_datastore_lock
298
+ end # begin
299
+ end # self.add_receipt
300
+
301
+ ### Delete a D3::Client::Receipt from the local databse
302
+ ###
303
+ ### @return [void]
304
+ ###
305
+ def self.remove_receipt(basename)
306
+
307
+ D3.log "Attempting to remove receipt for basename #{basename}", :info
308
+
309
+ self.reload_receipts :rw
310
+ begin
311
+ old_rcpt = self.all[basename]
312
+ if old_rcpt
313
+ @@installed_rcpts.delete basename
314
+ D3.log "Removed receipt for #{old_rcpt.edition}", :debug
315
+
316
+ self.save_receipts
317
+ else
318
+ D3.log "No receipt for basename #{basename}", :debug
319
+ end # if old_rcpt
320
+ ensure
321
+ self.release_datastore_lock
322
+ end # begin
323
+
324
+ end # self.remove_receipt
325
+
326
+ ### An alias of {self.remove_receipt}
327
+ def self.delete_receipt(basename) ; self.remove_receipt(basename) ; end # self.delete_receipt
328
+
329
+ ### Given a basename, edition, or id return the matching D3::Receipt
330
+ ### or nil if no match.
331
+ ### If a basename is used, any edition installed will
332
+ ### be returned if there is one.
333
+ ###
334
+ ### If an edition or id is used, nil will be returned unless that
335
+ ### exact pkg is installed.
336
+ ###
337
+ ### @param rcpt_to_find[String] basename or edition for which to return
338
+ ### the receipt
339
+ ###
340
+ ### @return [D3::Client::Receipt, nil] the matching receipt, if found
341
+ ###
342
+ def self.find_receipt (rcpt_to_find)
343
+ if self.all.keys.include? rcpt_to_find
344
+ return self.all[rcpt_to_find]
345
+ end
346
+ self.all.values.each do |rcpt|
347
+ return rcpt if rcpt.edition == rcpt_to_find or rcpt.id == rcpt_to_find.to_i
348
+ end
349
+ return nil
350
+ end
351
+
352
+ ### A hash of all d3 receipts currently installed on this machine.
353
+ ### keyed by their basenames. (Only one edition of a basename can be installed at a time)
354
+ ###
355
+ ### @param refresh[Boolean] Should the data be re-read from disk?
356
+ ###
357
+ ### @return [Hash{String => D3::Client::Receipt}] the receipts for the currently installed pkgs.
358
+ ###
359
+ def self.all (refresh = false)
360
+ refresh = true if @@installed_rcpts.nil?
361
+ self.reload_receipts if refresh
362
+ @@installed_rcpts
363
+ end # self.all
364
+
365
+ ### Return an array of the
366
+ ### basenames of all installed d3 pkgs. This doesn't
367
+ ### include those items installed by other casper methods
368
+ ###
369
+ def self.basenames(refresh = false)
370
+ self.all(refresh).keys
371
+ end # self.basenames
372
+
373
+ ### Return a hash of D3::Client::Receipt
374
+ ### objects for all installed pilot d3 pkgs, keyed by their basenames
375
+ ###
376
+ ### @return [Hash] All pilot receipts
377
+ ###
378
+ def self.pilots(refresh = false)
379
+ self.all(refresh).select{|b,r| r.pilot? }
380
+ end # installed_pkgs
381
+
382
+ ### Return a hash of D3::Client::Receipt
383
+ ### objects for all installed live d3 pkgs, keyed by their basenames
384
+ ###
385
+ ### @return [Hash] All live receipts
386
+ ###
387
+ def self.live(refresh = false)
388
+ self.all(refresh).select {|b,r| r.live? }
389
+ end # installed_pkgs
390
+
391
+ ### Return a hash of D3::Client::Receipt
392
+ ### objects for all installed deprecated d3 pkgs, keyed by their basenames
393
+ ###
394
+ ### @return [Hash] all deprecated receipts
395
+ ###
396
+ def self.deprecated(refresh = false)
397
+ self.all(refresh).select {|b,r| r.deprecated? }
398
+ end # installed_pkgs
399
+
400
+ ### Return a hash of D3::Client::Receipt
401
+ ### objects for all installed frozen d3 receipts, keyed by their basenames
402
+ ###
403
+ ### @return [Hash] all frozen receipts
404
+ ###
405
+ def self.frozen(refresh = false)
406
+ self.all(refresh).select {|b,r| r.frozen? }
407
+ end # installed_pkg
408
+
409
+ ### Return a hash of D3::Client::Receipt objects for all manually installed
410
+ ### pkgs (live or pilot) keyed by their basenames
411
+ ###
412
+ ### @return [Hash] all manually-installed receipts
413
+ ###
414
+ def self.manual(refresh = false)
415
+ self.all(refresh).select {|b,r| r.manual? }
416
+ end # installed_pkgs
417
+
418
+ ### An array of apple bundle id's for all .[m]pkgs
419
+ ### currently known to the OS's receipt db
420
+ ###
421
+ def self.os_pkg_rcpts(refresh = false)
422
+ @@os_pkg_rcpts = nil if refresh
423
+ return @@os_pkg_rcpts if @@os_pkg_rcpts
424
+ @@os_pkg_rcpts = `#{JSS::Composer::PKG_UTIL} --pkgs`.split("\n")
425
+ end
426
+
427
+ ### Rebuild the receipt database by reading the jamf receipts
428
+ ### and using server data.
429
+ ###
430
+ ### @return [void]
431
+ ###
432
+ def self.rebuild_database
433
+ orig_rcpts = self.all :refresh
434
+ new_rcpts = {}
435
+
436
+ jamf_rcpts = JSS::Client::RECEIPTS_FOLDER.children
437
+
438
+ D3::Package.all.values.each do |d3_pkg|
439
+
440
+ next unless jamf_rcpts.include? d3_pkg.receipt
441
+
442
+ # do we already have a rcpt for this edition?
443
+ if orig_rcpts[d3_pkg.basename] and (orig_rcpts[d3_pkg.basename].edition == d3_pkg.edition)
444
+ orig_rcpt = orig_rcpts[d3_pkg.basename]
445
+ else
446
+ orig_rcpt = nil
447
+ end
448
+
449
+ # if there's more than one of the same basename (which means
450
+ # someone installed a d3 pkg via non-d3 means) then
451
+ # which one wins? I say the last one, but log it.
452
+ if new_rcpts.keys.include? d3_pkg.basename
453
+ D3.log "Rebuilding local receipt database: multiple Casper installs of basename '#{d3_pkg.basename}'", :warn
454
+ new_rcpts.delete d3_pkg.basename
455
+ end # new_rcpts.keys.include? d3_pkg.basename
456
+
457
+ new_rcpts[d3_pkg.basename] = D3::Client::Receipt.new(:basename => d3_pkg.basename,
458
+ :version => d3_pkg.version,
459
+ :revision => d3_pkg.revision,
460
+ :admin => (orig_rcpt ? orig_rcpt.admin : "unknown"),
461
+ :installed_at => (orig_rcpt ? orig_rcpt.installed_at : Time.now),
462
+ :id => d3_pkg.id,
463
+ :status => d3_pkg.status,
464
+ :jamf_rcpt_file => d3_pkg.receipt,
465
+ :apple_pkg_ids => d3_pkg.apple_receipt_data.map{|r| r[:apple_pkg_id]},
466
+ :removable => d3_pkg.removable,
467
+ :pre_remove_script_id => d3_pkg.pre_remove_script_id,
468
+ :post_remove_script_id => d3_pkg.post_remove_script_id,
469
+ :expiraation => d3_pkg.expiraation,
470
+ :expiraation_path => d3_pkg.expiraation_path
471
+ )
472
+
473
+ end # .each do |d3_pkg|
474
+
475
+ @@installed_rcpts = new_rcpts
476
+ self.save_receipts
477
+
478
+ end # rebuild db
479
+
480
+
481
+ ################# Attributes #################
482
+
483
+ # @return [Pathnamee] the JAMF rcpt file for this installation
484
+ attr_reader :jamf_rcpt_file
485
+
486
+ # @return [Time] when was it installed?
487
+ attr_reader :installed_at
488
+
489
+ # @return [Array<String>] if its an apple pkg, what pkg_id's does it install?
490
+ attr_reader :apple_pkg_ids
491
+
492
+ # @return [Boolean] was this pkg manually installed?
493
+ attr_reader :manually_installed
494
+ alias manual? manually_installed
495
+
496
+ # @return [Boolean] can it be uninstalled?
497
+ attr_accessor :removable
498
+ alias removable? removable
499
+
500
+ # @return [Integer,nil] the jss id of the pre-remove-script
501
+ attr_accessor :pre_remove_script_id
502
+ alias pre_remove_script? pre_remove_script_id
503
+
504
+ # @return [Integer,nil] the jss id of the post-remove-script
505
+ attr_accessor :post_remove_script_id
506
+ alias post_remove_script? post_remove_script_id
507
+
508
+ # @return [Boolean] is the expiration on this rcpt a custom one?
509
+ # If so, it'll be carried forward when auto-updates occur
510
+ attr_accessor :custom_expiration
511
+
512
+ # @return [Boolean] is this rcpt exempt from auto-updates to its
513
+ # basename? If so, d3 sync will not update it, but a manual
514
+ # d3 install still can, and will re-enable syncs
515
+ attr_accessor :frozen
516
+
517
+ # @return [Time, nil] When was this app last used.
518
+ # nil if never checked, or no @expiration_path
519
+ attr_reader :last_usage
520
+
521
+ # @return [Time, nil] When was @last_usage updated?
522
+ # nil if never checked, or no @expiration_path
523
+ attr_reader :last_usage_as_of
524
+
525
+ ################# Constructor #################
526
+
527
+ ### Args are:
528
+ ### - :basename, required
529
+ ### - :version, required
530
+ ### - :revision, required
531
+ ### - :admin, required
532
+ ### - :id, required
533
+ ### - :status, required, :pilot or :live (or rarely :deprecated)
534
+ ### - :jamf_rcpt_file, required
535
+ ###
536
+ ### - :apple_pkg_ids, optional in general, required for .pkg installers
537
+ ### - :installed_at, optional, defaults to Time.now
538
+ ###
539
+ ### - :removable, optional, defaults to false
540
+ ### - :frozen, optional, defaults to false
541
+ ### - :pre_remove_script_id, optional
542
+ ### - :post_remove_script_id, optional
543
+ ###
544
+ def initialize(args = {})
545
+
546
+ missing_args = REQUIRED_INIT_ARGS - args.keys
547
+ unless missing_args.empty?
548
+ raise JSS::MissingDataError, "D3::Client::Receipt initialization requires these arguments: :#{REQUIRED_INIT_ARGS.join(', :')}"
549
+ end
550
+
551
+ args[:installed_at] ||= Time.now
552
+
553
+ @basename = args[:basename]
554
+ @version = args[:version]
555
+ @revision = args[:revision]
556
+ @admin = args[:admin]
557
+ @id = args[:id]
558
+ @status = args[:status]
559
+
560
+
561
+ # if we were given a string, convert to a Pathname
562
+ # and if it was just a filename, add the Receipts Folder path
563
+ @jamf_rcpt_file = Pathname.new args[:jamf_rcpt_file]
564
+ if @jamf_rcpt_file.parent != JSS::Client::RECEIPTS_FOLDER
565
+ @jamf_rcpt_file = JSS::Client::RECEIPTS_FOLDER + @jamf_rcpt_file
566
+ end
567
+
568
+ @apple_pkg_ids = args[:apple_pkg_ids]
569
+ @installed_at = args[:installed_at]
570
+
571
+ @removable = args[:removable]
572
+ @prohibiting_process = args[:prohibiting_process]
573
+ @frozen = args[:frozen]
574
+ @pre_remove_script_id = args[:pre_remove_script_id]
575
+ @post_remove_script_id = args[:post_remove_script_id]
576
+
577
+ @expiration = args[:expiration].to_i
578
+ @expiration_path = args[:expiration_path]
579
+ @custom_expiration = args[:custom_expiration]
580
+
581
+ @manually_installed = (@admin != D3::AUTO_INSTALL_ADMIN)
582
+ @package_type = @jamf_rcpt_file.to_s.end_with?(".dmg") ? :dmg : :pkg
583
+
584
+ end #initialize
585
+
586
+ ################# Public Instance Methods #################
587
+
588
+
589
+
590
+ ### UnInstall this pkg, and return the output of 'jamf uninstall' or
591
+ ### "receipts removed"
592
+ ###
593
+ ### If there's a pre-remove script, and it exits with a status of 111,
594
+ ### the d3 & jamf receipts are removed, but the actual uninstall doesn't
595
+ ### happen. This would be usefull if the uninstall process is too complex
596
+ ### for 'jamf uninstall' and is totally performed by the script.
597
+ ###
598
+ ### For receipts from .pkg installers, the force option will force deletion
599
+ ### even if the JSS isn't available. It does this by using the
600
+ ### @apple_pkg_ids with pkgutil to delete the files that were installed.
601
+ ### No pre- or post- remove scripts will be run. Use with caution.
602
+ ###
603
+ ### @param verbose[Boolean] be verbose to stdout
604
+ ###
605
+ ### @param force[Boolean] .(m)pkg receipts only!
606
+ ### Should the uninstall happen even if the JSS isn't available?
607
+ ### No pre- or post- scripts will be run.
608
+ ###
609
+ ### @return [void]
610
+ ###
611
+ def uninstall (verbose = false, force = D3::forced?)
612
+
613
+ raise D3::UninstallError, "#{edition} is not uninstallable" unless self.removable?
614
+
615
+ depiloting = pilot? && skipped?
616
+
617
+ begin # ...ensure...
618
+ if uninstall_prohibited_by_process? and (not force)
619
+ raise D3::InstallError, "#{edition} cannot be uninstalled now because '#{@prohibiting_process}' is running."
620
+ end
621
+ D3::Client.set_env :removing, edition
622
+ D3.log "Uninstalling #{edition}", :warn
623
+
624
+ # run a preflight if needed.
625
+ if pre_remove_script?
626
+ (exit_status, output) = run_pre_remove verbose
627
+ if exit_status == 111
628
+ delete
629
+ D3.log "pre_remove script exited 111, deleted receipt for #{edition} but not doing any more.", :info
630
+ return true
631
+ elsif exit_status != 0
632
+ raise D3::UninstallError, "Error running pre_remove script (exited #{exit_status}), not uninstalling #{edition}"
633
+ end # flight_status[0] == 111
634
+ end # if preflight?
635
+
636
+ # if it is still on the server...
637
+ if JSS::Package.all_ids.include? @id
638
+ # uninstall the pkg
639
+ D3.log "Running 'jamf uninstall' of #{edition}", :debug
640
+ uninstall_worked = JSS::Package.new(:id => @id).uninstall(:verbose => verbose).exitstatus == 0
641
+
642
+ # if it isn't on the server any more....
643
+ else
644
+ D3.log "Package is gone from server, no index available", :info
645
+
646
+ # if forced, deleting the rcpt is 'uninstalling'
647
+ if force
648
+ D3.log "Force-deleting receipt for #{edition}.", :info
649
+ uninstall_worked = true
650
+
651
+ # no force
652
+ else
653
+ # we can't do anything with dmgs
654
+ if @package_type == :dmg
655
+ D3.log "Package was a .dmg, can't uninstall.\n Use --force to remove the receipt", :error
656
+ uninstall_worked = false
657
+ else
658
+ uninstall_worked = uninstall_via_apple_rcpt
659
+ end # if @package_type == :dmg
660
+ end # if force
661
+
662
+ end # JSS::Package.all_ids.include? @id
663
+
664
+ ## Uninstall worked, so do more things and stuffs
665
+ if uninstall_worked
666
+
667
+ # remove this rcpt
668
+ delete
669
+ D3.log "Done, uninstalled #{edition}", :warn
670
+ # run a postflight if needed
671
+ if post_remove_script?
672
+ (exit_status, output) = run_post_remove verbose
673
+ if exit_status != 0
674
+ raise D3::UninstallError, "Error running post_remove script (exited #{exit_status}) for #{edition}"
675
+ end
676
+ end # if post_install_script?
677
+
678
+ # uninstall failed, but force deletes rececipt
679
+ else
680
+ if force
681
+ D3.log "Uninstall failed, but force-deleting receipt for #{edition}.", :warn
682
+ delete
683
+ else
684
+ raise D3::UninstallError, "There was a problem uninstalling #{edition}"
685
+ end # if force
686
+ end #if uninstall_worked
687
+
688
+ # do any sync-type auto installs if we just removed a pilot
689
+ # then the machine will get any live edition if it should.
690
+ D3::Client.do_auto_installs(OpenStruct.new) if depiloting
691
+
692
+ ensure
693
+ D3::Client.unset_env :removing
694
+ end # begin...ensure
695
+
696
+
697
+ end #uninstall
698
+
699
+ ### Run the pre-remove script, return the exit status and output
700
+ ###
701
+ ### @param verbose[Boolean] run verbosely?
702
+ ###
703
+ ### @return [Array<Integer, String>] the exit status and output of the script
704
+ ###
705
+ def run_pre_remove (verbose = false)
706
+ D3::Client.set_env :pre_remove, edition
707
+ D3.log "Running pre_remove script", :debug
708
+ begin
709
+ result = JSS::Script.new(:id => @pre_remove_script_id).run :verbose => verbose, :show_output => verbose
710
+ rescue D3::ScriptError
711
+ raise PreRemoveError, $!
712
+ ensure
713
+ D3::Client.unset_env :pre_remove
714
+ end
715
+ D3.log "Finished pre_remove script", :debug
716
+ return result
717
+ end
718
+
719
+ ### Run the post-remove script, return the exit status and output
720
+ ###
721
+ ### @param verbose[Boolean] run verbosely?
722
+ ###
723
+ ### @return [Array<Integer, String>] the exit status and output of the script
724
+ ###
725
+ def run_post_remove (verbose = false)
726
+ D3::Client.set_env :post_remove, edition
727
+ D3.log "Running post_remove script", :debug
728
+ begin
729
+ result = JSS::Script.new(:id => @post_remove_script_id).run :verbose => verbose, :show_output => verbose
730
+ rescue D3::ScriptError
731
+ raise PostRemoveError, $!
732
+ ensure
733
+ D3::Client.unset_env :post_remove
734
+ end
735
+ D3.log "Finished post_remove script", :debug
736
+ return result
737
+ end
738
+
739
+ ### Uninstall this .pkg by looking up the files it installed via
740
+ ### pkgutil and deleting them directly. Doesn't talk to the JSS
741
+ ### and only works for .pkg installers (.dmg installers don't
742
+ ### write their file lists to the local package db.)
743
+ ### This means that it won't run pre/post remove scripts either!
744
+ ###
745
+ ### @param verbose[Boolean] Should each deleted file be meentioned
746
+ ###
747
+ ### @return [Boolean] Was the uninstall successful?
748
+ ###
749
+ def uninstall_via_apple_rcpt (verbose = false)
750
+
751
+ D3.log "Uninstalling #{edition} via Apple pkg receipts", :debug
752
+ raise D3::UninstallError, "#{edition} is not a .pkg installer. Can't use Apple receipts." if @package_type == :dmg
753
+ to_delete = {}
754
+ begin
755
+ installed_apple_rcpts = `#{JSS::Composer::PKG_UTIL} --pkgs`.split("\n")
756
+ @apple_pkg_ids.each do |pkgid|
757
+
758
+ unless installed_apple_rcpts.include? pkgid
759
+ raise D3::UninstallError, "No local Apple receipt for '#{pkgid}'"
760
+ end
761
+ # this gets them in reverse order, so we can
762
+ # delete files and then test for and delete empty dirs on the way
763
+ to_delete[pkgid] = `#{JSS::Composer::PKG_UTIL} --files '#{pkgid}' 2>/dev/null`.split("\n").reverse
764
+ raise D3::UninstallError, "Error querying pkg file list for '#{pkgid}'" if $CHILD_STATUS.exitstatus > 0
765
+ end # each pkgid
766
+
767
+ to_delete.each do |pkgid, paths|
768
+ D3.log "Deleting items installed by apple pkg-id #{pkgid}", :debug
769
+ paths.each do |path|
770
+ target = Pathname.new "/#{path}"
771
+ target.delete if target.file?
772
+ target.rmdir if target.directory? and target.children.empty?
773
+ D3.log "Deleted #{path}", :debug
774
+ end # each path
775
+ system "#{JSS::Composer::PKG_UTIL} --forget '#{pkgid}' &>/dev/null"
776
+ end # each |pkgid, paths|
777
+ rescue
778
+ D3.log $!, :warn
779
+ D3.log_backtrace
780
+ return false
781
+ end # begin
782
+ return true
783
+ end # uninstall_via_apple_rcpt
784
+
785
+ ### Repair any missing or invalid data
786
+ ### in the receipt based on the matching D3::Package data
787
+ ###
788
+ ### @return [void]
789
+ ###
790
+ def repair
791
+ raise JSS::UnsupportedError, "This receipt has been deleted" if @deleted
792
+
793
+ d3_pkg = D3::Package.new :id => @id
794
+
795
+ @basename = d3_pkg.basename
796
+ @version = d3_pkg.version
797
+ @revision = d3_pkg.revision
798
+ @admin ||= "Repaired"
799
+ @status = d3_pkg.status
800
+ @jamf_rcpt_file = d3_pkg.receipt
801
+ @apple_pkg_ids = d3_pkg.apple_receipt_data.map{|r| r[:apple_pkg_id]}
802
+ @removable = d3_pkg.removable
803
+ @manually_installed = (@admin != D3::AUTO_INSTALL_ADMIN)
804
+ @package_type = @jamf_rcpt_file.end_with?(".dmg") ? :dmg : :pkg
805
+ @expiration = d3_pkg.expiration
806
+ @expiration_path = d3_pkg.expiration_path
807
+
808
+ end # repair rcpt
809
+
810
+ ### Is this rcpt frozen?
811
+ ###
812
+ ### @return [Boolean]
813
+ ###
814
+ def frozen?
815
+ return true if @frozen
816
+ return false
817
+ end
818
+
819
+ ### Freeze this rcpt
820
+ ###
821
+ ### @return [void]
822
+ ###
823
+ def freeze
824
+ @frozen = true
825
+ end
826
+
827
+ ### Unfreeze this rcpt
828
+ ###
829
+ ### @return [void]
830
+ ###
831
+ def unfreeze
832
+ @frozen = false
833
+ end
834
+ alias thaw unfreeze
835
+
836
+ ### Set a new expiration period
837
+ ### WARNING: setting this to a lower value
838
+ ### might cause the rcpt to be uninstalled
839
+ ### at the next sync.
840
+ ###
841
+ ### @param new_val[Integer] The new expiration period in days
842
+ ###
843
+ ### @return [void]
844
+ ###
845
+ def expiration= (new_val)
846
+ raise JSS::InvalidDataError, "#{edition} is not removable, no expiration allowed." unless @removable or new_val.to_i == 0
847
+ @expiration = new_val.to_i
848
+ end
849
+
850
+ ### Set a new expiration path
851
+ ### WARNING: changing this to a new value
852
+ ### might cause the rcpt to be uninstalled
853
+ ### at the next sync.
854
+ ###
855
+ ### @param new_val[Pathname,String] The new expiration path
856
+ ###
857
+ ### @return [void]
858
+ ###
859
+ def expiration_path= (new_val)
860
+ @expiration_path = Pathname.new new_val
861
+ end
862
+
863
+ ### Set a new prohibiting process
864
+ def prohibiting_process=(new_val)
865
+ @prohibiting_process = new_val
866
+ end
867
+
868
+ ### Update the current receipt in the receipt store
869
+ def update
870
+ D3::Client::Receipt.add_receipt(self, :replace)
871
+ end
872
+
873
+ ### Delete this receipt from the local machine.
874
+ ### This removes both the JAMF receipt file, and
875
+ ### the D3::Client::Receipt from the datastore, and sets @deleted
876
+ ### to true.
877
+ ###
878
+ ### @return [void]
879
+ ###
880
+ def delete
881
+ @jamf_rcpt_file.delete if @jamf_rcpt_file.exist?
882
+ D3::Client::Receipt.remove_receipt @basename
883
+ D3.log "Deleted JAMF receipt file #{@jamf_rcpt_file.basename}", :debug
884
+ @deleted = true
885
+ end
886
+
887
+ ### @return [Boolean] has this rcpt been deleted?
888
+ ### See also {#delete}
889
+ ###
890
+ def deleted?
891
+ @deleted
892
+ end
893
+
894
+ ### @return [String] a human-readable string of details about this
895
+ ### installed pkg
896
+ ###
897
+ def formatted_details
898
+ deets = <<-END_DEETS
899
+ Edition: #{@edition}
900
+ Status: #{@status}
901
+ Frozen: #{frozen? ? "yes" : "no"}
902
+ Install date: #{@installed_at.strftime "%Y-%m-%d %H:%M:%S"}
903
+ Installed by: #{@admin}
904
+ Manually installed: #{manual?}
905
+ JAMF receipt file: #{@jamf_rcpt_file.basename}
906
+ Casper Pkg ID: #{@id}
907
+ Un-installable: #{removable? ? "yes" : "no"}
908
+ END_DEETS
909
+
910
+ if removable?
911
+ if JSS::API.connected?
912
+ pre_name = pre_remove_script_id ? JSS::Script.map_all_ids_to(:name)[pre_remove_script_id] : "none"
913
+ post_name = post_remove_script_id ? JSS::Script.map_all_ids_to(:name)[post_remove_script_id] : "none"
914
+ else # not connected
915
+ pre_name = pre_remove_script_id ? "yes" : "none"
916
+ post_name = post_remove_script_id ? "yes" : "none"
917
+ end
918
+ deets += <<-END_DEETS
919
+ Pre-remove script: #{pre_name}
920
+ Post-remove script: #{post_name}
921
+ END_DEETS
922
+ end # if removable?
923
+
924
+ if @package_type == :pkg and @apple_pkg_ids
925
+ deets += <<-END_DEETS
926
+ Apple.pkg ids: #{@apple_pkg_ids.join(', ')}
927
+ END_DEETS
928
+ end
929
+ if @expiration_path
930
+ if @expiration.to_i > 0
931
+ lu = last_usage
932
+ if lu.nil?
933
+ last_usage_display = "Unknonwn"
934
+ elsif lu == @installed_at
935
+ last_usage_display = "Not since installation (#{days_since_last_usage} days ago)"
936
+ else
937
+ last_usage_display = "#{lu.strftime '%Y-%m-%d %H:%M:%S'} (#{days_since_last_usage} days ago)"
938
+ end # if my_last_usage == @installed_at
939
+
940
+ deets += <<-END_DEETS
941
+ Expiration period: #{@expiration} days#{@custom_expiration ? ' (custom)' : ''}
942
+ Expiration path: #{@expiration_path}
943
+ Last brought to foreground: #{last_usage_display}
944
+ END_DEETS
945
+ end # if exp > 0
946
+ end # if exp path
947
+ return deets
948
+ end
949
+
950
+ ### If a currently installed pilot goes live, just change it's state and mark it so.
951
+ ###
952
+ def make_live
953
+ return true if live?
954
+ D3.log "Marking pilot receipt #{edition} live", :debug
955
+ @status = :live
956
+ update
957
+ end
958
+
959
+ ### Should this item be expired right now?
960
+ ###
961
+ ### @return [Boolean]
962
+ ###
963
+ def should_expire?
964
+
965
+ # gotta be expirable
966
+ return false if @expiration.nil? or @expiration == 0
967
+
968
+ # gotta have an expiration path
969
+ unless @expiration_path
970
+ D3.log "Not expiring #{edition} because: No Expiration Path for #{edition}", :debug
971
+ return false
972
+ end
973
+
974
+ # must have up-to-date last usage data
975
+ # this also checks for usage dir existence and plist age
976
+ my_last_usage = last_usage
977
+ unlaunched_days = days_since_last_usage
978
+
979
+ # gotta have expirations turned on system-wide
980
+ unless D3::CONFIG.client_expiration_allowed
981
+ D3.log "Not expiring #{edition} because: expirations not allowed on this client", :debug
982
+ return false
983
+ end
984
+
985
+ # gotta be removable
986
+ unless @removable
987
+ D3.log "Not expiring #{edition} because: not removable", :debug
988
+ return false
989
+ end
990
+
991
+ # gotta have an expiration set for this rcpt.
992
+ if (not @expiration.is_a? Fixnum) or @expiration <= 0
993
+ D3.log "Not expiring #{edition} because: expiration value is invalid", :debug
994
+ return false
995
+ end
996
+
997
+ # the app usage monitor must be running
998
+ all_procs = `/bin/ps -A -c -o user -o comm`.split("\n")
999
+ if all_procs.select{|p| p =~ /\s#{APP_USAGE_MONITOR_PROC}$/}.empty?
1000
+ D3.log "Not expiring #{edition} because: '#{APP_USAGE_MONITOR_PROC}' isn't running", :debug
1001
+ return false
1002
+ end
1003
+
1004
+ # did we get any usage dates above?
1005
+ unless my_last_usage and unlaunched_days
1006
+ D3.log "Not expiring #{edition} because: could not retrieve last usage data", :debug
1007
+ return false
1008
+ end
1009
+
1010
+ # must be unlaunched for at least the expiration period
1011
+ if unlaunched_days <= @expiration
1012
+ D3.log "Not expiring #{edition} because: path has launched within #{expiration} days", :debug
1013
+ return false
1014
+ end
1015
+
1016
+ # gotta be connected to d3
1017
+ unless D3.connected?
1018
+ D3.log "Not expiring #{edition} because: not connected to the servers", :debug
1019
+ return false
1020
+ end
1021
+
1022
+ # if we're here, expire this thing
1023
+ return true
1024
+ end # should expire
1025
+
1026
+ ### Expire this item - uninstall it if no foreground use in
1027
+ ### the expiration period
1028
+ ###
1029
+ ### @return [String, nil] the edition that was expired or nil if none
1030
+ ###
1031
+ def expire(verbose = false, force = D3.forced?)
1032
+ return nil unless should_expire?
1033
+ begin
1034
+ D3::Client.set_env :expiring, edition
1035
+ D3.log "Expiring #{edition} after #{expiration} days of no use.", :warn
1036
+ uninstall verbose, force
1037
+ D3.log "Done expiring #{edition}", :info
1038
+ rescue
1039
+ D3.log "There was an error expiring #{edition}:\n #{$!}", :error
1040
+ D3.log_backtrace
1041
+ ensure
1042
+ D3::Client.unset_env :expiring
1043
+ end
1044
+ return deleted? ? edition : nil
1045
+ end # expire
1046
+
1047
+ ### Return the number of days since the last usage for the @expiration_path
1048
+ ### for this receipt
1049
+ ###s
1050
+ ### Returns nil if last_usage is nil
1051
+ ###
1052
+ ### See also {#last_usage}
1053
+ ###
1054
+ ### @return [Integer, nil] days since last usage
1055
+ ###
1056
+ def days_since_last_usage
1057
+ lu = last_usage
1058
+ return nil unless lu
1059
+ ((Time.now - lu) / 60 / 60 / 24).to_i
1060
+ end
1061
+
1062
+ ### The last usage date for this receipt and the number of days ago that was
1063
+ ###
1064
+ ### If we have access to the usage plists maintained by d3RepoMan, then read
1065
+ ### them and find the last usage, store it in @last_usage , and return it
1066
+ ###
1067
+ ### If we don't have access, return @last_usage, which is updated during
1068
+ ### d3 sync.
1069
+ ### Its up to the caller to use @last_usage_as_of appropriately.
1070
+ ###
1071
+ ### If @last_usage has never been set, or there is no expiration path,
1072
+ ### returns nil.
1073
+ ###
1074
+ ### @return [Time,nil] The last usage date, or nil if no
1075
+ ### expiration path or the data wasn't retrievable.
1076
+ ###
1077
+ def last_usage
1078
+ return nil unless @expiration_path
1079
+
1080
+ now = Time.now
1081
+
1082
+ # if it's in the foreground right now, return [now, 0]
1083
+ fgnd_path = D3::Client.foreground_executable_path
1084
+ if fgnd_path
1085
+ now_in_forground = (fgnd_path.to_s == @expiration_path.to_s.chomp('/'))
1086
+ else
1087
+ now_in_forground = nil
1088
+ end
1089
+
1090
+ if now_in_forground
1091
+ @last_usage = now
1092
+ @last_usage_as_of = now
1093
+ return @last_usage
1094
+ end
1095
+
1096
+ # if we're root, read the usage plists
1097
+ if JSS.superuser?
1098
+
1099
+ # usage data dir must exist
1100
+ unless LAST_APP_USAGE_DIR.directory?
1101
+ D3.log "Last app usage dir '#{LAST_APP_USAGE_DIR}' doesn't exist or isn't a directory.", :debug
1102
+ return nil
1103
+ end
1104
+
1105
+ # all the plists in the usage dir
1106
+ plists = LAST_APP_USAGE_DIR.children.select{|c| c.extname == ".plist" }
1107
+
1108
+ # gotta have new-enough usage data
1109
+ newest_mtime = plists.map{|pl| pl.stat.mtime}.max
1110
+ app_usage_update_age = (now - newest_mtime).to_i
1111
+ if app_usage_update_age > MAX_APP_USAGE_UPDATE_AGE
1112
+ D3.log "Last app usage update more than #{MAX_APP_USAGE_UPDATE_AGE} seconds ago.", :debug
1113
+ return nil
1114
+ end
1115
+
1116
+ # loop through the plists, get the newest usage time for this
1117
+ # expiration path, and append it to all_usages
1118
+ all_usages = []
1119
+ plists.each do |plist|
1120
+ usage_times = D3.parse_plist plist
1121
+ my_usage_keys = usage_times.keys.select{|k| k.start_with? @expiration_path.to_s }
1122
+ all_usages << my_usage_keys.map{|k| usage_times[k].to_time }.max
1123
+ end # do plist
1124
+
1125
+ @last_usage = all_usages.compact.max
1126
+
1127
+ # if never been used, last usage is the install date
1128
+ @last_usage ||= @installed_at
1129
+
1130
+ # if the install time is newer than the last usage,
1131
+ # use the install time.
1132
+ # this basically "resets the timer" when
1133
+ # something is re-installed.
1134
+ @last_usage = @installed_at if @installed_at > @last_usage
1135
+
1136
+ @last_usage_as_of = now
1137
+
1138
+ update
1139
+
1140
+ end # if JSS.superuser?
1141
+ return @last_usage
1142
+
1143
+ end # last_usage
1144
+
1145
+ ### set the status - for rcpts, this can't be a private method
1146
+ ###
1147
+ ### @param new_status[Symnol] one of the valid STATUSES
1148
+ ###
1149
+ ### @return [Symbol] the new status
1150
+ ###
1151
+ def status= (new_status)
1152
+ raise JSS::InvalidDataError, "status must be one of :#{D3::Basename::STATUSES.join(', :')}" unless D3::Basename::STATUSES.include? new_status
1153
+ @status = new_status
1154
+ update
1155
+ end
1156
+
1157
+
1158
+ ################# Provate Instance Methods #################
1159
+
1160
+ private
1161
+
1162
+ ### Is there a process running that would prevent removal?
1163
+ ###
1164
+ ### @return [Boolean]
1165
+ ###
1166
+ def uninstall_prohibited_by_process?
1167
+ return false unless @prohibiting_process
1168
+ D3.prohibited_by_process_running? @prohibiting_process
1169
+ end #
1170
+
1171
+ end # class Receipt
1172
+ end # class Client
1173
+ end # module