Class: D3::Client::Receipt

Inherits:
Object
  • Object
show all
Includes:
Basename
Defined in:
lib/d3/client/receipt.rb

Overview

Receipt - a d3 package that is currently installed on this machine.

D3 receipts are stored as their native ruby objects in a YAML file located at D3::Client::Receipt::DATASTORE

When the module loads, the file is read if it exists and all receipts are available in

The datastore contains a Hash of D3::Client::Receipt objects, keyed by their basenames (only one installation of a basename can be on a machine at a time)

Constant Summary collapse

DATASTORE =

This YAML file stores all D3::Client::Receipts on this machine

D3::SUPPORT_DIR + "receipts.yaml"
DATASTORE_LOCKFILE =

This locks the loading of receipts when there’s a potential to write them back ou. See Receipt.load_receipts.

D3::SUPPORT_DIR + "receipts.lock"
DATASTORE_LOCK_TIMEOUT =

How many seconds by default to keep trying to get the datastore lockfile.

10
DATASTORE_STALE_LOCK_AGE =

If a lockfile is this many seconds old, warn that it might be stale and need manual cleanup. 600 secs = 10 min

600
LAST_APP_USAGE_DIR =

This dir contains a plist for each GUI user, containing the last time any app was brought to the foreground for that user It’s updated by the helper app d3RepoMan.app which should always be running while a GUI user is logged in if expiration is turned on.

D3::SUPPORT_DIR + "Usage"
APP_USAGE_MONITOR_PROC =

This is the process (as listed in the output of ‘/bin/ps -A -c -o comm’) that updates the LAST_APP_USAGE_FILE. If it isn’t running as root when expiration is attempted, then expiration won’t happen.

"d3RepoMan"
MAX_APP_USAGE_UPDATE_AGE =

The newest of the plists in the LAST_APP_USAGE_DIR must have been updated within the last X number of seconds, or else we assume either no one’s logged in for a while, or something’s wrong with the usage monitoring, since nothing new has come to the foreground in that long. If so, nothing will be expired. Default is 24 hours

60 * 60 * 24
REQUIRED_INIT_ARGS =

These args are required when creating a new D3::Client::Receipt

[
  :basename,
  :version,
  :revision,
  :admin,
  :id,
  :jamf_rcpt_file,
  :status
]
CHANGABLE_ATTRIBS =

Only these attributes can be changed after a receipt is created

[
  :status,
  :removable,
  :pre_remove_script_id,
  :post_remove_script_id,
  :expiration,
  :expiration_paths,
  :prohibiting_processes
]
@@installed_rcpts =

The current receipts. See D3::Client::Receipt.load_receipts and D3::Client::Receipt.all

nil
@@got_lock =

Do we currently have the rw lock?

nil

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args = {}) ⇒ Receipt

Args are:

- :basename, required
- :version, required
- :revision, required
- :admin, required
- :id, required
- :status, required, :pilot or :live (or rarely :deprecated)
- :jamf_rcpt_file, required

- :apple_pkg_ids, optional in general, required for .pkg installers
- :installed_at, optional, defaults to Time.now

- :removable, optional, defaults to false
- :frozen, optional, defaults to false
- :pre_remove_script_id, optional
- :post_remove_script_id, optional


550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
# File 'lib/d3/client/receipt.rb', line 550

def initialize(args = {})

  missing_args = REQUIRED_INIT_ARGS - args.keys
  unless missing_args.empty?
    raise JSS::MissingDataError, "D3::Client::Receipt initialization requires these arguments: :#{REQUIRED_INIT_ARGS.join(', :')}"
  end

  args[:installed_at] ||= Time.now

  @basename = args[:basename]
  @version = args[:version]
  @revision = args[:revision]
  @admin = args[:admin]
  @id  = args[:id]
  @status  = args[:status]


  # if we were given a string, convert to a Pathname
  # and if it was just a filename, add the Receipts Folder path
  @jamf_rcpt_file = Pathname.new args[:jamf_rcpt_file]
  if @jamf_rcpt_file.parent != JSS::Client::RECEIPTS_FOLDER
     @jamf_rcpt_file = JSS::Client::RECEIPTS_FOLDER + @jamf_rcpt_file
  end

  @apple_pkg_ids = args[:apple_pkg_ids]
  @installed_at = args[:installed_at] ? args[:installed_at].to_time : Time.now

  @removable = args[:removable]
  @prohibiting_processes = args[:prohibiting_processes]
  @prohibiting_processes ||= []

  @frozen = args[:frozen]
  @pre_remove_script_id = args[:pre_remove_script_id]
  @post_remove_script_id = args[:post_remove_script_id]

  @expiration = args[:expiration].to_i
  @expiration_paths = args[:expiration_paths]
  @expiration_paths ||= []
  @custom_expiration = args[:custom_expiration]

  @manually_installed = (@admin != D3::AUTO_INSTALL_ADMIN)
  @package_type = @jamf_rcpt_file.to_s.end_with?(".dmg") ? :dmg : :pkg

end

Instance Attribute Details

#adminString (readonly) Originally defined in module Basename

Returns who’s uploading, releasing, installing, or archiving this thing?.

Returns:

  • (String)

    who’s uploading, releasing, installing, or archiving this thing?

#apple_pkg_idsArray<String> (readonly)

Returns if its an apple pkg, what pkg_id’s does it install?.

Returns:

  • (Array<String>)

    if its an apple pkg, what pkg_id’s does it install?



496
497
498
# File 'lib/d3/client/receipt.rb', line 496

def apple_pkg_ids
  @apple_pkg_ids
end

#basenameString (readonly) Originally defined in module Basename

Returns the basname of the thing installed.

Returns:

  • (String)

    the basname of the thing installed

