#! /usr/bin/env -i /usr/bin/ruby ## # Copyright (C) 2007 Apple Inc. All rights reserved. # # @APPLE_LICENSE_HEADER_START@ # # This file contains Original Code and/or Modifications of Original Code # as defined in and that are subject to the Apple Public Source License # Version 2.0 (the 'License'). You may not use this file except in # compliance with the License. Please obtain a copy of the License at # http://www.opensource.apple.com/apsl/ and read it before using this # file. # # The Original Code and all software distributed under the License are # distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER # EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, # INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. # Please see the License for the specific language governing rights and # limitations under the License. # # @APPLE_LICENSE_HEADER_END@ ## ENV.clear ENV['__CF_USER_TEXT_ENCODING'] = "0x#{Process::Sys::getuid()}:0:0" require 'osx/foundation' require 'optparse' require 'open3' require 'ipaddr' # NOTE Remember that this script is generally being called by either launchd or # some admin tool. This means that we must implement a best-effort service # model and never fail to do as much as possible. There will be no error output # unless the --verbose flag is specified. $VERBOSE = false # Print trace messages $LINGER = 0 # Wait for more preferences updates $RESTART = false # Force service restart $DEBUG = false # Print extra debugging statements $ID = '$Id: synchronize-preferences 32597 2007-07-22 20:51:18Z jpeach $' # Take an exclusive lock on ourselves. This is a mutex to serialise # simultaneous invocations. mutex = File.open($0, 'r') mutex.flock(File::LOCK_EX) $0 = File.basename($0) OSX.require_framework 'SystemConfiguration' # Wrapper class to access SCPreferences API. class Preferences def initialize(appid) # Unlike the CFPreferences API, SCPreferences requires the actual # plist filename, which always end in '.plist'. appid = "#{appid}.plist" unless appid =~ /\.plist$/ @prefs = OSX::SCPreferencesCreate(nil, $0, appid) @keys = Preferences.to_native(OSX::SCPreferencesCopyKeyList(@prefs)) @keys.push('PreferencesSignature') $stderr.print \ "SCPreferences (appid=#{appid}) keys: #{@keys.join(",")}\n" \ if $DEBUG end def each keys.each { | key | yield key, self[key] } end def signature sig = OSX::SCPreferencesGetSignature(@prefs) # Converting CFData to a string ends up with something that looks like # this: <0500000e 4a5e0f00 e8e24046 00000000 f8000000 00000000> # We strip the angle brackets and spaces to give a plain hex string. return sig.to_s.gsub(/[ <>]/, '') end def has_key?(key) return @keys.include?(key) end def [](key) case key when 'PreferencesSignature' return self.signature else val = OSX::SCPreferencesGetValue(@prefs, key) # Need to convert to a native Ruby type because we merge these # values with our default set, which are native types. return (Preferences.to_native(val) rescue nil) end end # Load a preferences hash from a plist. def Preferences.load_plist(path) print "#{$0}: loading #{path}\n" if $VERBOSE data = OSX::NSData.dataWithContentsOfFile(path) return nil unless data plist, format, err = OSX::NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription(data, OSX::NSPropertyListImmutable) if (plist == nil or !plist.kind_of? OSX::NSCFDictionary) return nil end return Preferences.to_native(plist) end # Convert a CFPropertyListRef to a native Ruby type. def Preferences.to_native(val) return nil if val == nil $stderr.print "converting (#{val.class})\n" if $DEBUG if val.kind_of? OSX::NSCFBoolean return (val == OSX::KCFBooleanTrue ? true : false) end if val.kind_of? OSX::NSCFString return val.to_s end if val.kind_of? OSX::NSCFNumber return val.to_i end if val.kind_of? OSX::NSCFArray array = [] val.each { |element| array += [ Preferences.to_native(element) ] } return array end if val.kind_of? OSX::NSCFDictionary hash = {} val.allKeys().each { | key | # Note: we need to convert both the key and the data, # otherwise we will end up indexed by OSX::NSCFString and # won't be able to index by Ruby Strings. new_key = Preferences.to_native(key) new_val = Preferences.to_native(val[key]) hash[new_key] = new_val } return hash end # NOTE: We don't convert CFData or CFDate because we # don't need them for the preferences we have. $stderr.print \ "#{$0}: preferences type #{val.class} is not supported\n" \ if $VERBOSE return nil end end # This class lets us run commands without invoking the shell. We are not to # concerned with trapping the error status, since we are implementing a # best-effort service model. It's nice to keep stdout and stderr separate # though. class ShellCommand LAUNCHCTL = '/bin/launchctl' SW_VERS = '/usr/bin/sw_vers' def ShellCommand.run(*cmd) # This ends up in Kernel.exec, which invokes the shell when passed a # single argument. return false unless cmd.length > 1 # A final layer of paranoia. return false unless (cmd[0] == LAUNCHCTL || cmd[0] == SW_VERS) print "#{$0}: running command: '#{cmd.join("' '")}'\n" if $VERBOSE; io = Open3.popen3(*cmd) { | stdin, stdout, stderr | stdin.close # we are not going to provide any input loop do read_array = [stdout, stderr].reject { |fd| fd.eof? } break if read_array.empty? ready = Kernel.select(read_array, nil, nil, 0.1) next if ready == nil ready.flatten.each { | fd | line = (fd.readline rescue nil) next if line == nil if (fd == stderr) $stderr.print "#{$0} (#{cmd[0]}): #{line}" \ if $VERBOSE else if block_given? yield line else $stdout.print line \ if $VERBOSE end end } end } end end class NotificationCenter def initialize @center = OSX::NSDistributedNotificationCenter.defaultCenter() end def post(notification, info) @center.postNotificationName_object_userInfo_options( notification, # notification name nil, # notification sender, info, # user info dictionary OSX::NSNotificationPostToAllSessions); end end class LaunchControl SMBD = { 'service' => 'org.samba.smbd', 'plist' => '/System/Library/LaunchDaemons/smbd.plist', 'enabled' => false, 'required' => false } WINBINDD = { 'service' => 'org.samba.winbindd', 'plist' => '/System/Library/LaunchDaemons/org.samba.winbindd.plist', 'enabled' => false, 'required' => true # We always want winbindd running } NMBD = { 'service' => 'org.samba.nmbd', 'plist' => '/System/Library/LaunchDaemons/nmbd.plist', 'enabled' => false, 'required' => false } @@force_restart = false # True is we should forcibly bounce all services @@force_disable = false # True if we should always disable all services @@config_change = true # False if the preferences didn't actually change def LaunchControl.force_restart @@force_restart = true end def LaunchControl.force_disable @@force_disable = true end # Mark this configuration as unchanged. Unless we are forcing something, # we can assume that there's no need to restart any services def LaunchControl.no_config_change @@config_change = false end def LaunchControl.init ShellCommand.run(ShellCommand::LAUNCHCTL, 'list') { |line| # List format is 3 fields: PID Status Label fields = line.split(/\s+/) next if fields.length != 3 case fields[2] when SMBD['service'] SMBD['enabled'] = true when WINBINDD['service'] WINBINDD['enabled'] = true when NMBD['service'] NMBD['enabled'] = true end } end def LaunchControl.require(service) print "#{$0}: requiring service #{service['service']}\n" \ if $VERBOSE case service when SMBD SMBD['required'] = true when WINBINDD WINBINDD['required'] = true when NMBD NMBD['required'] = true end end # XXX if the netbios name changed, we should always restart nmbd. # Actually, it is always safe to restart nmbd, it's only winbindd and smbd # that we need to worry about. def LaunchControl.sync print "#{$0}: syncing, config_change=#{@@config_change}, " + "force_restart=#{@@force_restart}, " + "force_disable=#{@@force_disable}\n" \ if $VERBOSE # Now we are going to restart all the necessary services. It's really # too difficult to figure out all the parameters could be changed # without requiring a restart. We should not spuriously end up here # too often because we know when the preferences changed, which # protects us from being called multiple times to update the same info. [SMBD, WINBINDD, NMBD].each { |service| if $VERBOSE print "#{$0}: service #{service['service']} " print "required=#{service['required']} " print "enabled=#{service['enabled']}\n" end # We need to leave all the services disabled. if @@force_disable launchd_disable(service['plist']) next end if service['required'] if service['enabled'] # Service already enabled. Restart it if necessary. if @@config_change || @@force_restart print "#{$0}: restarting #{service['service']}\n" \ if $VERBOSE launchd_stop(service['service']) end else launchd_enable(service['plist']) end else if service['enabled'] launchd_disable(service['plist']) end end } notify = NotificationCenter.new() notify.post('com.apple.ServiceConfigurationChangedNotification', { 'ServiceName' => 'smb', 'State' => (SMBD['enabled'] ? 'RUNNING' : 'STOPPED') }) end private def LaunchControl.launchd_stop(service) ShellCommand.run(ShellCommand::LAUNCHCTL, 'stop', service) end def LaunchControl.launchd_enable(plist) ShellCommand.run(ShellCommand::LAUNCHCTL, 'load', '-w', plist) end def LaunchControl.launchd_disable(plist) ShellCommand.run(ShellCommand::LAUNCHCTL, 'unload', '-w', plist) end end # Handle options which we can't or don't want to implement. class NullOption def initialize(name) @name = name end def emit(prefs, config) print "#{$0}: ignoring unimplemented option '#{@name}'\n" if $VERBOSE end def to_s return @name end end class SuspendOption def initialize(name) @name = name end def emit(prefs, config) if prefs[@name] LaunchControl.force_disable end end end # Handle options that have a simple 1-1 mapping to smb.conf options. class SimpleOption < NullOption def initialize(name, text) @name = name @text = text end def emit(prefs, config) # It's possible (in principle) that setting a value to empty might # be different to not setting it at all. I don't know of any such # cases, so let's avoid this for now -- jpeach. return unless self.has_value?(prefs) if (prefs[@name].class == TrueClass || prefs[@name].class == FalseClass) val = prefs[@name] ? 'yes' : 'no' elsif prefs[@name].class == String val = prefs[@name] elsif prefs[@name].class == Fixnum val = "#{prefs[@name]}" else # SimpleOption is only for simple options! $stderr.print \ "#{$0}: the #{@name} preference is an invalid type (#{prefs[@name].class})\n" \ if $VERBOSE return end config.append(@text, val) end def has_value?(prefs) val = prefs[@name] return false if val == nil return false if val.kind_of?(String) && val == "" return false if val.kind_of?(Array) && val.length == 0 return true end end # Handle setting the Kerberos realm (both the managed and local). class KerberosRealmOption < NullOption def initialize(realm_tag, local_realm_tag) @name = realm_tag @mrealm_tag = realm_tag @lrealm_tag = local_realm_tag end def emit(prefs, config) mrealm = SimpleOption.new(@mrealm_tag, 'realm') lrealm = SimpleOption.new(@lrealm_tag, 'com.apple: lkdc realm') # Just bail immediately if neither the managed nor local realm is # configured. return unless (mrealm.has_value?(prefs) || lrealm.has_value?(prefs)) mrealm.emit(prefs, config) lrealm.emit(prefs, config) config.append('use kerberos keytab', 'yes') # If we have a local realm, but no managed realm, set both to the # local realm. if lrealm.has_value?(prefs) && !mrealm.has_value?(prefs) SimpleOption.new(@lrealm_tag, 'realm').emit(prefs, config) end end end # Handle the case where we want to register our name with a WINS server. We # take a bool that indicates whether we want to register and a list of IP # addresses for the servers we will register with. class WinsRegisterOption < NullOption def initialize(register_tag, addrlist_tag) @name = register_tag @addrlist = addrlist_tag end def emit(prefs, config) return unless prefs[@name] # We know we want to register, so check for WINS server addresses. The # order of operations here matters, because we can throw an exception. list = IPAddressOption.new(@addrlist, 'wins server') list.emit(prefs, config) LaunchControl.require(LaunchControl::NMBD) end end # Configure whether guest access is allowed globally. If this is enabled, the # per-share guest settings will be respected. If this is disabled, then no # guest for you, even if it is enabled on a share. class GuestAccessOption < NullOption def initialize(pref) @name = pref end def emit(prefs, config) if self.enabled(prefs) config.append('map to guest', 'Bad User') else config.append('map to guest', 'Never') end config.append_section(SmbConfigFile::STATUS, 'Guest access', self.enabled(prefs) ? 'per-share' : 'never') end def enabled(prefs) return prefs[@name] ? true : false end end class ServicesOption < NullOption WINS = 'wins' DISK = 'disk' PRINT = 'print' def initialize(pref) @name = pref end def emit(prefs, config) disk = false print = false wins = false prefs[@name] = [] unless prefs[@name] != nil begin # If the preferences file is messed up, prefs[@name] might not # be an array or it might be missing, hence the exception handler prefs[@name].each { |service| case when service.casecmp(WINS) == 0 wins = true LaunchControl.require(LaunchControl::NMBD) when service.casecmp(DISK) == 0 disk = true LaunchControl.require(LaunchControl::NMBD) LaunchControl.require(LaunchControl::SMBD) when service.casecmp(PRINT) == 0 print = true LaunchControl.require(LaunchControl::NMBD) LaunchControl.require(LaunchControl::SMBD) end } rescue StandardError => err if $VERBOSE $stderr.print "#{$0}: #{err}\n" $stderr.print \ "#{$0}: the #{@name} preference is missing or invalid\n" end end # Tell nmbd to act as a WINS server. config.append('wins support', wins ? 'yes' : 'no') config.append('enable disk services', disk ? 'yes' : 'no') config.append('enable print services', print ? 'yes' : 'no') end end # Handle automatic shares. The two cases we care about are where the logged-in # user is shown their own home directory and where members of the "admin" group # are shown all local volumes. class AutoSharesOption < NullOption def initialize(homes_tag, admin_tag) super("#{homes_tag} or #{admin_tag}") @homes = homes_tag @admin = admin_tag end def emit(prefs, config) # XXX This is dubious. We should have a way to enable virtual admin # shares without also enabling virtual home shares. need_homes = (prefs[@homes] || prefs[@admin]) if need_homes config.append_section(SmbConfigFile::HOMES, 'comment', 'User Home Directories') config.append_section(SmbConfigFile::HOMES, 'browseable', 'no') config.append_section(SmbConfigFile::HOMES, 'read only', 'no') config.append_section(SmbConfigFile::HOMES, 'create mode', '0750') config.append_section(SmbConfigFile::HOMES, 'guest ok', 'no') end if prefs[@admin] config.append_section(SmbConfigFile::HOMES, 'com.apple: show admin all volumes', prefs[@admin]) end end end class IPAddressOption < NullOption def initialize(name, text) @name = name @text = text end def emit(prefs, config) return unless prefs[@name] addresses = [] # Convert the strings to IP addresses just to validate the syntax prefs[@name].each { | addr | ip = (IPAddr.new(addr, Socket::AF_INET) rescue nil) unless ip $stderr.print \ "#{$0} (#{@name}): #{addr} is not a valid IPv4 address\n" \ if $VERBOSE next end addresses += [ addr ] } config.append(@text, addresses.join(' ')) if addresses.length > 0 end end # Handle the NetBIOS browse master configuration. Depending on the server role, # we will attempt to become with a domain or a master browser. class MasterBrowserOption < NullOption LocalMaster = 'local' DomainMaster = 'domain' def initialize(name, role) @name = name @smbrole = role end def emit(prefs, config) val = prefs[@name] ? prefs[@name] : 'none' case @smbrole.role(prefs) when ServerRoleOption::PDC # If we are a PDC, we really have to be a master browser. val = DomainMaster when ServerRoleOption::BDC # If we are a BDC, we really have to be a local browser. val = LocalMaster end case when val.casecmp(DomainMaster) == 0 config.append('domain master', 'yes') config.append('preferred master', 'yes') config.append('os level', '65') config.append_section(SmbConfigFile::STATUS, 'NetBIOS browsing', 'domain master browser') when val.casecmp(LocalMaster) == 0 config.append('domain master', 'no') config.append('local master', 'yes') config.append('preferred master', 'yes') config.append('os level', '65') config.append_section(SmbConfigFile::STATUS, 'NetBIOS browsing', 'local master browser') else config.append('domain master', 'no') config.append('local master', 'no') config.append('preferred master', 'no') config.append_section(SmbConfigFile::STATUS, 'NetBIOS browsing', 'not a master browser') return end # Yes, to be a master browser, we need to be running both smbdand # and nmbd. LaunchControl.require(LaunchControl::NMBD) LaunchControl.require(LaunchControl::SMBD) end end class ServerRoleOption < NullOption STANDALONE = 'Standalone' ADS = 'ActiveDirectoryMember' PDC = 'PrimaryDomainController' BDC = 'BackupDomainController' DOMAIN = 'DomainMember' MACHINE_SCRIPT = '/usr/bin/opendirectorypdbconfig ' + '-c create_computer_account -r %u -n "/LDAPv3/127.0.0.1"' USER_SCRIPT = '/usr/bin/opendirectorypdbconfig ' + '-c create_user_account -r %u -n "/LDAPv3/127.0.0.1"' def initialize(name, guest) @name = name @guestopt = guest end # Return the configured role. STANDALONE is the default in the case of an # invalid or missing role. def role(prefs) current_role = prefs[@name] return STANDALONE unless current_role case when current_role.casecmp(DOMAIN) == 0 return DOMAIN when current_role.casecmp(ADS) == 0 return ADS when current_role.casecmp(PDC) == 0 return PDC when current_role.casecmp(BDC) == 0 return BDC else return STANDALONE end end def emit(prefs, config) auth = @guestopt.enabled(prefs) ? "guest odsam" : "odsam"; case self.role(prefs) # STANDALONE and ADS are the same except for the security mode. when STANDALONE config.append('security', 'USER') config.append('use spnego', 'yes') # Member of an AD domain. when ADS config.append('security', 'ADS') config.append('use spnego', 'yes') # Member of an NT4 domain. when DOMAIN config.append('security', 'DOMAIN') config.append('domain logons', 'no') auth = @guestopt.enabled(prefs) ? "guest ntdomain odsam" \ : "ntdomain odsam" # PDC of an NT4 domain. when PDC config.append('security', 'USER') config.append('add machine script', MACHINE_SCRIPT) config.append('add user script', USER_SCRIPT) add_domain_logon(config) # BDC of an NT4 domain. when BDC config.append('security', 'USER') add_domain_logon(config) end config.append('auth methods', auth) config.append_section(SmbConfigFile::STATUS, 'Server role', self.role(prefs)) end # Add the parameters we need to for domain logons. def add_domain_logon(config) config.append('domain logons', 'yes') # If we are doing user logons, autocreate a home directory. config.append_section(SmbConfigFile::HOMES, 'root preexec', '/usr/sbin/inituser %U') config.append('logon drive', 'H:') config.append('logon path', '\\%N\profiles%u') # Add a [netlogon] share. config.append_section(SmbConfigFile::NETLOGON, 'path', '/etc/netlogon') config.append_section(SmbConfigFile::NETLOGON, 'browseable', 'no') config.append_section(SmbConfigFile::NETLOGON, 'write list', '@admin') config.append_section(SmbConfigFile::NETLOGON, 'oplocks', 'yes') config.append_section(SmbConfigFile::NETLOGON, 'strict locking', 'no') # Add a [profiles] share. config.append_section(SmbConfigFile::PROFILES, 'path', '/Users/Profiles') config.append_section(SmbConfigFile::NETLOGON, 'browseable', 'no') config.append_section(SmbConfigFile::NETLOGON, 'read only', 'no') config.append_section(SmbConfigFile::NETLOGON, 'oplocks', 'yes') config.append_section(SmbConfigFile::NETLOGON, 'strict locking', 'no') end end # Determine whether the preferences have changed since the last time we # synchronized. class PrefsChangedOption < NullOption @@force_change = false def PrefsChangedOption.force_change @@force_change = true end def initialize(name, path) @name = name @path = path @prev_sig, @prev_gen = get_config_signatures(@path) end def emit(prefs, config) LaunchControl.no_config_change unless self.changed(prefs) if prefs[@name] config.append_section(SmbConfigFile::STATUS, 'Preferences signature', prefs[@name]) end # Stash the svn Id keyword so we can tell which script version # generated this configuration. config.append_section(SmbConfigFile::STATUS, 'Preferences generator', $ID) end # Return true if the preferences have changed (ie. we need to regenerate # the SMB configuration). def changed(prefs) curr_sig = prefs[@name] if @@force_change return true end if curr_sig == nil || curr_sig != @prev_sig # Preference signature didn't match, don't bother checking the Id. return true end if @prev_gen == nil || @prev_gen != $ID # This config was generated by a different version of this script. # We should be regenerating it because the configuration rules # might have changed. return true end return false end private def get_config_signatures(path) signature = nil generator = nil begin File.open(path, 'r') { | fd | fd.each_line { | line | if line.match(/Preferences signature: ([abcdef0-9]+)/) signature = $1 end if line.match(/Preferences generator: (\$Id*\$)/) generator = $1 end if (signature && generator) return [signature, generator] end } } return [signature, generator] rescue Exception => err if $VERBOSE $stderr.print "#{$0}: unable to read signatures from #{path}\n" $stderr.print "#{$0}: #{err}\n" end end return [nil, nil] end end class SmbConfigFile SmbConfigPath = '/etc/smb.conf' SmbRunConfigPath = '/var/run/smb.conf' GLOBAL = 'global' PRINTERS = 'printers' HOMES = 'homes' NETLOGON = 'netlogon' PROFILES = 'profiles' STATUS = '_internal_only_' def initialize() # @config is a hash where the keys are section labels and the # values are a list of the key/value pairs in that section. @config = { GLOBAL => {}, HOMES => {}, NETLOGON => {}, PROFILES => {}, PRINTERS => {}, STATUS => {} } end def sections return @config.keys end def lines(section) unless @config.has_key? section raise ArgumentError, "invalid config section #{section}", caller end @config[section].keys.sort.each { | key | yield key, @config[section][key] } end def has_section?(section) unless @config.has_key? section raise ArgumentError, "invalid config section #{section}", caller end return @config[section].length > 0 ? true : false end def append(key, value) append_section(GLOBAL, key, value) end def append_section(section, key, value) unless @config.has_key? section raise ArgumentError, "invalid config section #{section}", caller end @config[section][key.downcase] = value end end class SmbPreferences SERVER = 1 DESKTOP = 2 AppID = 'com.apple.smb.server' DefaultPrefsPath = \ '/System/Library/CoreServices/SmbFileServer.bundle/Resources' ServerDefaultPrefs = "#{DefaultPrefsPath}/ServerDefaults.plist" DesktopDefaultPrefs = "#{DefaultPrefsPath}/DesktopDefaults.plist" # Builtin preferences to be used when neither the Server nor the Desktop # defaults can be found. BUILTIN = { 'NetBIOSName' => nil, 'NetBIOSNodeType' => nil, 'NetBIOSScope' => nil, 'WINSServerAddressList' => [], 'Workgroup' => 'WORKGROUP', 'KerberosRealm' => nil, 'LocalKerberosRealm' => nil, 'SuspendServices' => false, 'EnabledServices' => [], 'ServerRole' => ServerRoleOption::STANDALONE, 'ServerDescription' => 'Mac OS X', 'AllowGuestAccess' => false, 'MaxClients' => 10 , 'AllowKerberosAuth' => true, 'AllowNTLMAuth' => true, 'AllowLanManAuth' => false, 'LoggingLevel' => 1, 'DOSCodePage' => '437', 'MasterBrowser' => false, 'RegisterWINSName' => false, 'VirtualHomeShares' => true, 'VirtualAdminShares' => true, 'PasswordServer' => nil, 'PreferencesSignature' => nil, } def SmbPreferences.system_type is_server = false ShellCommand.run(ShellCommand::SW_VERS, '-productName') { |line| is_server = true if line =~ /Server/ } return (is_server ? SERVER : DESKTOP) end def SmbPreferences.defaults prefs = {} case system_type() when SERVER prefs = Preferences.load_plist(ServerDefaultPrefs) when DESKTOP prefs = Preferences.load_plist(DesktopDefaultPrefs) end unless prefs $stderr.print \ "#{$0}: failed to load default preferences\n" return {} end return prefs end def initialize # Start with the BUILTIN preferences set and merge in the defaults # from the static plists. This protects us if they ever go AWOL. @current = BUILTIN self.merge_prefs(SmbPreferences.defaults) end # Merge the given set of Preferences with the current set. def merge_prefs(prefs) keys = @current.keys keys.each { | key | # Only override the preference if we know there is a key for it. # This prevents us acceidentally overriding with nil by # referencing a key that's not present, but allows us to override # with nil when it's actually specified. next unless prefs.has_key?(key) val = prefs[key] $stderr.print \ "updating #{key} from '#{@current[key]}' to '#{val}'\n" \ if $DEBUG @current[key] = val } end def each @current.each do |key, value| yield key, value end end def [](key) return @current[key] end # Write the generated config to the config file. def sync_config(optionlist) File.open(SmbConfigFile::SmbRunConfigPath, "w") { | fd | config_lines(optionlist) { | line | fd.write(line) } fd.fsync } end # Print the generated config to stdout. def print_config(optionlist) config_lines(optionlist) { | line | $stdout.print line } end private def config_lines(optionlist) config = SmbConfigFile.new() optionlist.each do |option| begin option.emit(@current, config) rescue Exception => err if $VERBOSE $stderr.print "#{$0}: failed to handle option #{option}\n" $stderr.print "#{$0}: #{err}\n" end end end yield < err $stderr.print "#{$0}: #{err}\n" if $VERBOSE return 1 end LaunchControl.init LaunchControl.sync return 0 end def Command.ListPending smbopts = SmbPreferences.new() smbopts.merge_prefs(Preferences.new(SmbPreferences::AppID)) smbopts.print_config(OPTIONS) return 0 end def Command.ListDefaults smbopts = SmbPreferences.new() smbopts.print_config(OPTIONS) return 0 end def Command.ChangesPending if Command.need_pref_sync print "#{$0}: configuration is out of date\n" if $VERBOSE return 0 else print "#{$0}: configuration is current\n" if $VERBOSE return 2 end end private def Command.need_pref_sync prefs = Preferences.new(SmbPreferences::AppID) check = PrefsChangedOption.new('PreferencesSignature', SmbConfigFile::SmbRunConfigPath) return check.changed(prefs) end end opts = OptionParser.new opts.on('--verbose', 'print extra debugging messages') { $VERBOSE = true } opts.on('--linger=ARG', Integer, 'stick around and sync updates until ARG', 'seconds of inactivity') { |val| $LINGER = val.to_i } opts.on('--force-sync', 'force synchronization even if it is', 'unnecessary') { PrefsChangedOption.force_change } opts.on('--restart-services', 'restart any services that are', 'already running') { LaunchControl.force_restart } # Disable all the services. This is for apps that are going to mess with the # Samba state in more detail. They should set the preferences, then synchronize # and suspend. Once they have done the rest of the configuration, they can # synchronize again and the services will be started. opts.on('--suspend-services', "leave all services disabled") { LaunchControl.force_disable } opts.on('--changes-pending', 'exit with 0 status if there are', 'unsynchronized changes') { exit Command.ChangesPending } opts.on('--list-pending', 'print the pending configuration, but do', 'not synchronize') { exit Command.ListPending } opts.on('--list-defaults', "print the default configuration and exit") { exit Command.ListDefaults } begin opts.parse!(ARGV) # Remove args as they are parsed. if (ARGV.length != 0) raise OptionParser::InvalidOption, ARGV[0], caller end rescue OptionParser::ParseError => err $stderr.print "#{$0}: #{err}\n" $stderr.print opts.help() exit 1 end if $LINGER <= 0 exit Command.SyncPrefs else # Do an initial sync to handle the change that we started up for. Command.SyncPrefs print "#{$0}: lingering for #{$LINGER}s\n" if $VERBOSE stop = Time.now() + $LINGER path = \ "/Library/Preferences/SystemConfiguration/#{SmbPreferences::AppID}.plist" last = File.mtime(path) # NB. We should be using SCPreferencesSetCallback and a runloop to figure # out when the changes happen, but System Prefs writes the plist directly, # so we have to resort to checking the mtime. while Time.now() < stop sleep 0.2 current = File.mtime(path) if last != current last = current Command.SyncPrefs # Push out the stop time since there was some activity. stop = Time.now() + $LINGER end end exit 0 end # vim: filetype=ruby ai ts=8 sts=4 sw=4 tw=79