mixlib-shellout 2.2.7-universal-mingw32 → 2.3.0-universal-mingw32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +6 -6
- data/README.md +5 -1
- data/Rakefile +10 -18
- data/lib/mixlib/shellout.rb +38 -32
- data/lib/mixlib/shellout/exceptions.rb +5 -3
- data/lib/mixlib/shellout/unix.rb +21 -21
- data/lib/mixlib/shellout/version.rb +1 -1
- data/lib/mixlib/shellout/windows.rb +154 -157
- data/lib/mixlib/shellout/windows/core_ext.rb +166 -127
- data/mixlib-shellout-windows.gemspec +1 -1
- data/mixlib-shellout.gemspec +8 -7
- metadata +18 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a40ea886f43696d31f832a9c84c6d71a381e3657
|
4
|
+
data.tar.gz: ca25e83f24eed9d3271baf9c2e9c79c831a21781
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 185e8d0875b6670c9b85c653262c0f9a082bd47cf0fd455c14a90210e45b229d352cd5b30449d0b4b3a25c6fccd310ca8751d2acb0d7734c6da728a0c9f0d32b
|
7
|
+
data.tar.gz: 998acc35a68b4953a162495d1ed24229b1e361c918b25a255527d9564d269980e52e5eacc1b4999f7d73ab6e9332f5115386c9cff3a702f845f69a109772b7a5
|
data/Gemfile
CHANGED
@@ -1,15 +1,15 @@
|
|
1
|
-
source
|
1
|
+
source "https://rubygems.org"
|
2
2
|
|
3
3
|
gemspec :name => "mixlib-shellout"
|
4
4
|
|
5
5
|
group(:test) do
|
6
6
|
gem "rspec_junit_formatter"
|
7
|
-
gem
|
7
|
+
gem "rake"
|
8
8
|
end
|
9
9
|
|
10
10
|
group(:development) do
|
11
|
-
gem
|
12
|
-
gem
|
13
|
-
gem
|
14
|
-
gem
|
11
|
+
gem "pry"
|
12
|
+
gem "pry-byebug"
|
13
|
+
gem "pry-stack_explorer"
|
14
|
+
gem "rb-readline"
|
15
15
|
end
|
data/README.md
CHANGED
@@ -65,9 +65,13 @@ Invoke "whoami.exe" to demonstrate running a command as another user:
|
|
65
65
|
Mixlib::ShellOut does a standard fork/exec on Unix, and uses the Win32 API on Windows. There is not currently support for JRuby.
|
66
66
|
|
67
67
|
## See Also
|
68
|
-
- `Process.spawn` in Ruby 1.9
|
68
|
+
- `Process.spawn` in Ruby 1.9+
|
69
69
|
- [https://github.com/rtomayko/posix-spawn](https://github.com/rtomayko/posix-spawn)
|
70
70
|
|
71
|
+
## Contributing
|
72
|
+
|
73
|
+
For information on contributing to this project see <https://github.com/chef/chef/blob/master/CONTRIBUTING.md>
|
74
|
+
|
71
75
|
## License
|
72
76
|
- Copyright:: Copyright (c) 2011-2016 Chef Software, Inc.
|
73
77
|
- License:: Apache License, Version 2.0
|
data/Rakefile
CHANGED
@@ -1,24 +1,16 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require 'mixlib/shellout/version'
|
1
|
+
require "bundler"
|
2
|
+
require "rspec/core/rake_task"
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
Bundler::GemHelper.install_tasks name: "mixlib-shellout"
|
5
|
+
|
6
|
+
require "chefstyle"
|
7
|
+
require "rubocop/rake_task"
|
8
|
+
desc "Run Ruby style checks"
|
9
|
+
RuboCop::RakeTask.new(:style)
|
9
10
|
|
10
11
|
desc "Run all specs in spec directory"
|
11
12
|
RSpec::Core::RakeTask.new(:spec) do |t|
|
12
|
-
t.pattern = FileList[
|
13
|
-
end
|
14
|
-
|
15
|
-
desc "Build it and ship it"
|
16
|
-
task ship: [:clobber_package, :gem] do
|
17
|
-
# sh("git tag #{Mixlib::ShellOut::VERSION}")
|
18
|
-
sh("git push opscode --tags")
|
19
|
-
Dir[File.expand_path("../pkg/*.gem", __FILE__)].reverse.each do |built_gem|
|
20
|
-
sh("gem push #{built_gem}")
|
21
|
-
end
|
13
|
+
t.pattern = FileList["spec/**/*_spec.rb"]
|
22
14
|
end
|
23
15
|
|
24
|
-
task default: :spec
|
16
|
+
task default: [:spec, :style]
|
data/lib/mixlib/shellout.rb
CHANGED
@@ -16,10 +16,10 @@
|
|
16
16
|
# limitations under the License.
|
17
17
|
#
|
18
18
|
|
19
|
-
require
|
20
|
-
require
|
21
|
-
require
|
22
|
-
require
|
19
|
+
require "etc"
|
20
|
+
require "tmpdir"
|
21
|
+
require "fcntl"
|
22
|
+
require "mixlib/shellout/exceptions"
|
23
23
|
|
24
24
|
module Mixlib
|
25
25
|
|
@@ -29,10 +29,10 @@ module Mixlib
|
|
29
29
|
DEFAULT_READ_TIMEOUT = 600
|
30
30
|
|
31
31
|
if RUBY_PLATFORM =~ /mswin|mingw32|windows/
|
32
|
-
require
|
32
|
+
require "mixlib/shellout/windows"
|
33
33
|
include ShellOut::Windows
|
34
34
|
else
|
35
|
-
require
|
35
|
+
require "mixlib/shellout/unix"
|
36
36
|
include ShellOut::Unix
|
37
37
|
end
|
38
38
|
|
@@ -109,6 +109,9 @@ module Mixlib
|
|
109
109
|
|
110
110
|
attr_reader :stdin_pipe, :stdout_pipe, :stderr_pipe, :process_status_pipe
|
111
111
|
|
112
|
+
# Runs windows process with elevated privileges. Required for Powershell commands which need elevated privileges
|
113
|
+
attr_accessor :elevated
|
114
|
+
|
112
115
|
# === Arguments:
|
113
116
|
# Takes a single command, or a list of command fragments. These are used
|
114
117
|
# as arguments to Kernel.exec. See the Kernel.exec documentation for more
|
@@ -133,7 +136,7 @@ module Mixlib
|
|
133
136
|
# * +timeout+: a Numeric value for the number of seconds to wait on the
|
134
137
|
# child process before raising an Exception. This is calculated as the
|
135
138
|
# total amount of time that ShellOut waited on the child process without
|
136
|
-
# receiving any output (i.e., IO.select returned nil). Default is
|
139
|
+
# receiving any output (i.e., IO.select returned nil). Default is 600
|
137
140
|
# seconds. Note: the stdlib Timeout library is not used.
|
138
141
|
# * +input+: A String of data to be passed to the subcommand. This is
|
139
142
|
# written to the child process' stdin stream before the process is
|
@@ -162,7 +165,7 @@ module Mixlib
|
|
162
165
|
# cmd = Mixlib::ShellOut.new("apachectl", "start", :user => 'www', :env => nil, :cwd => '/tmp')
|
163
166
|
# cmd.run_command # etc.
|
164
167
|
def initialize(*command_args)
|
165
|
-
@stdout, @stderr, @process_status =
|
168
|
+
@stdout, @stderr, @process_status = "", "", ""
|
166
169
|
@live_stdout = @live_stderr = nil
|
167
170
|
@input = nil
|
168
171
|
@log_level = :debug
|
@@ -172,6 +175,7 @@ module Mixlib
|
|
172
175
|
@valid_exit_codes = [0]
|
173
176
|
@terminate_reason = nil
|
174
177
|
@timeout = nil
|
178
|
+
@elevated = false
|
175
179
|
|
176
180
|
if command_args.last.is_a?(Hash)
|
177
181
|
parse_options(command_args.pop)
|
@@ -212,7 +216,7 @@ module Mixlib
|
|
212
216
|
def gid
|
213
217
|
return group.kind_of?(Integer) ? group : Etc.getgrnam(group.to_s).gid if group
|
214
218
|
return Etc.getpwuid(uid).gid if using_login?
|
215
|
-
|
219
|
+
nil
|
216
220
|
end
|
217
221
|
|
218
222
|
def timeout
|
@@ -253,7 +257,7 @@ module Mixlib
|
|
253
257
|
# within +timeout+ seconds (default: 600s)
|
254
258
|
def run_command
|
255
259
|
if logger
|
256
|
-
log_message = (log_tag.nil? ? "" : "
|
260
|
+
log_message = (log_tag.nil? ? "" : "#{@log_tag} ") << "sh(#{@command})"
|
257
261
|
logger.send(log_level, log_message)
|
258
262
|
end
|
259
263
|
super
|
@@ -284,15 +288,15 @@ module Mixlib
|
|
284
288
|
# is highly encouraged.
|
285
289
|
# === Raises
|
286
290
|
# ShellCommandFailed always
|
287
|
-
def invalid!(msg=nil)
|
291
|
+
def invalid!(msg = nil)
|
288
292
|
msg ||= "Command produced unexpected results"
|
289
293
|
raise ShellCommandFailed, msg + "\n" + format_for_exception
|
290
294
|
end
|
291
295
|
|
292
296
|
def inspect
|
293
|
-
"<#{self.class.name}##{object_id}: command: '
|
294
|
-
|
295
|
-
|
297
|
+
"<#{self.class.name}##{object_id}: command: '#{@command}' process_status: #{@status.inspect} " +
|
298
|
+
"stdout: '#{stdout.strip}' stderr: '#{stderr.strip}' child_pid: #{@child_pid.inspect} " +
|
299
|
+
"environment: #{@environment.inspect} timeout: #{timeout} user: #{@user} group: #{@group} working_dir: #{@cwd} >"
|
296
300
|
end
|
297
301
|
|
298
302
|
private
|
@@ -300,45 +304,47 @@ module Mixlib
|
|
300
304
|
def parse_options(opts)
|
301
305
|
opts.each do |option, setting|
|
302
306
|
case option.to_s
|
303
|
-
when
|
307
|
+
when "cwd"
|
304
308
|
self.cwd = setting
|
305
|
-
when
|
309
|
+
when "domain"
|
306
310
|
self.domain = setting
|
307
|
-
when
|
311
|
+
when "password"
|
308
312
|
self.password = setting
|
309
|
-
when
|
313
|
+
when "user"
|
310
314
|
self.user = setting
|
311
315
|
self.with_logon = setting
|
312
|
-
when
|
316
|
+
when "group"
|
313
317
|
self.group = setting
|
314
|
-
when
|
318
|
+
when "umask"
|
315
319
|
self.umask = setting
|
316
|
-
when
|
320
|
+
when "timeout"
|
317
321
|
self.timeout = setting
|
318
|
-
when
|
322
|
+
when "returns"
|
319
323
|
self.valid_exit_codes = Array(setting)
|
320
|
-
when
|
324
|
+
when "live_stream"
|
321
325
|
self.live_stdout = self.live_stderr = setting
|
322
|
-
when
|
326
|
+
when "live_stdout"
|
323
327
|
self.live_stdout = setting
|
324
|
-
when
|
328
|
+
when "live_stderr"
|
325
329
|
self.live_stderr = setting
|
326
|
-
when
|
330
|
+
when "input"
|
327
331
|
self.input = setting
|
328
|
-
when
|
332
|
+
when "logger"
|
329
333
|
self.logger = setting
|
330
|
-
when
|
334
|
+
when "log_level"
|
331
335
|
self.log_level = setting
|
332
|
-
when
|
336
|
+
when "log_tag"
|
333
337
|
self.log_tag = setting
|
334
|
-
when
|
338
|
+
when "environment", "env"
|
335
339
|
if setting
|
336
|
-
self.environment = Hash[setting.map{|(k,v)| [k.to_s,v]}]
|
340
|
+
self.environment = Hash[setting.map { |(k, v)| [k.to_s, v] }]
|
337
341
|
else
|
338
342
|
self.environment = {}
|
339
343
|
end
|
340
|
-
when
|
344
|
+
when "login"
|
341
345
|
self.login = setting
|
346
|
+
when "elevated"
|
347
|
+
self.elevated = setting
|
342
348
|
else
|
343
349
|
raise InvalidCommandOption, "option '#{option.inspect}' is not a valid option for #{self.class.name}"
|
344
350
|
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
module Mixlib
|
2
2
|
class ShellOut
|
3
|
-
class
|
4
|
-
class
|
5
|
-
class
|
3
|
+
class Error < RuntimeError; end
|
4
|
+
class ShellCommandFailed < Error; end
|
5
|
+
class CommandTimeout < Error; end
|
6
|
+
class InvalidCommandOption < Error; end
|
7
|
+
class EmptyWindowsCommand < Error; end
|
6
8
|
end
|
7
9
|
end
|
data/lib/mixlib/shellout/unix.rb
CHANGED
@@ -27,23 +27,25 @@ module Mixlib
|
|
27
27
|
|
28
28
|
# Option validation that is unix specific
|
29
29
|
def validate_options(opts)
|
30
|
-
|
30
|
+
if opts[:elevated]
|
31
|
+
raise InvalidCommandOption, "Option `elevated` is supported for Powershell commands only"
|
32
|
+
end
|
31
33
|
end
|
32
34
|
|
33
35
|
# Whether we're simulating a login shell
|
34
36
|
def using_login?
|
35
|
-
|
37
|
+
login && user
|
36
38
|
end
|
37
39
|
|
38
40
|
# Helper method for sgids
|
39
41
|
def all_seconderies
|
40
42
|
ret = []
|
41
43
|
Etc.endgrent
|
42
|
-
while ( g = Etc.getgrent )
|
44
|
+
while ( g = Etc.getgrent )
|
43
45
|
ret << g
|
44
46
|
end
|
45
47
|
Etc.endgrent
|
46
|
-
|
48
|
+
ret
|
47
49
|
end
|
48
50
|
|
49
51
|
# The secondary groups that the subprocess will switch to.
|
@@ -52,7 +54,7 @@ module Mixlib
|
|
52
54
|
def sgids
|
53
55
|
return nil unless using_login?
|
54
56
|
user_name = Etc.getpwuid(uid).name
|
55
|
-
all_seconderies.select{|g| g.mem.include?(user_name)}.map{|g|g.gid}
|
57
|
+
all_seconderies.select { |g| g.mem.include?(user_name) }.map { |g| g.gid }
|
56
58
|
end
|
57
59
|
|
58
60
|
# The environment variables that are deduced from simulating logon
|
@@ -63,12 +65,12 @@ module Mixlib
|
|
63
65
|
# According to `man su`, the set fields are:
|
64
66
|
# $HOME, $SHELL, $USER, $LOGNAME, $PATH, and $IFS
|
65
67
|
# Values are copied from "shadow" package in Ubuntu 14.10
|
66
|
-
{
|
68
|
+
{ "HOME" => entry.dir, "SHELL" => entry.shell, "USER" => entry.name, "LOGNAME" => entry.name, "PATH" => "/sbin:/bin:/usr/sbin:/usr/bin", "IFS" => "\t\n" }
|
67
69
|
end
|
68
70
|
|
69
71
|
# Merges the two environments for the process
|
70
72
|
def process_environment
|
71
|
-
logon_environment.merge(
|
73
|
+
logon_environment.merge(environment)
|
72
74
|
end
|
73
75
|
|
74
76
|
# Run the command, writing the command's standard out and standard error
|
@@ -170,7 +172,7 @@ module Mixlib
|
|
170
172
|
|
171
173
|
def set_environment
|
172
174
|
# user-set variables should override the login ones
|
173
|
-
process_environment.each do |env_var,value|
|
175
|
+
process_environment.each do |env_var, value|
|
174
176
|
ENV[env_var] = value
|
175
177
|
end
|
176
178
|
end
|
@@ -335,14 +337,14 @@ module Mixlib
|
|
335
337
|
set_cwd
|
336
338
|
|
337
339
|
begin
|
338
|
-
command.kind_of?(Array) ? exec(*command, :close_others=>true) : exec(command, :close_others=>true)
|
340
|
+
command.kind_of?(Array) ? exec(*command, :close_others => true) : exec(command, :close_others => true)
|
339
341
|
|
340
|
-
raise
|
342
|
+
raise "forty-two" # Should never get here
|
341
343
|
rescue Exception => e
|
342
344
|
Marshal.dump(e, process_status_pipe.last)
|
343
345
|
process_status_pipe.last.flush
|
344
346
|
end
|
345
|
-
process_status_pipe.last.close unless
|
347
|
+
process_status_pipe.last.close unless process_status_pipe.last.closed?
|
346
348
|
exit!
|
347
349
|
end
|
348
350
|
end
|
@@ -351,16 +353,14 @@ module Mixlib
|
|
351
353
|
# If it's there, un-marshal it and raise. If it's not there,
|
352
354
|
# assume everything went well.
|
353
355
|
def propagate_pre_exec_failure
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
open_pipes.delete(child_process_status)
|
363
|
-
end
|
356
|
+
attempt_buffer_read until child_process_status.eof?
|
357
|
+
e = Marshal.load(@process_status)
|
358
|
+
raise(Exception === e ? e : "unknown failure: #{e.inspect}")
|
359
|
+
rescue ArgumentError # If we get an ArgumentError error, then the exec was successful
|
360
|
+
true
|
361
|
+
ensure
|
362
|
+
child_process_status.close
|
363
|
+
open_pipes.delete(child_process_status)
|
364
364
|
end
|
365
365
|
|
366
366
|
def reap_errant_child
|
@@ -18,8 +18,8 @@
|
|
18
18
|
# limitations under the License.
|
19
19
|
#
|
20
20
|
|
21
|
-
require
|
22
|
-
require
|
21
|
+
require "win32/process"
|
22
|
+
require "mixlib/shellout/windows/core_ext"
|
23
23
|
|
24
24
|
module Mixlib
|
25
25
|
class ShellOut
|
@@ -32,10 +32,12 @@ module Mixlib
|
|
32
32
|
|
33
33
|
# Option validation that is windows specific
|
34
34
|
def validate_options(opts)
|
35
|
-
if opts[:user]
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
if opts[:user] && !opts[:password]
|
36
|
+
raise InvalidCommandOption, "You must supply a password when supplying a user in windows"
|
37
|
+
end
|
38
|
+
|
39
|
+
if opts[:elevated] && opts[:elevated] != true && opts[:elevated] != false
|
40
|
+
raise InvalidCommandOption, "Invalid value passed for `elevated`. Please provide true/false."
|
39
41
|
end
|
40
42
|
end
|
41
43
|
|
@@ -56,29 +58,30 @@ module Mixlib
|
|
56
58
|
#
|
57
59
|
# Set cwd, environment, appname, etc.
|
58
60
|
#
|
59
|
-
app_name, command_line = command_to_run(
|
61
|
+
app_name, command_line = command_to_run(command)
|
60
62
|
create_process_args = {
|
61
63
|
:app_name => app_name,
|
62
64
|
:command_line => command_line,
|
63
65
|
:startup_info => {
|
64
66
|
:stdout => stdout_write,
|
65
67
|
:stderr => stderr_write,
|
66
|
-
:stdin => stdin_read
|
68
|
+
:stdin => stdin_read,
|
67
69
|
},
|
68
|
-
:environment => inherit_environment.map { |k,v| "#{k}=#{v}" },
|
69
|
-
:close_handles => false
|
70
|
+
:environment => inherit_environment.map { |k, v| "#{k}=#{v}" },
|
71
|
+
:close_handles => false,
|
70
72
|
}
|
71
73
|
create_process_args[:cwd] = cwd if cwd
|
72
74
|
# default to local account database if domain is not specified
|
73
75
|
create_process_args[:domain] = domain.nil? ? "." : domain
|
74
76
|
create_process_args[:with_logon] = with_logon if with_logon
|
75
77
|
create_process_args[:password] = password if password
|
78
|
+
create_process_args[:elevated] = elevated if elevated
|
76
79
|
|
77
80
|
#
|
78
81
|
# Start the process
|
79
82
|
#
|
80
83
|
process = Process.create(create_process_args)
|
81
|
-
logger.debug(
|
84
|
+
logger.debug(format_process(process, app_name, command_line, timeout)) if logger
|
82
85
|
begin
|
83
86
|
# Start pushing data into input
|
84
87
|
stdin_write << input if input
|
@@ -90,26 +93,26 @@ module Mixlib
|
|
90
93
|
# Wait for the process to finish, consuming output as we go
|
91
94
|
#
|
92
95
|
start_wait = Time.now
|
93
|
-
|
96
|
+
loop do
|
94
97
|
wait_status = WaitForSingleObject(process.process_handle, 0)
|
95
98
|
case wait_status
|
96
99
|
when WAIT_OBJECT_0
|
97
100
|
# Get process exit code
|
98
|
-
exit_code = [0].pack(
|
101
|
+
exit_code = [0].pack("l")
|
99
102
|
unless GetExitCodeProcess(process.process_handle, exit_code)
|
100
103
|
raise get_last_error
|
101
104
|
end
|
102
105
|
@status = ThingThatLooksSortOfLikeAProcessStatus.new
|
103
|
-
@status.exitstatus = exit_code.unpack(
|
106
|
+
@status.exitstatus = exit_code.unpack("l").first
|
104
107
|
|
105
108
|
return self
|
106
109
|
when WAIT_TIMEOUT
|
107
110
|
# Kill the process
|
108
111
|
if (Time.now - start_wait) > timeout
|
109
112
|
begin
|
110
|
-
require
|
113
|
+
require "wmi-lite/wmi"
|
111
114
|
wmi = WmiLite::Wmi.new
|
112
|
-
|
115
|
+
kill_process_tree(process.process_id, wmi, logger)
|
113
116
|
Process.kill(:KILL, process.process_id)
|
114
117
|
rescue Errno::EIO, SystemCallError
|
115
118
|
logger.warn("Failed to kill timed out process #{process.process_id}") if logger
|
@@ -118,13 +121,13 @@ module Mixlib
|
|
118
121
|
raise Mixlib::ShellOut::CommandTimeout, [
|
119
122
|
"command timed out:",
|
120
123
|
format_for_exception,
|
121
|
-
|
124
|
+
format_process(process, app_name, command_line, timeout),
|
122
125
|
].join("\n")
|
123
126
|
end
|
124
127
|
|
125
128
|
consume_output(open_streams, stdout_read, stderr_read)
|
126
129
|
else
|
127
|
-
raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout*1000}): #{wait_status}"
|
130
|
+
raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout * 1000}): #{wait_status}"
|
128
131
|
end
|
129
132
|
|
130
133
|
end
|
@@ -146,8 +149,6 @@ module Mixlib
|
|
146
149
|
end
|
147
150
|
end
|
148
151
|
|
149
|
-
private
|
150
|
-
|
151
152
|
class ThingThatLooksSortOfLikeAProcessStatus
|
152
153
|
attr_accessor :exitstatus
|
153
154
|
def success?
|
@@ -155,6 +156,8 @@ module Mixlib
|
|
155
156
|
end
|
156
157
|
end
|
157
158
|
|
159
|
+
private
|
160
|
+
|
158
161
|
def consume_output(open_streams, stdout_read, stderr_read)
|
159
162
|
return false if open_streams.length == 0
|
160
163
|
ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME)
|
@@ -182,65 +185,59 @@ module Mixlib
|
|
182
185
|
end
|
183
186
|
end
|
184
187
|
|
185
|
-
|
188
|
+
true
|
186
189
|
end
|
187
190
|
|
188
|
-
IS_BATCH_FILE = /\.bat"?$|\.cmd"?$/i
|
189
|
-
|
190
191
|
def command_to_run(command)
|
191
|
-
return
|
192
|
+
return run_under_cmd(command) if should_run_under_cmd?(command)
|
192
193
|
|
193
194
|
candidate = candidate_executable_for_command(command)
|
194
195
|
|
195
|
-
|
196
|
-
|
196
|
+
if candidate.length == 0
|
197
|
+
raise Mixlib::ShellOut::EmptyWindowsCommand, "could not parse script/executable out of command: `#{command}`"
|
198
|
+
end
|
197
199
|
|
198
200
|
# Check if the exe exists directly. Otherwise, search PATH.
|
199
|
-
exe =
|
200
|
-
|
201
|
-
|
202
|
-
# Batch files MUST use cmd; and if we couldn't find the command we're looking for,
|
203
|
-
# we assume it must be a cmd builtin.
|
204
|
-
if exe.nil? || exe =~ IS_BATCH_FILE
|
205
|
-
_run_under_cmd(command)
|
201
|
+
exe = which(candidate)
|
202
|
+
if exe_needs_cmd?(exe)
|
203
|
+
run_under_cmd(command)
|
206
204
|
else
|
207
|
-
|
205
|
+
[ exe, command ]
|
208
206
|
end
|
209
207
|
end
|
210
208
|
|
209
|
+
# Batch files MUST use cmd; and if we couldn't find the command we're looking for,
|
210
|
+
# we assume it must be a cmd builtin.
|
211
|
+
def exe_needs_cmd?(exe)
|
212
|
+
!exe || exe =~ /\.bat"?$|\.cmd"?$/i
|
213
|
+
end
|
214
|
+
|
211
215
|
# cmd does not parse multiple quotes well unless the whole thing is wrapped up in quotes.
|
212
216
|
# https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4837859
|
213
217
|
# http://ss64.com/nt/syntax-esc.html
|
214
|
-
def
|
215
|
-
[ ENV[
|
216
|
-
end
|
217
|
-
|
218
|
-
def _run_directly(command, exe)
|
219
|
-
[ exe, command ]
|
220
|
-
end
|
221
|
-
|
222
|
-
def unquoted_executable_path(command)
|
223
|
-
command[0,command.index(/\s/) || command.length]
|
218
|
+
def run_under_cmd(command)
|
219
|
+
[ ENV["COMSPEC"], "cmd /c \"#{command}\"" ]
|
224
220
|
end
|
225
221
|
|
222
|
+
# FIXME: this extracts ARGV[0] but is it correct?
|
226
223
|
def candidate_executable_for_command(command)
|
227
224
|
if command =~ /^\s*"(.*?)"/
|
228
225
|
# If we have quotes, do an exact match
|
229
226
|
$1
|
230
227
|
else
|
231
228
|
# Otherwise check everything up to the first space
|
232
|
-
|
229
|
+
command[0, command.index(/\s/) || command.length].strip
|
233
230
|
end
|
234
231
|
end
|
235
232
|
|
236
233
|
def inherit_environment
|
237
234
|
result = {}
|
238
|
-
ENV.each_pair do |k,v|
|
235
|
+
ENV.each_pair do |k, v|
|
239
236
|
result[k] = v
|
240
237
|
end
|
241
238
|
|
242
|
-
environment.each_pair do |k,v|
|
243
|
-
if v
|
239
|
+
environment.each_pair do |k, v|
|
240
|
+
if v.nil?
|
244
241
|
result.delete(k)
|
245
242
|
else
|
246
243
|
result[k] = v
|
@@ -249,134 +246,134 @@ module Mixlib
|
|
249
246
|
result
|
250
247
|
end
|
251
248
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
quote = nil
|
273
|
-
end
|
274
|
-
next
|
275
|
-
when '>', '<', '|', '&', "\n"
|
276
|
-
return true unless quote
|
277
|
-
when '%'
|
278
|
-
return true if env
|
279
|
-
env = env_first_char = true
|
280
|
-
next
|
281
|
-
else
|
282
|
-
next unless env
|
283
|
-
if env_first_char
|
284
|
-
env_first_char = false
|
285
|
-
env = false and next if c !~ /[A-Za-z_]/
|
286
|
-
end
|
287
|
-
env = false if c !~ /[A-Za-z1-9_]/
|
249
|
+
# api: semi-private
|
250
|
+
# If there are special characters parsable by cmd.exe (such as file redirection), then
|
251
|
+
# this method should return true.
|
252
|
+
#
|
253
|
+
# This parser is based on
|
254
|
+
# https://github.com/ruby/ruby/blob/9073db5cb1d3173aff62be5b48d00f0fb2890991/win32/win32.c#L1437
|
255
|
+
def should_run_under_cmd?(command)
|
256
|
+
return true if command =~ /^@/
|
257
|
+
|
258
|
+
quote = nil
|
259
|
+
env = false
|
260
|
+
env_first_char = false
|
261
|
+
|
262
|
+
command.dup.each_char do |c|
|
263
|
+
case c
|
264
|
+
when "'", '"'
|
265
|
+
if !quote
|
266
|
+
quote = c
|
267
|
+
elsif quote == c
|
268
|
+
quote = nil
|
288
269
|
end
|
270
|
+
next
|
271
|
+
when ">", "<", "|", "&", "\n"
|
272
|
+
return true unless quote
|
273
|
+
when "%"
|
274
|
+
return true if env
|
275
|
+
env = env_first_char = true
|
276
|
+
next
|
277
|
+
else
|
278
|
+
next unless env
|
279
|
+
if env_first_char
|
280
|
+
env_first_char = false
|
281
|
+
(env = false) && next if c !~ /[A-Za-z_]/
|
282
|
+
end
|
283
|
+
env = false if c !~ /[A-Za-z1-9_]/
|
289
284
|
end
|
290
|
-
return false
|
291
|
-
end
|
292
|
-
|
293
|
-
def self.pathext
|
294
|
-
@pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : ['']
|
295
285
|
end
|
286
|
+
false
|
287
|
+
end
|
296
288
|
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
return nil
|
289
|
+
# FIXME: reduce code duplication with chef/chef
|
290
|
+
def which(cmd)
|
291
|
+
exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") + [""] : [""]
|
292
|
+
# windows always searches '.' first
|
293
|
+
exts.each do |ext|
|
294
|
+
filename = "#{cmd}#{ext}"
|
295
|
+
return filename if File.executable?(filename) && !File.directory?(filename)
|
305
296
|
end
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
exe = "#{path}#{ext}"
|
315
|
-
return exe if executable? exe
|
297
|
+
# only search through the path if the Filename does not contain separators
|
298
|
+
if File.basename(cmd) == cmd
|
299
|
+
paths = ENV["PATH"].split(File::PATH_SEPARATOR)
|
300
|
+
paths.each do |path|
|
301
|
+
exts.each do |ext|
|
302
|
+
filename = File.join(path, "#{cmd}#{ext}")
|
303
|
+
return filename if File.executable?(filename) && !File.directory?(filename)
|
304
|
+
end
|
316
305
|
end
|
317
|
-
return nil
|
318
|
-
end
|
319
|
-
|
320
|
-
def self.executable?(path)
|
321
|
-
File.executable?(path) && !File.directory?(path)
|
322
306
|
end
|
307
|
+
false
|
308
|
+
end
|
323
309
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
310
|
+
def system_required_processes
|
311
|
+
[
|
312
|
+
"System Idle Process",
|
313
|
+
"System",
|
314
|
+
"spoolsv.exe",
|
315
|
+
"lsass.exe",
|
316
|
+
"csrss.exe",
|
317
|
+
"smss.exe",
|
318
|
+
"svchost.exe",
|
319
|
+
]
|
320
|
+
end
|
335
321
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
322
|
+
def unsafe_process?(name, logger)
|
323
|
+
return false unless system_required_processes.include? name
|
324
|
+
logger.debug(
|
325
|
+
"A request to kill a critical system process - #{name} - was received and skipped."
|
326
|
+
)
|
327
|
+
true
|
328
|
+
end
|
343
329
|
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
end
|
330
|
+
# recursively kills all child processes of given pid
|
331
|
+
# calls itself querying for children child procs until
|
332
|
+
# none remain. Important that a single WmiLite instance
|
333
|
+
# is passed in since each creates its own WMI rpc process
|
334
|
+
def kill_process_tree(pid, wmi, logger)
|
335
|
+
wmi.query("select * from Win32_Process where ParentProcessID=#{pid}").each do |instance|
|
336
|
+
next if unsafe_process?(instance.wmi_ole_object.name, logger)
|
337
|
+
child_pid = instance.wmi_ole_object.processid
|
338
|
+
kill_process_tree(child_pid, wmi, logger)
|
339
|
+
kill_process(instance, logger)
|
355
340
|
end
|
341
|
+
end
|
356
342
|
|
357
|
-
|
358
|
-
|
343
|
+
def kill_process(instance, logger)
|
344
|
+
child_pid = instance.wmi_ole_object.processid
|
345
|
+
if logger
|
359
346
|
logger.debug([
|
360
347
|
"killing child process #{child_pid}::",
|
361
|
-
"#{instance.wmi_ole_object.Name} of parent #{pid}"
|
362
|
-
|
363
|
-
|
364
|
-
|
348
|
+
"#{instance.wmi_ole_object.Name} of parent #{pid}",
|
349
|
+
].join)
|
350
|
+
end
|
351
|
+
Process.kill(:KILL, instance.wmi_ole_object.processid)
|
352
|
+
rescue Errno::EIO, SystemCallError
|
353
|
+
if logger
|
365
354
|
logger.debug([
|
366
355
|
"Failed to kill child process #{child_pid}::",
|
367
|
-
"#{instance.wmi_ole_object.Name} of parent #{pid}"
|
368
|
-
].join)
|
356
|
+
"#{instance.wmi_ole_object.Name} of parent #{pid}",
|
357
|
+
].join)
|
369
358
|
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def format_process(process, app_name, command_line, timeout)
|
362
|
+
msg = []
|
363
|
+
msg << "ProcessId: #{process.process_id}"
|
364
|
+
msg << "app_name: #{app_name}"
|
365
|
+
msg << "command_line: #{command_line}"
|
366
|
+
msg << "timeout: #{timeout}"
|
367
|
+
msg.join("\n")
|
368
|
+
end
|
370
369
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
msg << "timeout: #{timeout}"
|
377
|
-
msg.join("\n")
|
370
|
+
# DEPRECATED do not use
|
371
|
+
class Utils
|
372
|
+
include Mixlib::ShellOut::Windows
|
373
|
+
def self.should_run_under_cmd?(cmd)
|
374
|
+
Mixlib::ShellOut::Windows::Utils.new.send(:should_run_under_cmd?, cmd)
|
378
375
|
end
|
379
376
|
end
|
380
|
-
end
|
377
|
+
end
|
381
378
|
end
|
382
379
|
end
|