#custom_expirationBoolean

Returns is the expiration on this rcpt a custom one? If so, it’ll be carried forward when auto-updates occur.

Returns:

  • (Boolean)

    is the expiration on this rcpt a custom one? If so, it’ll be carried forward when auto-updates occur



516
517
518
# File 'lib/d3/client/receipt.rb', line 516

def custom_expiration
  @custom_expiration
end

#expirationInteger (readonly) Originally defined in module Basename

Returns the days of disuse before an expirable edition expires. 0=never.

Returns:

  • (Integer)

    the days of disuse before an expirable edition expires. 0=never

#expiration_pathsString (readonly) Originally defined in module Basename

Returns the path to the executable that needs come to the foreground to prevent expiration.

Returns:

  • (String)

    the path to the executable that needs come to the foreground to prevent expiration

#frozenBoolean

Returns is this rcpt exempt from auto-updates to its basename? If so, d3 sync will not update it, but a manual d3 install still can, and will re-enable syncs.

Returns:

  • (Boolean)

    is this rcpt exempt from auto-updates to its basename? If so, d3 sync will not update it, but a manual d3 install still can, and will re-enable syncs



521
522
523
# File 'lib/d3/client/receipt.rb', line 521

def frozen
  @frozen
end

#idInteger (readonly) Originally defined in module Basename

Returns the JSS id of this package.

Returns:

  • (Integer)

    the JSS id of this package

#installed_atTime (readonly)

Returns when was it installed?.

Returns:

  • (Time)

    when was it installed?



493
494
495
# File 'lib/d3/client/receipt.rb', line 493

def installed_at
  @installed_at
end

#jamf_rcpt_filePathnamee (readonly)

Returns the JAMF rcpt file for this installation.

Returns:

  • (Pathnamee)

    the JAMF rcpt file for this installation



490
491
492
# File 'lib/d3/client/receipt.rb', line 490

def jamf_rcpt_file
  @jamf_rcpt_file
end

#last_usageTime? (readonly)

The last usage date for this receipt and the number of days ago that was

If we have access to the usage plists maintained by d3RepoMan, then read them and find the last usage, store it in @last_usage , and return it

If we don’t have access, return @last_usage, which is updated during d3 sync. Its up to the caller to use @last_usage_as_of appropriately.

If @last_usage has never been set, or there is no expiration path, returns nil.

Returns:

  • (Time, nil)

    The last usage date, or nil if no expiration path or the data wasn’t retrievable.



525
526
527
# File 'lib/d3/client/receipt.rb', line 525

def last_usage
  @last_usage
end

#last_usage_as_ofTime? (readonly)

Returns When was @last_usage updated? nil if never checked, or no @expiration_paths.

Returns:

  • (Time, nil)

    When was @last_usage updated? nil if never checked, or no @expiration_paths



529
530
531
# File 'lib/d3/client/receipt.rb', line 529

def last_usage_as_of
  @last_usage_as_of
end

#manually_installedBoolean (readonly) Also known as: manual?

Returns was this pkg manually installed?.

Returns:

  • (Boolean)

    was this pkg manually installed?



499
500
501
# File 'lib/d3/client/receipt.rb', line 499

def manually_installed
  @manually_installed
end

#package_typeSymbol (readonly) Originally defined in module Basename

Returns Is this package a .dmg or .pkg?.

Returns:

  • (Symbol)

    Is this package a .dmg or .pkg?

#post_remove_script_idInteger? Also known as: post_remove_script?

Returns the jss id of the post-remove-script.

Returns:

  • (Integer, nil)

    the jss id of the post-remove-script



511
512
513
# File 'lib/d3/client/receipt.rb', line 511

def post_remove_script_id
  @post_remove_script_id
end

#pre_remove_script_idInteger? Also known as: pre_remove_script?

Returns the jss id of the pre-remove-script.

Returns:

  • (Integer, nil)

    the jss id of the pre-remove-script



507
508
509
# File 'lib/d3/client/receipt.rb', line 507

def pre_remove_script_id
  @pre_remove_script_id
end

#prohibiting_processesArray<String> (readonly) Originally defined in module Basename

Returns an array of Strings for matching to the output lines of ‘/bin/ps -A -c -o comm’. If there’s a match, this pkg won’t be installed or uninstalled without a graceful quit.

Returns:

  • (Array<String>)

    an array of Strings for matching to the output lines of ‘/bin/ps -A -c -o comm’. If there’s a match, this pkg won’t be installed or uninstalled without a graceful quit

#removableBoolean Also known as: removable?

Returns can it be uninstalled?.

Returns:

  • (Boolean)

    can it be uninstalled?



503
504
505
# File 'lib/d3/client/receipt.rb', line 503

def removable
  @removable
end

#revisionInteger (readonly) Originally defined in module Basename

Returns the d3 release number of the thing installed.

Returns:

  • (Integer)

    the d3 release number of the thing installed

#statusSymbol Originally defined in module Basename

Returns whats the d3 status of this package? One of the values of D3::Basename::STATUSES.

Returns:

  • (Symbol)

    whats the d3 status of this package? One of the values of D3::Basename::STATUSES

#versionString (readonly) Originally defined in module Basename

Returns the version of the thing installed.

Returns:

  • (String)

    the version of the thing installed

Class Method Details

.add_receipt(receipt, replace = false) ⇒ void

This method returns an undefined value.

Add a D3::Client::Receipt to the local rcpt database

Parameters:

Raises:

  • (JSS::InvalidDataError)


285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/d3/client/receipt.rb', line 285

