WinRb / WinRM

Ruby library for Windows Remote Management
Apache License 2.0
412 stars 117 forks source link

[HELP] Running commands asynchronously aka background jobs #296

Closed djberg96 closed 5 years ago

djberg96 commented 5 years ago

I'm trying to figure out how to do some async + poll using winrm. My idea was to create a wrapper that would wrap everything in a Start-Job -ScriptBlock { ... }, and then immediately return control. I would then periodically poll the job via Wait-Job in a separate call, and finally Receive-Job when the job has finished (presumably successfully).

However, I'm having trouble getting that approach to work, as the API seems to wait for the job to complete before returning. Am I doing something wrong? Has this workflow already been simplified in some way? Is there a better way?

mwrock commented 5 years ago

I'd think you would open a shell and then run the start-job command and capture the job ID. That should return immediately. You just want to make sure to call wait-job in a separate run call in the same shell.

djberg96 commented 5 years ago

@mwrock Hm, upon further review, it doesn't seem to return right away. But, it always returns a job ID of "1", which can't be right, and trying to Get-Job on Id 1 causes an error, as it's not found. I also tried using a custom name, but I see the same thing - always a job ID of 1. I'm basically just doing this:

connection.shell(:powershell) do |shell|
  command = "Start-Job -Name dberger -ScriptBlock { sleep 30; Get-Process -Name system }"
  output = shell.run(command)

  if output.stderr && output.stderr.size > 0
    puts "Failed: #{output.stderr}"
  else
    puts output.stdout
  end
end

What am I doing wrong here?

djberg96 commented 5 years ago

Ah, I think the issue is that the Job ID is for the current shell, and since a new shell is always created and closed, I'll never be able to get at the job with a separate session. Is there a way to persist the shell?

mwrock commented 5 years ago

yes! Just assign the shell to a variable instead of running in a do block:

shell = connection.shell(:powershell)
output = shell.run(command)
# do a whole bunch of super great stuff
shell.close
djberg96 commented 5 years ago

Aha, thanks! For anyone following, this was what I did (includes some extra debug prints):

require 'winrm'
require 'json'

host = "1.2.3.4"
port = "5985"

options = {
  :endpoint     => "http://#{host}:#{port}/wsman",
  :user         => "dberger",
  :password     => "xxx",
  :disable_sspi => true
}

connection = WinRM::Connection.new(options)

begin
  shell = connection.shell(:powershell)

  command = "Start-Job -ScriptBlock { sleep 30; Get-Process -Name system } | Select -ExpandProperty Id"
  puts "FIRST COMMAND: #{command}"

  output = shell.run(command)

  job_id = if output.stderr && output.stderr.size > 0
    raise "Failed: #{output.stderr}"
  else
    output.stdout.strip
  end

  puts "Job ID: #{job_id}"

  state    = "Running"
  interval = [shell.connection_opts[:operation_timeout], 10].min

  command  = "Get-Job -Id #{job_id} | Select -ExpandProperty State"

  while !state.casecmp('completed').zero?
    puts "COMMAND: #{command}"
    state = shell.run(command).stdout.strip.downcase
    puts "STATE: #{state}"
    raise "OOPS!" if state == 'failed'
    sleep(interval)
  end

  command = "Receive-Job -Id #{job_id} | ConvertTo-Json -Depth 1"
  output = shell.run(command).stdout

  p JSON.parse(output)
ensure
  shell.close if shell
end