mixlib-shellout 2.2.7 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: 8d23587c6be044427986ac00a0881aecb540b69a
|
4
|
+
data.tar.gz: dea69938e652a6b531eade231bf3c49d4d2d33ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ccb01c5680a379e58b64d749aa3c444224335395a5ca0f58a46442693fb4b564604eac2357fcda02a19dd4d7e786e3c9a1d1e4ad7720bce00d07ba9aebc171b
|
7
|
+
data.tar.gz: b4993a462a6a05a4adaa2e5db96d7ff4171e07bc07f79cdf2e73e8c8e7fb88b668f8350cb1562a96aa6ee797e0dc49a46453b0f1626560f3b625a7d0c83c511d
|
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
|