def self.add_receipt(receipt, replace = false)
  raise JSS::InvalidDataError, "Argument must be a D3::Client::Receipt" unless receipt.is_a? D3::Client::Receipt
  D3.log "Attempting to #{replace ? "replace" : "add"} receipt for #{receipt.edition}.", :debug
  self.reload_receipts :rw
  begin
    unless replace
      if @@installed_rcpts.member? receipt.basename
        raise JSS::AlreadyExistsError, "There's already a receipt on this machine for basemame '#{receipt.basename}'"
      end # if
    end # unless replace

    replacing = @@installed_rcpts[receipt.basename] ? true : false
    @@installed_rcpts[receipt.basename] = receipt
    self.save_receipts
    D3.log "#{replacing ? "Replaced" : "Added"} receipt for #{receipt.edition}", :info

  ensure
    # always release the rw lock even after an error
    self.release_datastore_lock
  end # begin
end

.all(refresh = false) ⇒ Hash{String => D3::Client::Receipt}

A hash of all d3 receipts currently installed on this machine. keyed by their basenames. (Only one edition of a basename can be installed at a time)

Parameters:

  • refresh (Boolean) (defaults to: false)

    Should the data be re-read from disk?

Returns:



365
366
367
368
369
# File 'lib/d3/client/receipt.rb', line 365

def self.all (refresh = false)
  refresh = true if @@installed_rcpts.nil?
  self.reload_receipts if refresh
  @@installed_rcpts
end

.basenames(refresh = false) ⇒ Object

Return an array of the basenames of all installed d3 pkgs. This doesn’t include those items installed by other jamf methods



375
376
377
# File 'lib/d3/client/receipt.rb', line 375

def self.basenames(refresh = false)
  self.all(refresh).keys
end

.delete_receipt(basename) ⇒ Object

An alias of selfself.remove_receipt



333
# File 'lib/d3/client/receipt.rb', line 333

def self.delete_receipt(basename) ; self.remove_receipt(basename) ; end

.deprecated(refresh = false) ⇒ Hash

Return a hash of D3::Client::Receipt objects for all installed deprecated d3 pkgs, keyed by their basenames

Returns:

  • (Hash)

    all deprecated receipts



402
403
404
# File 'lib/d3/client/receipt.rb', line 402

def self.deprecated(refresh = false)
  self.all(refresh).select {|b,r| r.deprecated? }
end

.find_receipt(rcpt_to_find) ⇒ D3::Client::Receipt?

Given a basename, edition, or id return the matching D3::Receipt or nil if no match. If a basename is used, any edition installed will be returned if there is one.

If an edition or id is used, nil will be returned unless that exact pkg is installed.

Parameters:

  • rcpt_to_find (String)

    basename or edition for which to return the receipt

Returns:



348
349
350
351
352
353
354
355
356
# File 'lib/d3/client/receipt.rb', line 348

def self.find_receipt (rcpt_to_find)
  if self.all.keys.include? rcpt_to_find
    return self.all[rcpt_to_find]
  end
  self.all.values.each do |rcpt|
    return rcpt if rcpt.edition == rcpt_to_find or rcpt.id == rcpt_to_find.to_i
  end
  return nil
end

.force_clear_datastore_lockObject

Force the release of the lock, regardless of who has it Useful for testing, but very dangerous - could cause data loss.



263
264
265
266
267
# File 'lib/d3/client/receipt.rb', line 263

def self.force_clear_datastore_lock
  D3.log "Force-clearing the receipt write lock", :debug
  DATASTORE_LOCKFILE.delete if DATASTORE_LOCKFILE.exist?
  @@got_lock = false
end

.frozen(refresh = false) ⇒ Hash

Return a hash of D3::Client::Receipt objects for all installed frozen d3 receipts, keyed by their basenames

Returns:

  • (Hash)

    all frozen receipts



411
412
413
# File 'lib/d3/client/receipt.rb', line 411

def self.frozen(refresh = false)
  self.all(refresh).select {|b,r| r.frozen? }
end

.get_datastore_lock(lock_timeout = DATASTORE_LOCK_TIMEOUT) ⇒ void

This method returns an undefined value.

Try to get the lock for read-write access to the datastore. Raise an exception if we fail after the timeout

Parameters:

  • lock_timeout (Integer) (defaults to: DATASTORE_LOCK_TIMEOUT)

    How many seconds to keep trying to get the lock?



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/d3/client/receipt.rb', line 222

def self.get_datastore_lock (lock_timeout = DATASTORE_LOCK_TIMEOUT)
  D3.log "Attempting to get receipt datastore write lock.", :debug
  # try to get it 10x per second...
  if DATASTORE_LOCKFILE.exist?
    D3.log "Lock in use, retrying for #{lock_timeout} secs", :debug
    max_tries = lock_timeout * 10
    tries = 0
    while tries < max_tries do
      sleep 0.1
      tries += 1 if DATASTORE_LOCKFILE.exist?
    end # while
  end # if DATASTORE_LOCKFILE.exist?

  if DATASTORE_LOCKFILE.exist?
    errmsg = "Couldn't get receipt write lock after #{lock_timeout} seconds."
    lockfile_age =  (Time.now - DATASTORE_LOCKFILE.ctime).to_i

    # if its stale, warn that it might need manual fixing
    errmsg += " Potentially stale. Please investigate manually." if lockfile_age > DATASTORE_STALE_LOCK_AGE
    D3.log errmsg, :error
    raise JSS::TimeoutError, errmsg
  else
    DATASTORE_LOCKFILE.parent.mkpath
    DATASTORE_LOCKFILE.jss_save $$.to_s
    D3.log "Acquired write lock on receipt datastore.", :debug
    @@got_lock = true
  end
end

.got_lock?boolean

Do we currently have the rw lock on the rcpt file?

Returns:

  • (boolean)


