Module: StateMate::Adapters::Defaults

Includes:
StateMate::Adapters
Defined in:
lib/state_mate/adapters/defaults.rb

Constant Summary collapse

KEY_SEP =

string seperator used to split keys

':'
DEFAULTS_CMD =

path to the defaults system command

'/usr/bin/defaults'

Constants included from StateMate::Adapters

API_METHOD_NAMES, DEFAULT_KEY_SEP

Class Method Summary collapse

Methods included from StateMate::Adapters

get, included, register

Class Method Details

.basic_delete(domain, key, current_host) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

does a delete of either a entire domain's properties or a single top level key directly using defaults delete ....

called by basic_write when it's provided nil for a value.

Parameters:

  • domain (String)

    that defaults will accept as a domain.

  • key (String)

    that defaults will accept as a key (top-level only).

  • current_host (Boolean)

    if true, the write will be done for the domain's "current host" plist file (using the -currentHost option when calling the system's defaults command).

Returns:

  • nil



504
505
506
507
508
509
510
511
512
513
514
515
# File 'lib/state_mate/adapters/defaults.rb', line 504

def self.basic_delete domain, key, current_host
  sudo = domain.start_with?('/Library') ? "sudo" : nil
  
  Cmds! '%{sudo?} %{cmd} %{current_host?} delete %{domain} %{key?}',
        cmd: DEFAULTS_CMD,
        current_host: (current_host ? '-currentHost' : nil),
        domain: domain,
        key: (key ? key : nil),
        sudo: sudo
  
  nil
end

.basic_write(domain, key, value, current_host) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

does a write of either a entire domain's properties or a single top level key directly using defaults write ....

called by write when there are zero or one key segments.

Parameters:

  • domain (String)

    that defaults will accept as a domain.

  • key (String)

    that defaults will accept as a key (top-level only).

  • value (Object)

    something that is acceptible to to_xml_element.

  • current_host (Boolean)

    if true, the write will be done for the domain's "current host" plist file (using the -currentHost option when calling the system's defaults command).

Returns:

  • nil



537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
# File 'lib/state_mate/adapters/defaults.rb', line 537

def self.basic_write domain, key, value, current_host
  if value.nil?
    basic_delete(domain, key, current_host)
  else
    sudo = domain.start_with?('/Library') ? "sudo" : nil
    
    Cmds! '%{sudo?} %{cmd} %{current_host?} write %{domain} %{key?} %{xml}',
          cmd: DEFAULTS_CMD,
          current_host: (current_host ? '-currentHost' : nil),
          domain: domain,
          key: (key ? key : nil),
          xml: to_xml_element(value).to_s,
          sudo: sudo
  end
  
  nil
end

.deep_write(domain, key, deep_segs, value, current_host) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

internal compliment to basic_write that writes "deep" keys (keys with additional segments beyond domain and top-level).

Parameters:

  • domain (String)

    domain string that defaults will accept.

  • key (String)

    key string that defaults will accept.

  • deep_segs (Array<String>)

    non-empty strings that form the "deep" part of the key.

  • value (Object)

    something that is acceptible to to_xml_element.

  • current_host (Boolean)

    if true, the write will be done for the domain's "current host" plist file (using the -currentHost option when calling the system's defaults command).

Returns:

  • nil



576
577
578
579
580
581
582
583
# File 'lib/state_mate/adapters/defaults.rb', line 576

def self.deep_write domain, key, deep_segs, value, current_host
  root = read [domain, key], current_host: current_host
  # handle the root not being there
  root = {} unless root.is_a? Hash
  hash_deep_write! root, deep_segs, value
  basic_write domain, key, root, current_host
  nil
end

.domain_to_filepath(domain, user = ENV['USER'], current_host = false) ⇒ String

get the filepath to the .plist for a domain string.

not currently called by any StateMate stuff but seemed nice to keep around for scripts and the like.

