diff --git a/lib/puppet/provider/loginctl_user/ruby.rb b/lib/puppet/provider/loginctl_user/ruby.rb index 2bc3529f..86475213 100644 --- a/lib/puppet/provider/loginctl_user/ruby.rb +++ b/lib/puppet/provider/loginctl_user/ruby.rb @@ -33,8 +33,48 @@ def linger=(value) case value when :enabled loginctl('enable-linger', resource[:name]) + # Wait for systemd --user instance to be ready after enabling linger + wait_for_user_systemd_ready(resource[:name]) when :disabled loginctl('disable-linger', resource[:name]) end end + + def wait_for_user_systemd_ready(username, timeout = 10) + # Try to connect to the user's systemd instance using loginctl show-user + # which queries the user's systemd instance state + start_time = Time.now + loop do + begin + # Try to get user runtime directory which indicates systemd --user is running + output = loginctl('show-user', username, '-p', 'RuntimePath') + runtime_path = output.strip.split('=')[1] + + # Check if the systemd --user socket exists and is accessible + if runtime_path && !runtime_path.empty? + socket_path = "#{runtime_path}/systemd/private" + if File.exist?(socket_path) + # try to actually communicate with systemd --user + # by checking if the user's systemd is in running state + state_output = loginctl('show-user', username, '-p', 'State') + state = state_output.strip.split('=')[1] + if %w[active lingering].include?(state) + Puppet.debug("systemd --user for #{username} is ready") + break + end + end + end + rescue Puppet::ExecutionFailure => e + # loginctl command failed, systemd --user might not be ready yet + Puppet.debug("Waiting for systemd --user for #{username}: #{e.message}") + end + + if Time.now - start_time > timeout + Puppet.warning("Timeout waiting for systemd --user instance for user #{username} to become ready, continuing anyway") + break + end + + sleep 1 + end + end end diff --git a/spec/acceptance/user_service_spec.rb b/spec/acceptance/user_service_spec.rb index cad887b9..af3df4a3 100644 --- a/spec/acceptance/user_service_spec.rb +++ b/spec/acceptance/user_service_spec.rb @@ -22,13 +22,6 @@ linger => enabled, } - # https://github.com/voxpupuli/puppet-systemd/issues/578 - exec{'/usr/bin/sleep 10 && touch /tmp/sleep-only-once': - creates => '/tmp/sleep-only-once', - require => Loginctl_user['higgs'], - before => File['/home/higgs/.config'], - } - # Assumes home directory was created as /home/higgs file{['/home/higgs/.config', '/home/higgs/.config/systemd','/home/higgs/.config/systemd/user']: ensure => directory, diff --git a/spec/unit/puppet/provider/loginctl_user/ruby_spec.rb b/spec/unit/puppet/provider/loginctl_user/ruby_spec.rb index c9091d48..3a9b5a9b 100644 --- a/spec/unit/puppet/provider/loginctl_user/ruby_spec.rb +++ b/spec/unit/puppet/provider/loginctl_user/ruby_spec.rb @@ -27,13 +27,126 @@ end end - it 'enables linger' do - resource = Puppet::Type.type(:loginctl_user).new(common_params) - expect(provider_class).to receive(:loginctl).with('enable-linger', 'foo') - resource.provider.linger = :enabled + context 'when enabling linger' do + let(:resource) { Puppet::Type.type(:loginctl_user).new(common_params) } + let(:provider) { resource.provider } + + it 'enables linger and waits for systemd --user to be ready' do + expect(provider).to receive(:loginctl).with('enable-linger', 'foo') + runtime_path_called = false + allow(provider).to receive(:loginctl).with('show-user', 'foo', '-p', 'RuntimePath') do + runtime_path_called = true + "RuntimePath=/run/user/1000\n" + end + + systemd_private_exists_called = false + allow(File).to receive(:exist?).with('/run/user/1000/systemd/private') do + systemd_private_exists_called = true + true + end + + state_called = false + allow(provider).to receive(:loginctl).with('show-user', 'foo', '-p', 'State') do + state_called = true + "State=active\n" + end + expect(Puppet).to receive(:debug).with('systemd --user for foo is ready') + + provider.linger = :enabled + + expect(runtime_path_called).to be(true) + expect(systemd_private_exists_called).to be(true) + expect(state_called).to be(true) + end + + it 'waits and retries if systemd --user is not immediately ready' do + expect(provider).to receive(:loginctl).with('enable-linger', 'foo') + + runtime_path_calls = 0 + allow(provider).to receive(:loginctl).with('show-user', 'foo', '-p', 'RuntimePath') do + runtime_path_calls += 1 + case runtime_path_calls + when 1 + "RuntimePath=\n" + else + "RuntimePath=/run/user/1000\n" + end + end + + systemd_private_exists_calls = 0 + allow(File).to receive(:exist?).with('/run/user/1000/systemd/private') do + systemd_private_exists_calls += 1 + case systemd_private_exists_calls + when 1 + false + else + true + end + end + + state_calls = 0 + allow(provider).to receive(:loginctl).with('show-user', 'foo', '-p', 'State') do + state_calls += 1 + case state_calls + when 1 + "State=opening\n" + else + "State=lingering\n" + end + end + + expect(Puppet).to receive(:debug).with('systemd --user for foo is ready') + + allow(provider).to receive(:sleep) + + provider.linger = :enabled + + expect(runtime_path_calls).to eq(4) + expect(systemd_private_exists_calls).to eq(3) + expect(state_calls).to eq(2) + end + + it 'handles loginctl failures gracefully while waiting' do + expect(provider).to receive(:loginctl).with('enable-linger', 'foo') + + runtime_path_calls = 0 + allow(provider).to receive(:loginctl).with('show-user', 'foo', '-p', 'RuntimePath') do + runtime_path_calls += 1 + raise(Puppet::ExecutionFailure, 'Failed to get user properties') if runtime_path_calls == 1 + + "RuntimePath=/run/user/1000\n" + end + + allow(File).to receive(:exist?).with('/run/user/1000/systemd/private').and_return(true) + allow(provider).to receive(:loginctl).with('show-user', 'foo', '-p', 'State').and_return("State=active\n") + + expect(Puppet).to receive(:debug).with(%r{Waiting for systemd --user for foo}) + expect(Puppet).to receive(:debug).with('systemd --user for foo is ready') + + allow(provider).to receive(:sleep) + + provider.linger = :enabled + + expect(runtime_path_calls).to eq(2) + end + + it 'logs a warning and continues if timeout is exceeded' do + expect(provider).to receive(:loginctl).with('enable-linger', 'foo') + + # Mock Time to simulate timeout + start_time = Time.now + allow(Time).to receive(:now).and_return(start_time, start_time + 11) + + allow(provider).to receive(:loginctl).with('show-user', 'foo', '-p', 'RuntimePath'). + and_return("RuntimePath=\n") + + expect(Puppet).to receive(:warning).with(%r{Timeout waiting for systemd --user instance for user foo to become ready, continuing anyway}) + + provider.linger = :enabled + end end - it 'disables linger' do + it 'disables linger without waiting' do resource = Puppet::Type.type(:loginctl_user).new(common_params) expect(provider_class).to receive(:loginctl).with('disable-linger', 'foo') resource.provider.linger = :disabled