273
274
275
# File 'lib/d3/client/receipt.rb', line 273

def self.got_lock?
  @@got_lock
end

.live(refresh = false) ⇒ Hash

Return a hash of D3::Client::Receipt objects for all installed live d3 pkgs, keyed by their basenames

Returns:

  • (Hash)

    All live receipts



393
394
395
# File 'lib/d3/client/receipt.rb', line 393

def self.live(refresh = false)
  self.all(refresh).select {|b,r| r.live? }
end

.load_receipts(rw = false, lock_timeout = DATASTORE_LOCK_TIMEOUT) ⇒ void

This method returns an undefined value.

Load in the existing rcpt database if it exists. This makes them available in @@installed_rcpts and from D3::Client::Receipt.all

When loading read-write, if another process has loaded them read-write, and hasn’t saved them yet, a lock file will be present and this load will retry for lock_timeout seconds before raising an exception

Parameters:

  • rw (Boolean) (defaults to: false)

    Load the receipts read-write, meaning that a lock file is created and changes can be saved. Defaults to false.

  • lock_timeout (Integer) (defaults to: DATASTORE_LOCK_TIMEOUT)

    How many seconds to keep trying to get the read-write lock, when loading read-write.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/d3/client/receipt.rb', line 139

def self.load_receipts(rw = false, lock_timeout = DATASTORE_LOCK_TIMEOUT)

  # have we already loaded them?
  # (use self.reload if needed)
  return if @@installed_rcpts

  D3.log "Loading receipts, #{rw ? 'read-write' : 'read-only'}", :debug

  # get the lock if needed
  self.get_datastore_lock(lock_timeout) if rw

  @@installed_rcpts = DATASTORE.file? ? YAML.load(DATASTORE.read) : {}
  @@installed_rcpts ||= {}

  D3.log "Receipts loaded", :debug
end

.manual(refresh = false) ⇒ Hash

Return a hash of D3::Client::Receipt objects for all manually installed pkgs (live or pilot) keyed by their basenames

Returns:

  • (Hash)

    all manually-installed receipts



420
421
422
# File 'lib/d3/client/receipt.rb', line 420

def self.manual(refresh = false)
  self.all(refresh).select {|b,r| r.manual? }
end

.os_pkg_rcpts(refresh = false) ⇒ Object

An array of apple bundle id’s for all .[m]pkgs currently known to the OS’s receipt db



427
428
429
430
431
# File 'lib/d3/client/receipt.rb', line 427

def self.os_pkg_rcpts(refresh = false)
  @@os_pkg_rcpts = nil if refresh
  return @@os_pkg_rcpts if @@os_pkg_rcpts
  @@os_pkg_rcpts = `#{JSS::Composer::PKG_UTIL} --pkgs`.split("\n")
end

.pilots(refresh = false) ⇒ Hash

Return a hash of D3::Client::Receipt objects for all installed pilot d3 pkgs, keyed by their basenames

Returns:

  • (Hash)

    All pilot receipts



384
385
386
# File 'lib/d3/client/receipt.rb', line 384

def self.pilots(refresh = false)
  self.all(refresh).select{|b,r| r.pilot? }
end

.rebuild_databasevoid

This method returns an undefined value.

Rebuild the receipt database by reading the jamf receipts and using server data.



438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/d3/client/receipt.rb', line 438

def self.rebuild_database
  orig_rcpts = self.all :refresh
  new_rcpts = {}

  jamf_rcpts = JSS::Client::RECEIPTS_FOLDER.children

  D3::Package.all.values.each do |d3_pkg|

    next unless jamf_rcpts.include? d3_pkg.receipt

    # do we already have a rcpt for this edition?
    if orig_rcpts[d3_pkg.basename] and (orig_rcpts[d3_pkg.basename].edition == d3_pkg.edition)
      orig_rcpt = orig_rcpts[d3_pkg.basename]
    else
      orig_rcpt = nil
    end

    # if there's more than one of the same basename (which means
    # someone installed a d3 pkg via non-d3 means) then
    # which one wins? I say the last one, but log it.
    if new_rcpts.keys.include? d3_pkg.basename
      D3.log "Rebuilding local receipt database: multiple Jamf Pro installs of basename '#{d3_pkg.basename}'", :warn
      new_rcpts.delete d3_pkg.basename
    end # new_rcpts.keys.include? d3_pkg.basename

    new_rcpts[d3_pkg.basename] = D3::Client::Receipt.new(:basename => d3_pkg.basename,
      :version => d3_pkg.version,
      :revision => d3_pkg.revision,
      :admin => (orig_rcpt ? orig_rcpt.admin : "unknown"),
      :installed_at => (orig_rcpt ? orig_rcpt.installed_at : Time.now),
      :id => d3_pkg.id,
      :status => d3_pkg.status,
      :jamf_rcpt_file => d3_pkg.receipt,
      :apple_pkg_ids => d3_pkg.apple_receipt_data.map{|r| r[:apple_pkg_id]},
      :removable => d3_pkg.removable,
      :pre_remove_script_id => d3_pkg.pre_remove_script_id,
      :post_remove_script_id => d3_pkg.post_remove_script_id,
      :expiration => d3_pkg.expiration,
      :expiration_paths => d3_pkg.expiration_paths
    )

  end # .each do |d3_pkg|

  @@installed_rcpts = new_rcpts
  self.save_receipts

end

.release_datastore_lockObject

Release the rw lock on the datastore, if we have it.



253
254
255
256
257
258
# File 'lib/d3/client/receipt.rb', line 253

def self.release_datastore_lock
  return nil unless @@got_lock
  DATASTORE_LOCKFILE.delete if DATASTORE_LOCKFILE.exist?
  D3.log "Receipt datastore write lock released", :debug
  @@got_lock = false