Parameters:

  • domain (Stirng)

    handles domains and path in the forms

    • absolute paths (that start with /)
    • home-based paths (that start with ~)
    • "NSGlobalDomain" for the global domain
    • stadard domain-style paths ("com.nrser.state_mate" style)
  • user (String) (defaults to: ENV['USER'])

    user name ("nrser", "root", etc.)

  • current_host (Boolean) (defaults to: false)

    whether to path to the "current host" location of a domain. onyl applicable when using global or domain-style domain argument.

Returns:

  • (String)

    path to .plist file (which may not exist)



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/state_mate/adapters/defaults.rb', line 235

def self.domain_to_filepath domain, user = ENV['USER'], current_host = false
  # there are a few cases:
  #
  # 1.) absolute file path
  if domain.start_with? '/'
    domain
  # 
  # 2.) home-based path
  elsif domain.start_with? '~/'
    if user == 'root'
      "/var/root/#{ domain[2..-1] }"
    else
      "/Users/#{ user }/#{ domain[2..-1] }"
    end
  #
  # global domain
  elsif domain == "NSGlobalDomain"
    if current_host
      "#{ prefs_path user }/.GlobalPreferences.#{ hardware_uuid }.plist"
    else
      "#{ prefs_path user }/.GlobalPreferences.plist"
    end
  # 
  # 3.) domain with corresponding plist
  else
    if current_host
      "#{ prefs_path user }/ByHost/#{ domain }.#{ hardware_uuid }.plist"
    else
      "#{ prefs_path user }/#{ domain }.plist"
    end
  end
end

.hardware_uuidString

get the "by host" / "current host" id, also called the "hardware uuid".

adapted from

http://stackoverflow.com/questions/933460/unique-hardware-id-in-mac-os-x

Returns:

  • (String)

    the hardware uuid



205
206
207
208
209
210
# File 'lib/state_mate/adapters/defaults.rb', line 205

def self.hardware_uuid
  plist_xml_str = Cmds!("ioreg -r -d 1 -c IOPlatformExpertDevice -a").out
  plist = CFPropertyList::List.new data: plist_xml_str
  dict = CFPropertyList.native_types(plist.value).first
  dict['IOPlatformUUID']
end

.hash_deep_write!(hash, key, value) ⇒ Object

does a "deep" mutating write in a Hash given a series of keys and a value.

Parameters:

  • hash (Hash)

    the hash to modify.

  • key (Array<Object>)

    series of keys.

  • value (Object)

    value to write.

Returns:

  • the value.



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/state_mate/adapters/defaults.rb', line 373

def self.hash_deep_write! hash, key, value
  segment = key.first
  rest = key[1..-1]

  # terminating case: we are at the last segment
  if rest.empty?
    hash[segment] = value
  else
    case hash[segment]
    when Hash
      # go deeper
      hash_deep_write! hash[segment], rest, value
    else
      hash[segment] = {}
      hash_deep_write! hash[segment], rest, value
    end
  end
  value
end

.native_types(object, keys_as_symbols = false) ⇒ Object

pure

creates a native Ruby type represnetation of a CFType hiercharchy.

customized from CFPropertyList to use the Base64 encoding of binary blobs since JSON pukes on the raw ones.

Parameters:

  • object (CFPropertyList::CFType, nil)

    the object to convert.

  • keys_as_symbols (Boolean) (defaults to: false)

    provide true to convert dictionary keys to Symbols instead of the default Strings.

Returns:

  • native ruby object represnetation of the CFType.



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/state_mate/adapters/defaults.rb', line 330

def self.native_types(object,keys_as_symbols=false)
  return if object.nil?

  if (object.is_a?(CFPropertyList::CFDate) ||
      object.is_a?(CFPropertyList::CFString) || 
      object.is_a?(CFPropertyList::CFInteger) || 
      object.is_a?(CFPropertyList::CFReal) || 
      object.is_a?(CFPropertyList::CFBoolean)) || 
      object.is_a?(CFPropertyList::CFUid) then
    return object.value
  elsif(object.is_a?(CFPropertyList::CFData)) then
    return CFPropertyList::Blob.new(object.encoded_value)
  elsif(object.is_a?(CFPropertyList::CFArray)) then
    ary = []
    object.value.each do
      |v|
      ary.push native_types(v)
    end

    return ary
  elsif(object.is_a?(CFPropertyList::CFDictionary)) then
    hsh = {}
    object.value.each_pair do
      |k,v|
      k = k.to_sym if keys_as_symbols
      hsh[k] = native_types(v)
    end

    return hsh
  end
