Class: PuppetX::Dsc::PowerShellManager

Inherits:
Object
  • Object
show all
Defined in:
lib/puppet_x/puppetlabs/powershell_manager.rb

Constant Summary collapse

@@instances =
{}

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cmd, debug, pipe_timeout) ⇒ PowerShellManager

Returns a new instance of PowerShellManager.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/puppet_x/puppetlabs/powershell_manager.rb', line 41

def initialize(cmd, debug, pipe_timeout)
  @usable = true

  named_pipe_name = "#{SecureRandom.uuid}PuppetPsHost"

  ps_args = ['-File', self.class.init_path, "\"#{named_pipe_name}\""]
  ps_args << '"-EmitDebugOutput"' if debug
  # @stderr should never be written to as PowerShell host redirects output
  stdin, @stdout, @stderr, @ps_process = Open3.popen3("#{cmd} #{ps_args.join(' ')}")
  stdin.close

  Puppet.debug "#{Time.now} #{cmd} is running as pid: #{@ps_process[:pid]}"

  pipe_path = "\\\\.\\pipe\\#{named_pipe_name}"
  # wait for the pipe server to signal ready, and fail if no response in 10 seconds

  # wait up to 30 seconds in 0.2 second intervals to be able to open the pipe
  # If the pipe_timeout is ever specified as less than the sleep interval it will
  # never try to connect to a pipe and error out as if a timeout occurred.
  sleep_interval = 0.2
  (pipe_timeout / sleep_interval).to_int.times do
    begin
      # pipe is opened in binary mode and must always
      @pipe = File.open(pipe_path, 'r+b')
      break
    rescue
      sleep sleep_interval
    end
  end
  if @pipe.nil?
    # Tear down and kill the process if unable to connect to the pipe; failure to do so
    # results in zombie processes being left after the puppet run. We discovered that
    # Closing @ps_process via .kill instead of using this method actually kills the watcher
    # and leaves an orphaned process behind. Failing to close stdout and stderr also leaves
    # clutter behind, so explicitly close those too.
    @stdout.close if !@stdout.closed?
    @stderr.close if !@stderr.closed?
    Process.kill("KILL", @ps_process[:pid]) if @ps_process.alive?
    raise "Failure waiting for PowerShell process #{@ps_process[:pid]} to start pipe server"
  end
  Puppet.debug "#{Time.now} PowerShell initialization complete for pid: #{@ps_process[:pid]}"

  at_exit { exit }
end

Class Method Details

.compatible_version_of_powershell?Boolean

Returns:

  • (Boolean)


31
32
33
# File 'lib/puppet_x/puppetlabs/powershell_manager.rb', line 31

def self.compatible_version_of_powershell?
  @compatible_powershell_version ||= PuppetX::PuppetLabs::Dsc::CompatiblePowerShellVersion.compatible_version?
end

.init_pathObject



133
134
135
136
137
138
# File 'lib/puppet_x/puppetlabs/powershell_manager.rb', line 133

def self.init_path
  # a PowerShell -File compatible path to bootstrap the instance
  path = File.expand_path('../../templates/dsc', __FILE__)
  path = File.join(path, 'init_ps.ps1').gsub('/', '\\')
  "\"#{path}\""
end

.instance(cmd, debug = false, pipe_timeout = 30) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/puppet_x/puppetlabs/powershell_manager.rb', line 12

def self.instance(cmd, debug = false, pipe_timeout = 30)
  key = cmd + debug.to_s
  manager = @@instances[key]

  if manager.nil? || !manager.alive?
    # ignore any errors trying to tear down this unusable instance
    manager.exit if manager rescue nil
    @@instances[key] = PowerShellManager.new(cmd, debug, pipe_timeout)
  end

   @@instances[key]
end

.supported?Boolean

Returns:

  • (Boolean)


35
36
37
38
39
# File 'lib/puppet_x/puppetlabs/powershell_manager.rb', line 35

def self.supported?
  Puppet::Util::Platform.windows? &&
  compatible_version_of_powershell? &&
  !win32console_enabled?
end

.win32console_enabled?Boolean

Returns:

  • (Boolean)


25
26
27
28
29
# File 'lib/puppet_x/puppetlabs/powershell_manager.rb', line 25

def self.win32console_enabled?
  @win32console_enabled ||= defined?(Win32) &&
    defined?(Win32::Console) &&
    Win32::Console.class == Class
end

Instance Method Details

#alive?Boolean

Returns:

  • (Boolean)


86
87
88
89
90
91
92
93
94
95
# File 'lib/puppet_x/puppetlabs/powershell_manager.rb', line 86

def alive?
  # powershell process running
  @ps_process.alive? &&
    # explicitly set during a read / write failure, like broken pipe EPIPE
    @usable &&
    # an explicit failure state might not have been hit, but IO may be closed
    self.class.is_stream_valid?(@pipe) &&
    self.class.is_stream_valid?(@stdout) &&
    self.class.is_stream_valid?(@stderr)
end

#execute(powershell_code, timeout_ms = nil, working_dir = nil) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/puppet_x/puppetlabs/powershell_manager.rb', line 97

def execute(powershell_code, timeout_ms = nil, working_dir = nil)
  code = make_ps_code(powershell_code, timeout_ms, working_dir)


  # err is drained stderr pipe (not captured by redirection inside PS)
  # or during a failure, a Ruby callstack array
  out, native_stdout, err = exec_read_result(code)

  # an error was caught during execution that has invalidated any results
  return { :exitcode => -1, :stderr => err } if !@usable && out.nil?

  out[:exitcode] = out[:exitcode].to_i if !out[:exitcode].nil?
  # if err contains data it must be "real" stderr output
  # which should be appended to what PS has already captured
  out[:stderr] = out[:stderr].nil? ? [] : [out[:stderr]]
  out[:stderr] += err if !err.nil?
  out[:native_stdout] = native_stdout

  out
end

#exitObject



118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/puppet_x/puppetlabs/powershell_manager.rb', line 118

def exit
  @usable = false

  Puppet.debug "PowerShellManager exiting..."
  # pipe may still be open, but if stdout / stderr are dead PS process is in trouble
  # and will block forever on a write to the pipe
  # its safer to close pipe on Ruby side, which gracefully shuts down PS side
  @pipe.close if !@pipe.closed?
  @stdout.close if !@stdout.closed?
  @stderr.close if !@stderr.closed?

  # wait up to 2 seconds for the watcher thread to fully exit
  @ps_process.join(2)
end

#make_ps_code(powershell_code, timeout_ms = nil, working_dir = nil) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/puppet_x/puppetlabs/powershell_manager.rb', line 140

def make_ps_code(powershell_code, timeout_ms = nil, working_dir = nil)
  begin
    timeout_ms = Integer(timeout_ms)
    # Lower bound protection. The polling resolution is only 50ms
    if (timeout_ms < 50) then timeout_ms = 50 end
  rescue
    timeout_ms = 300 * 1000
  end
  # PS side expects Invoke-PowerShellUserCode is always the return value here
  <<-CODE
$params = @{
  Code = @'
#{powershell_code}
'@
  TimeoutMilliseconds = #{timeout_ms}
  WorkingDirectory = "#{working_dir}"
}

Invoke-PowerShellUserCode @params
  CODE
end