end

.reload_receipts(rw = false, lock_timeout = DATASTORE_LOCK_TIMEOUT) ⇒ void

This method returns an undefined value.

Reload the existing rcpt database

Parameters:

  • rw (Boolean) (defaults to: false)

    Load the receipts read-write, meaning that a lock file is created and changes can be saved. Defaults to false.

  • lock_timeout (Integer) (defaults to: DATASTORE_LOCK_TIMEOUT)

    How many seconds to keep trying to get the read-write lock, when loading read-write.



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/d3/client/receipt.rb', line 166

def self.reload_receipts (rw = false, lock_timeout = DATASTORE_LOCK_TIMEOUT)

  # if we  haven't loaded them at all yet, just do that.
  unless @@installed_rcpts
    self.load_receipts rw, lock_timeout
    return
  end # unless @@installed_rcpts

  D3.log "Reloading receipts, #{rw ? 'read-write' : 'read-only'}", :debug

  # Are we trying to re-load with rw?
  if rw
    # if we already have the lock, then we don't need to get it again
    self.get_datastore_lock(lock_timeout) unless @@got_lock
  else
    # not reloading rw, so release the lock if we have it
    self.release_datastore_lock if @@got_lock
  end

  # reload it
  @@installed_rcpts = DATASTORE.file? ? YAML.load(DATASTORE.read) : {}
  @@installed_rcpts ||= {}
  D3.log "Receipts reloaded", :debug
end

.remove_receipt(basename) ⇒ void

This method returns an undefined value.

Delete a D3::Client::Receipt from the local databse



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/d3/client/receipt.rb', line 311

def self.remove_receipt(basename)

  D3.log "Attempting to remove receipt for basename #{basename}", :info

  self.reload_receipts :rw
  begin
    old_rcpt = self.all[basename]
    if old_rcpt
      @@installed_rcpts.delete basename
      D3.log "Removed receipt for #{old_rcpt.edition}", :debug

      self.save_receipts
    else
      D3.log "No receipt for basename #{basename}", :debug
    end # if old_rcpt
  ensure
    self.release_datastore_lock
  end # begin

end

.save_receipts(release_lock = true) ⇒ void

This method returns an undefined value.

Write existing rcpt database to disk

Raises:

  • (JSS::MissingDataError)


195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/d3/client/receipt.rb', line 195

def self.save_receipts(release_lock = true)
  raise JSS::MissingDataError, "Receipts not loaded, can't save." unless @@installed_rcpts
  D3.log "Saving receipts", :debug

  unless @@got_lock
    D3.log "Receipts were loaded read-only, can't save", :error
    raise JSS::UnsupportedError,"Receipts were loaded read-only, can't save"
  end

  # ensure any deleted rcpts are gone
  @@installed_rcpts.delete_if{|basename, rcpt| rcpt.deleted? }

  DATASTORE.parent.mktree unless DATASTORE.parent.directory?
  DATASTORE.jss_save YAML.dump(@@installed_rcpts)
  D3.log "Receipts saved", :debug
  if release_lock
    self.release_datastore_lock
  end
end

Instance Method Details

#<=>(other) ⇒ Object Originally defined in module Basename

Use comparable to give sortability and equality.

#days_since_last_usageInteger?

Return the number of days since the last usage for the @expiration_paths for this receipt s Returns nil if last_usage is nil

See also #last_usage

Returns:

  • (Integer, nil)

    days since last usage



1066
1067
1068
1069
1070
# File 'lib/d3/client/receipt.rb', line 1066

def days_since_last_usage
  lu = last_usage
  return nil unless lu
  ((Time.now - lu) / 60 / 60 / 24).to_i
end

#deletevoid

This method returns an undefined value.

Delete this receipt from the local machine. This removes both the JAMF receipt file, and the D3::Client::Receipt from the datastore, and sets @deleted to true.



890
891
892
893
894
895
# File 'lib/d3/client/receipt.rb', line 890

def delete
  @jamf_rcpt_file.delete if @jamf_rcpt_file.exist?
  D3::Client::Receipt.remove_receipt @basename
  D3.log "Deleted JAMF receipt file #{@jamf_rcpt_file.basename}", :debug
  @deleted = true
end

#deleted?Boolean

Returns has this rcpt been deleted? See also #delete.

Returns:

  • (Boolean)

    has this rcpt been deleted? See also #delete



900
901
902
# File 'lib/d3/client/receipt.rb', line 900

def deleted?
  @deleted
end

#deprecated?Boolean Originally defined in module Basename

Is the status :deprecated?

Returns:

  • (Boolean)

#editionString Originally defined in module Basename

While several packages can have the same basename, the combination of basename, version, and revision (called the ‘edition’) must be unique among the d3 packages.

Returns:

  • (String)

    the basename, version ,and revision of this package, joined with hyphens

#expiration=(new_val) ⇒ void

This method returns an undefined value.

Set a new expiration period WARNING: setting this to a lower value might cause the rcpt to be uninstalled at the next sync.

Parameters:

  • new_val (Integer)

    The new expiration period in days

Raises:

  • (JSS::InvalidDataError)


855
856
857
858
# File 'lib/d3/client/receipt.rb', line 855

def expiration= (new_val)
  raise JSS::InvalidDataError, "#{edition} is not removable, no expiration allowed." unless @removable or new_val.to_i == 0
  @expiration = new_val.to_i
end

#expiration_paths=(new_val) ⇒ void

This method returns an undefined value.

Set a new expiration path WARNING: changing this to a new value might cause the rcpt to be uninstalled at the next sync.

Parameters:

  • new_val (Pathname, String)

    The new expiration path