end

.parse_key(key) ⇒ Array<String, Array<String>>

pure

parses the key into domain and key segments.

Parameters:

  • key (Array<String>, String)

    an Array of non-empty Strings or a a String that splits by : into an non-empty Array of non-empty Strings.

Returns:

  • (Array<String, Array<String>>)

    the String domain followed by an array of key segments.

Raises:

  • (ArgumentError)

    if the key does not parse into a non-empty list of non-empty strings.



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/state_mate/adapters/defaults.rb', line 284

def self.parse_key key
  strings = case key
  when Array
    key
  when String
    key.split KEY_SEP
  else
    raise "must be string or array, not #{ key.inspect }"
  end # case
  
  # make sure there is at least one element
  if strings.empty?
    raise ArgumentError.new NRSER.squish "      key parsed into empty list: \#{ key.inspect }.\n    END\n  end\n  \n  # check for non-strings, empty domain or key segments\n  strings.each do |string|\n    if !string.is_a?(String) || string.empty?\n      raise ArgumentError.new NRSER.squish <<-END\n        domain and all key segments must be non-empty Strings,\n        found \#{ string.inspect } in key \#{ key.inspect }.\n      END\n    end\n  end\n\n  [strings[0], strings[1..-1]]\nend\n"

.prefs_path(user) ⇒ String

pure

builds the Preferences folder path depending on the user given, which will be either

"/Library/Preferences"

if user is "root", otherwise

"/Users/#{ user }/Library/Preferences"

Parameters:

  • user (String)

    the user in question.

Returns:

  • (String)

    the path to their Preferences folder.



186
187
188
189
190
191
192
# File 'lib/state_mate/adapters/defaults.rb', line 186

def self.prefs_path user
  if user == 'root'
    '/Library/Preferences'
  else
    "/Users/#{ user }/Library/Preferences"
  end
end

.read(key, options = {}) ⇒ Object

the API method that StateMate.execute calls (through StateSet#execute) to read the value of a (possibly deep) key.

Parameters:

  • key (String)

    a : seperated string who's first segment is the domain and remaining segments are keys in presumably nested dictionaries

    Defaults.read "com.nrser.state_mate:x:y"
    

    would read the com.nrser.state_mate domain's x key, and, assuming it's a dictionary, get the value of it's y key. if the method stops finding dictionaries at any point travering the key it will return nil.

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :current_host (Boolean)

    if true, the read will be done for the domain's "current host" plist file (using the -currentHost option when calling the system's defaults command).

Returns:

  • our Ruby representation of the value, or nil if it's not found.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/state_mate/adapters/defaults.rb', line 54

def self.read key, options = {}
  options = {
    current_host: false,
  }.merge options

  domain, key_segs = parse_key key

  value = read_defaults domain, options[:current_host]

  key_segs.each do |seg|
    value = if (value.is_a?(Hash) && value.key?(seg))
      value[seg]
    else
      nil
    end
  end

  value
end

.read_defaults(domain, current_host = false) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

does an system call to read and parse an domain's entire plist file using defaults export ....

Parameters:

  • domain (String)

    anything defaults excepts as a domain. think it can still be a filepath (they've been saying they're gonna depreciate that) or a "com.whatever.someapp" type string.

  • current_host (Boolean) (defaults to: false)

    whether to read the defaults for the "current host" by including the -currentHost flag

Returns:

  • (Hash)

    our Ruby representation of the underlying property list.

Raises:

  • (SystemCallError)

    if the defaults command fails.



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/state_mate/adapters/defaults.rb', line 413

def self.read_defaults domain, current_host = false
  file = Tempfile.new('read_defaults')
  begin
    Cmds! '%{cmd} %{current_host?} export %{domain} %{filepath}',
          cmd: DEFAULTS_CMD,
          current_host: (current_host ? '-currentHost' : nil),
          domain: domain,
          filepath: file.path

    plist = CFPropertyList::List.new file: file.path
    data = native_types plist.value
  ensure
    file.close
    file.unlink   # deletes the temp file
  end
end

.read_type(domain, key, current_host) ⇒ Symbol

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

reads the type of key using defauls read-type ... (hence it only reads top-level keys).

Parameters:

  • domain (String)

    anything defaults excepts as a domain. think it can still be a filepath (they've been saying they're gonna depreciate that) or a "com.whatever.someapp" type string.

  • key (String)

    the key to read (top-level only).

  • current_host (Boolean)

    whether to read the type for the "current host" by including the -currentHost flag

Returns:

  • (Symbol)

    one of

    • :string
    • :data
    • :int
    • :float
    • :bool
    • :date
    • :array
    • :dict


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/state_mate/adapters/defaults.rb', line 455

def self.read_type domain, key, current_host    
  result = Cmds!  '%{cmd} %{current_host?} read-type %{domain} %{key}',
                  cmd: DEFAULTS_CMD,
                  current_host: (current_host ? '-currentHost' : nil),
                  domain: domain,
                  key: key

  out = result.out.chomp

  case out
  when "Type is string"
    :string
  when "Type is data"
    :data
  when "Type is integer"
    :int
  when "Type is float"
    :float
  when "Type is boolean"
    :bool
  when "Type is date"
    :date
  when "Type is array"
    :array
  when "Type is dictionary"
    :dict
  else
    raise "unknown output: #{ out.inspect }"
  end
end

.to_xml_element(obj) ⇒ REXML::Element

pure

convert a ruby object to a REXML::Element for a plist.

not sure why i'm using this instead of something from CFPropertyList... maybe it's a left-over from before CFPropertyList was included, maybe there was some issue with CFPropertyList... not sure.

Parameters:

  • obj (String, Fixnum, Float, Hash, Array, Boolean, Time)

    object to convert. Hashs and Arrays need to be composed of the same types.

Returns:

  • (REXML::Element)

    the XML element representation.

Raises:

  • (TypeError)

    if it can't handle the type of obj.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/state_mate/adapters/defaults.rb', line 139

def self.to_xml_element obj
  case obj
  when String
    REXML::Element.new("string").add_text obj
  when Fixnum
    REXML::Element.new('integer').add_text obj.to_s
  when Float
    REXML::Element.new('real').add_text obj.to_s
  when Hash
    dict = REXML::Element.new('dict')
    obj.each {|dict_key, dict_obj|
      dict.add_element REXML::Element.new('key').add_text(dict_key)
      dict.add_element to_xml_element(dict_obj)
    }
    dict
  when Array
    array = REXML::Element.new('array')
    obj.each {|array_entry|
      array.add_element to_xml_element(array_entry)
    }
    array
  when TrueClass, FalseClass
    REXML::Element.new obj.to_s
  when Time
    REXML::Element.new('date').add_text obj.utc.iso8601
  else
    raise TypeError, "can't handle type: #{ obj.inspect }"
  end
end

.write(key, value, options = {}) ⇒ Object

the API method that StateMate.execute calls (through StateSet#execute) to write the value of a (possibly deep) key.

Parameters:

  • key (String)

    a : seperated string who's first segment is the domain and remaining segments are keys in presumably nested dictionaries

    Defaults.write "com.nrser.state_mate:x", 1
    

    would write the integer 1 the com.nrser.state_mate domain's x key.

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :current_host (Boolean)

    if true, the read will be done for the domain's "current host" plist file (using the -currentHost option when calling the system's defaults command).

Returns:

  • nil



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/state_mate/adapters/defaults.rb', line 96

def self.write key, value, options = {}
  options = {
    current_host: false,
  }.merge options

  domain, key_segs = parse_key key

  if key_segs.length > 1
    deep_write  domain,
                key_segs[0],
                key_segs.drop(1),
                value,
                options[:current_host]
  else
    basic_write domain,
                key_segs[0],
                value,
                options[:current_host]
  end
  
  nil
end