869
870
871
# File 'lib/d3/client/receipt.rb', line 869

def expiration_paths= (new_val)
  @expiration_paths = new_val
end

#expiration_paths_match?(other_exp_paths) ⇒ Boolean Originally defined in module Basename

Does a given array of pathnames have the same elements as This is generally used to compare two @expiration_paths arrays for “equality”

Parameters:

  • other_exp_paths (Array)

    An array if Pathnames to compare to @expiration_paths

Returns:

  • (Boolean)

    Are they the same aside from order?

#expire(verbose = false, force = D3.forced?) ⇒ String?

Expire this item - uninstall it if no foreground use in the expiration period

Returns:

  • (String, nil)

    the edition that was expired or nil if none



1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
# File 'lib/d3/client/receipt.rb', line 1041

def expire(verbose = false, force = D3.forced?)
  return nil unless should_expire?
  begin
    D3::Client.set_env :expiring, edition
    D3.log "Expiring #{edition} after #{expiration} days of no use.", :warn
    uninstall verbose, force
    D3.log "Done expiring #{edition}", :info
  rescue
    D3.log "There was an error expiring #{edition}:\n   #{$!}", :error
    D3.log_backtrace
  ensure
    D3::Client.unset_env :expiring
  end
  return deleted? ? edition : nil
end

#formatted_detailsString

installed pkg

Returns:

  • (String)

    a human-readable string of details about this



907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
# File 'lib/d3/client/receipt.rb', line 907

def formatted_details
  deets = <<-END_DEETS
Edition: #{@edition}
Status: #{@status}
Frozen: #{frozen? ? "yes" : "no"}
Install date: #{@installed_at.strftime "%Y-%m-%d %H:%M:%S"}
Installed by: #{@admin}
Manually installed: #{manual?}
JAMF receipt file: #{@jamf_rcpt_file.basename}
Jamf Pro Pkg ID: #{@id}
Un-installable: #{removable? ? "yes" : "no"}
  END_DEETS

  if removable?
    if JSS::API.connected?
      pre_name = pre_remove_script_id ? JSS::Script.map_all_ids_to(:name)[pre_remove_script_id] : "none"
      post_name = post_remove_script_id ? JSS::Script.map_all_ids_to(:name)[post_remove_script_id] : "none"
    else # not connected
      pre_name = pre_remove_script_id ? "yes" : "none"
      post_name = post_remove_script_id ? "yes" : "none"
    end
    deets += <<-END_DEETS
Pre-remove script: #{pre_name}
Post-remove script: #{post_name}
    END_DEETS
  end # if removable?

  if @package_type == :pkg and @apple_pkg_ids
    deets += <<-END_DEETS
Apple.pkg ids: #{@apple_pkg_ids.join(', ')}
    END_DEETS
  end
  if @expiration_paths
    if @expiration.to_i > 0
      lu = last_usage
      if lu.nil?
        last_usage_display = "Unknonwn"
      elsif lu == @installed_at
        last_usage_display = "Not since installation (#{days_since_last_usage} days ago)"
      else
        last_usage_display = "#{lu.strftime '%Y-%m-%d %H:%M:%S'} (#{days_since_last_usage} days ago)"
      end #  if my_last_usage == @installed_at

      deets += <<-END_DEETS
Expiration period: #{@expiration} days#{@custom_expiration ? ' (custom)' : ''}
Expiration path(s): #{D3::Database::ARRAY_OF_PATHNAMES_TO_COMMA_STRING.call @expiration_paths}
Last brought to foreground: #{last_usage_display}
      END_DEETS
    end # if exp > 0
  end # if exp path
  return deets
end

#freezevoid

This method returns an undefined value.

Freeze this rcpt



833
834
835
# File 'lib/d3/client/receipt.rb', line 833

def freeze
  @frozen = true
end

#frozen?Boolean

Is this rcpt frozen?

Returns:

  • (Boolean)


824
825
826
827
# File 'lib/d3/client/receipt.rb', line 824

def frozen?
  return true if @frozen
  return false
end

#live?Boolean Originally defined in module Basename

Is the status :live?

Returns:

  • (Boolean)

#make_liveObject

If a currently installed pilot goes live, just change it’s state and mark it so.



962
963
964
965
966
967
# File 'lib/d3/client/receipt.rb', line 962

def make_live
  return true if live?
  D3.log "Marking pilot receipt #{edition} live", :debug
  @status = :live
  update
end

#missing?Boolean Originally defined in module Basename

Is the status :missing?

Returns:

  • (Boolean)

#pilot?Boolean Originally defined in module Basename

Is the status :pilot?

Returns:

  • (Boolean)

#prohibiting_processes=(new_val) ⇒ Object

Set new prohibiting process(es)



874
875
876
# File 'lib/d3/client/receipt.rb', line 874

def prohibiting_processes=(new_val)
  @prohibiting_processes = new_val
end

#repairvoid

This method returns an undefined value.

Repair any missing or invalid data in the receipt based on the matching D3::Package data

Raises:

  • (JSS::UnsupportedError)


800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
# File 'lib/d3/client/receipt.rb', line 800

def repair
  raise JSS::UnsupportedError, "This receipt has been deleted" if @deleted

  d3_pkg = D3::Package.fetch :id => @id

  @basename = d3_pkg.basename
  @version = d3_pkg.version
  @revision = d3_pkg.revision
  @admin ||= "Repaired"
  @status  = d3_pkg.status
  @jamf_rcpt_file = d3_pkg.receipt
  @apple_pkg_ids = d3_pkg.apple_receipt_data.map{|r| r[:apple_pkg_id]}
  @removable = d3_pkg.removable
  @manually_installed = (@admin != D3::AUTO_INSTALL_ADMIN)
  @package_type = @jamf_rcpt_file.end_with?(".dmg") ? :dmg : :pkg
  @expiration = d3_pkg.expiration
  @expiration_paths = d3_pkg.expiration_paths

end

#run_post_remove(verbose = false) ⇒ Array<Integer, String>

Run the post-remove script, return the exit status and output

Parameters:

  • verbose (Boolean) (defaults to: false)

    run verbosely?

Returns:

  • (Array<Integer, String>)

    the exit status and output of the script



734
735
736
737
738
739
740
741
742
743
744
745
746
# File 'lib/d3/client/receipt.rb', line 734

def run_post_remove (verbose = false)
  D3::Client.set_env :post_remove, edition
  D3.log "Running post_remove script", :debug
  begin
    result = JSS::Script.fetch(:id => @post_remove_script_id).run :verbose => verbose, :show_output => verbose
  rescue D3::ScriptError
    raise PostRemoveError, $!
  ensure
    D3::Client.unset_env :post_remove
  end
  D3.log "Finished post_remove script", :debug
  return result
end

#run_pre_remove(verbose = false) ⇒ Array<Integer, String>

Run the pre-remove script, return the exit status and output

Parameters:

  • verbose (Boolean) (defaults to: false)

    run verbosely?

Returns:

  • (Array<Integer, String>)

    the exit status and output of the script



714
715
716
717
718
719
720
721
722
723
724
725
726
# File 'lib/d3/client/receipt.rb', line 714

def run_pre_remove (verbose = false)
  D3::Client.set_env :pre_remove, edition
  D3.log "Running pre_remove script", :debug
  begin
    result = JSS::Script.fetch(:id => @pre_remove_script_id).run :verbose => verbose, :show_output => verbose
  rescue D3::ScriptError
    raise PreRemoveError, $!
  ensure
    D3::Client.unset_env :pre_remove
  end
  D3.log "Finished pre_remove script", :debug
  return result
end

#saved?Boolean Originally defined in module Basename

Is the status :saved?

Returns:

  • (Boolean)

#should_expire?Boolean

Should this item be expired right now?

Returns:

  • (Boolean)


973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
# File 'lib/d3/client/receipt.rb', line 973

def should_expire?

  # gotta be expirable
  return false if @expiration.nil? or @expiration == 0

  # gotta have an expiration path
  if @expiration_paths.empty?
    D3.log "Not expiring #{edition} because: No Expiration Path(s) for #{edition}", :debug
    return false
  end

  # must have up-to-date last usage data
  # this also checks for usage dir existence and plist age
  my_last_usage = last_usage
  unlaunched_days = days_since_last_usage

  # gotta have expirations turned on system-wide
  unless D3::CONFIG.client_expiration_allowed
    D3.log "Not expiring #{edition} because: expirations not allowed on this client", :debug
    return false
  end

  # gotta be removable
  unless @removable
    D3.log "Not expiring #{edition} because: not removable", :debug
    return false
  end

  # gotta have an expiration set for this rcpt.
  if (not @expiration.is_a? Fixnum) or @expiration <= 0
    D3.log "Not expiring #{edition} because: expiration value is invalid", :debug
    return false
  end

  # the app usage monitor must be running
  all_procs = `/bin/ps -A -c -o user -o comm`.split("\n")
  if all_procs.select{|p| p =~ /\s#{APP_USAGE_MONITOR_PROC}$/}.empty?
    D3.log "Not expiring #{edition} because: '#{APP_USAGE_MONITOR_PROC}' isn't running", :debug
    return false
  end

  # did we get any usage dates above?
  unless my_last_usage and unlaunched_days
    D3.log "Not expiring #{edition} because: could not retrieve last usage data", :debug
    return false
  end

  # must be unlaunched for at least the expiration period
  if unlaunched_days <= @expiration
    D3.log "Not expiring #{edition} because: path has launched within #{expiration} days", :debug
    return false
  end

  # gotta be connected to d3
  unless D3.connected?
    D3.log "Not expiring #{edition} because: not connected to the servers", :debug
    return false
  end

  # if we're here, expire this thing
  return true
end

#skipped?Boolean Originally defined in module Basename

Returns Is this pkg skipped? See Database::PACKAGE_STATUSES for details.

Returns:

  • (Boolean)

    Is this pkg skipped? See Database::PACKAGE_STATUSES for details

#status=(new_status) ⇒ Symbol

set the status - for rcpts, this can’t be a private method

Parameters:

  • new_status (Symbol)

    one of the valid STATUSES

Returns:

  • (Symbol)

    the new status

Raises:

  • (JSS::InvalidDataError)


1165
1166
1167
1168
1169
# File 'lib/d3/client/receipt.rb', line 1165

def status= (new_status)
  raise JSS::InvalidDataError, "status must be one of :#{D3::Basename::STATUSES.join(', :')}" unless D3::Basename::STATUSES.include? new_status
  @status = new_status
  update
end

#unfreezevoid Also known as: thaw

This method returns an undefined value.

Unfreeze this rcpt



841
842
843
# File 'lib/d3/client/receipt.rb', line 841

def unfreeze
  @frozen = false
end

#uninstall(verbose = false, force = D3::forced?) ⇒ void

This method returns an undefined value.

UnInstall this pkg, and return the output of ‘jamf uninstall’ or “receipts removed”

If there’s a pre-remove script, and it exits with a status of 111, the d3 & jamf receipts are removed, but the actual uninstall doesn’t happen. This would be usefull if the uninstall process is too complex for ‘jamf uninstall’ and is totally performed by the script.

For receipts from .pkg installers, the force option will force deletion even if the JSS isn’t available. It does this by using the No pre- or post- remove scripts will be run. Use with caution.

Parameters:

  • verbose (Boolean) (defaults to: false)

    be verbose to stdout

  • force (Boolean) (defaults to: D3::forced?)

    .(m)pkg receipts only! Should the uninstall happen even if the JSS isn’t available? No pre- or post- scripts will be run.

Raises:



620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
# File 'lib/d3/client/receipt.rb', line 620

def uninstall (verbose = false, force = D3::forced?)

  raise D3::UninstallError,  "#{edition} is not uninstallable" unless self.removable?

  depiloting = pilot? && skipped?

  begin # ...ensure...
    if uninstall_prohibited_by_process? and (not force)
      raise D3::InstallError, "#{edition} cannot be uninstalled now because one or more of the following processes is running: #{D3::Admin::OPTIONS[:prohibiting_processes][:display_conversion].call @prohibiting_processes}"
    end
    D3::Client.set_env :removing, edition
    D3.log "Uninstalling #{edition}", :warn

    # run a preflight if needed.
    if pre_remove_script?
      (exit_status, output) = run_pre_remove verbose
      if exit_status == 111
        delete
        D3.log "pre_remove script exited 111, deleted receipt for #{edition} but not doing any more.", :info
        return true
      elsif exit_status != 0
        raise D3::UninstallError, "Error running pre_remove script (exited #{exit_status}), not uninstalling #{edition}"
      end # flight_status[0] == 111
    end # if preflight?

    # if it is still on the server...
    if JSS::Package.all_ids.include? @id
      # uninstall the pkg
      D3.log "Running 'jamf uninstall' of #{edition}", :debug
      uninstall_worked = JSS::Package.fetch(:id => @id).uninstall(:verbose => verbose).exitstatus == 0

    # if it isn't on the server any more....
    else
      D3.log "Package is gone from server, no index available", :info

      # if forced, deleting the rcpt is 'uninstalling'
      if force
        D3.log "Force-deleting receipt for #{edition}.", :info
        uninstall_worked = true

      # no force
      else
        # we can't do anything with dmgs
        if @package_type == :dmg
          D3.log "Package was a .dmg, can't uninstall.\n   Use --force to remove the receipt", :error
          uninstall_worked = false
        else
          uninstall_worked = uninstall_via_apple_rcpt
        end # if @package_type == :dmg
      end # if force

    end # JSS::Package.all_ids.include? @id

    ## Uninstall worked, so do more things and stuffs
    if uninstall_worked

      # remove this rcpt
      delete
      D3.log "Done, uninstalled #{edition}", :warn
      # run a postflight if needed
      if post_remove_script?
        (exit_status, output) = run_post_remove verbose
        if exit_status != 0
          raise D3::UninstallError,  "Error running post_remove script (exited #{exit_status}) for #{edition}"
        end
      end # if post_install_script?

    # uninstall failed, but force deletes rececipt
    else
      if force
        D3.log "Uninstall failed, but force-deleting receipt for #{edition}.", :warn
        delete
      else
        raise D3::UninstallError, "There was a problem uninstalling #{edition}"
      end # if force
    end #if uninstall_worked

    # do any sync-type auto installs if we just removed a pilot
    # then the machine will get any live edition if it should.
    D3::Client.do_auto_installs(OpenStruct.new) if depiloting

  ensure
    D3::Client.unset_env :removing
  end # begin...ensure


end

#uninstall_via_apple_rcpt(verbose = false) ⇒ Boolean

Uninstall this .pkg by looking up the files it installed via pkgutil and deleting them directly. Doesn’t talk to the JSS and only works for .pkg installers (.dmg installers don’t write their file lists to the local package db.) This means that it won’t run pre/post remove scripts either!

Parameters:

  • verbose (Boolean) (defaults to: false)

    Should each deleted file be meentioned

Returns:

  • (Boolean)

    Was the uninstall successful?

Raises:



758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
# File 'lib/d3/client/receipt.rb', line 758

def uninstall_via_apple_rcpt (verbose = false)

  D3.log "Uninstalling #{edition} via Apple pkg receipts", :debug
  raise D3::UninstallError,  "#{edition} is not a .pkg installer. Can't use Apple receipts." if @package_type == :dmg
  to_delete = {}
  begin
    installed_apple_rcpts = `#{JSS::Composer::PKG_UTIL} --pkgs`.split("\n")
    @apple_pkg_ids.each do |pkgid|
      unless installed_apple_rcpts.include? pkgid
        D3.log "No local Apple receipt for '#{pkgid}', ignoring", :warn
        next
      end

      # this gets them in reverse order, so we can
      # delete files and then test for and delete empty dirs on the way
      to_delete[pkgid] = `#{JSS::Composer::PKG_UTIL} --files '#{pkgid}' 2>/dev/null`.split("\n").reverse
      raise D3::UninstallError, "Error querying pkg file list for '#{pkgid}'" if $CHILD_STATUS.exitstatus > 0
    end # each pkgid

    to_delete.each do |pkgid, paths|
      D3.log "Deleting items installed by apple pkg-id #{pkgid}", :debug
      paths.each do |path|
        target = Pathname.new "/#{path}"
        target.delete if target.file?
        target.rmdir if target.directory? and target.children.empty?
        D3.log "Deleted #{path}", :debug
      end # each path
      system "#{JSS::Composer::PKG_UTIL} --forget '#{pkgid}' &>/dev/null"
    end # each |pkgid, paths|
  rescue
    D3.log $!, :warn
    D3.log_backtrace
    return false
  end # begin
  return true
end

#updateObject

Update the current receipt in the receipt store



879
880
881
# File 'lib/d3/client/receipt.rb', line 879

def update
  D3::Client::Receipt.add_receipt(self, :replace)
end