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
@@ -17,14 +17,15 @@
|
|
17
17
|
# limitations under the License.
|
18
18
|
#
|
19
19
|
|
20
|
-
require
|
20
|
+
require "win32/process"
|
21
21
|
|
22
22
|
# Add new constants for Logon
|
23
23
|
module Process::Constants
|
24
24
|
private
|
25
25
|
|
26
26
|
LOGON32_LOGON_INTERACTIVE = 0x00000002
|
27
|
-
|
27
|
+
LOGON32_LOGON_BATCH = 0x00000004
|
28
|
+
LOGON32_PROVIDER_DEFAULT = 0x00000000
|
28
29
|
UOI_NAME = 0x00000002
|
29
30
|
|
30
31
|
WAIT_OBJECT_0 = 0
|
@@ -32,6 +33,9 @@ module Process::Constants
|
|
32
33
|
WAIT_ABANDONED = 128
|
33
34
|
WAIT_ABANDONED_0 = WAIT_ABANDONED
|
34
35
|
WAIT_FAILED = 0xFFFFFFFF
|
36
|
+
|
37
|
+
ERROR_PRIVILEGE_NOT_HELD = 1314
|
38
|
+
ERROR_LOGON_TYPE_NOT_GRANTED = 0x569
|
35
39
|
end
|
36
40
|
|
37
41
|
# Define the functions needed to check with Service windows station
|
@@ -65,78 +69,78 @@ module Process
|
|
65
69
|
class << self
|
66
70
|
def create(args)
|
67
71
|
unless args.kind_of?(Hash)
|
68
|
-
raise TypeError,
|
72
|
+
raise TypeError, "hash keyword arguments expected"
|
69
73
|
end
|
70
74
|
|
71
|
-
valid_keys = %w
|
75
|
+
valid_keys = %w{
|
72
76
|
app_name command_line inherit creation_flags cwd environment
|
73
77
|
startup_info thread_inherit process_inherit close_handles with_logon
|
74
|
-
domain password
|
75
|
-
|
78
|
+
domain password elevated
|
79
|
+
}
|
76
80
|
|
77
|
-
valid_si_keys = %w
|
81
|
+
valid_si_keys = %w{
|
78
82
|
startf_flags desktop title x y x_size y_size x_count_chars
|
79
83
|
y_count_chars fill_attribute sw_flags stdin stdout stderr
|
80
|
-
|
84
|
+
}
|
81
85
|
|
82
86
|
# Set default values
|
83
87
|
hash = {
|
84
|
-
|
85
|
-
|
86
|
-
|
88
|
+
"app_name" => nil,
|
89
|
+
"creation_flags" => 0,
|
90
|
+
"close_handles" => true,
|
87
91
|
}
|
88
92
|
|
89
93
|
# Validate the keys, and convert symbols and case to lowercase strings.
|
90
|
-
args.each
|
94
|
+
args.each do |key, val|
|
91
95
|
key = key.to_s.downcase
|
92
96
|
unless valid_keys.include?(key)
|
93
97
|
raise ArgumentError, "invalid key '#{key}'"
|
94
98
|
end
|
95
99
|
hash[key] = val
|
96
|
-
|
100
|
+
end
|
97
101
|
|
98
102
|
si_hash = {}
|
99
103
|
|
100
104
|
# If the startup_info key is present, validate its subkeys
|
101
|
-
if hash[
|
102
|
-
hash[
|
105
|
+
if hash["startup_info"]
|
106
|
+
hash["startup_info"].each do |key, val|
|
103
107
|
key = key.to_s.downcase
|
104
108
|
unless valid_si_keys.include?(key)
|
105
109
|
raise ArgumentError, "invalid startup_info key '#{key}'"
|
106
110
|
end
|
107
111
|
si_hash[key] = val
|
108
|
-
|
112
|
+
end
|
109
113
|
end
|
110
114
|
|
111
115
|
# The +command_line+ key is mandatory unless the +app_name+ key
|
112
116
|
# is specified.
|
113
|
-
unless hash[
|
114
|
-
if hash[
|
115
|
-
hash[
|
116
|
-
hash[
|
117
|
+
unless hash["command_line"]
|
118
|
+
if hash["app_name"]
|
119
|
+
hash["command_line"] = hash["app_name"]
|
120
|
+
hash["app_name"] = nil
|
117
121
|
else
|
118
|
-
raise ArgumentError,
|
122
|
+
raise ArgumentError, "command_line or app_name must be specified"
|
119
123
|
end
|
120
124
|
end
|
121
125
|
|
122
126
|
env = nil
|
123
127
|
|
124
128
|
# The env string should be passed as a string of ';' separated paths.
|
125
|
-
if hash[
|
126
|
-
env = hash[
|
129
|
+
if hash["environment"]
|
130
|
+
env = hash["environment"]
|
127
131
|
|
128
132
|
unless env.respond_to?(:join)
|
129
|
-
env = hash[
|
133
|
+
env = hash["environment"].split(File::PATH_SEPARATOR)
|
130
134
|
end
|
131
135
|
|
132
|
-
env = env.map{ |e| e + 0.chr }.join(
|
133
|
-
env.to_wide_string! if hash[
|
136
|
+
env = env.map { |e| e + 0.chr }.join("") + 0.chr
|
137
|
+
env.to_wide_string! if hash["with_logon"]
|
134
138
|
end
|
135
139
|
|
136
140
|
# Process SECURITY_ATTRIBUTE structure
|
137
141
|
process_security = nil
|
138
142
|
|
139
|
-
if hash[
|
143
|
+
if hash["process_inherit"]
|
140
144
|
process_security = SECURITY_ATTRIBUTES.new
|
141
145
|
process_security[:nLength] = 12
|
142
146
|
process_security[:bInheritHandle] = 1
|
@@ -145,7 +149,7 @@ module Process
|
|
145
149
|
# Thread SECURITY_ATTRIBUTE structure
|
146
150
|
thread_security = nil
|
147
151
|
|
148
|
-
if hash[
|
152
|
+
if hash["thread_inherit"]
|
149
153
|
thread_security = SECURITY_ATTRIBUTES.new
|
150
154
|
thread_security[:nLength] = 12
|
151
155
|
thread_security[:bInheritHandle] = 1
|
@@ -156,7 +160,7 @@ module Process
|
|
156
160
|
# will not work on JRuby because of the way it handles internal file
|
157
161
|
# descriptors.
|
158
162
|
#
|
159
|
-
|
163
|
+
%w{stdin stdout stderr}.each do |io|
|
160
164
|
if si_hash[io]
|
161
165
|
if si_hash[io].respond_to?(:fileno)
|
162
166
|
handle = get_osfhandle(si_hash[io].fileno)
|
@@ -187,129 +191,79 @@ module Process
|
|
187
191
|
raise SystemCallError.new("SetHandleInformation", FFI.errno) unless bool
|
188
192
|
|
189
193
|
si_hash[io] = handle
|
190
|
-
si_hash[
|
191
|
-
si_hash[
|
192
|
-
hash[
|
194
|
+
si_hash["startf_flags"] ||= 0
|
195
|
+
si_hash["startf_flags"] |= STARTF_USESTDHANDLES
|
196
|
+
hash["inherit"] = true
|
193
197
|
end
|
194
|
-
|
198
|
+
end
|
195
199
|
|
196
200
|
procinfo = PROCESS_INFORMATION.new
|
197
201
|
startinfo = STARTUPINFO.new
|
198
202
|
|
199
203
|
unless si_hash.empty?
|
200
204
|
startinfo[:cb] = startinfo.size
|
201
|
-
startinfo[:lpDesktop] = si_hash[
|
202
|
-
startinfo[:lpTitle] = si_hash[
|
203
|
-
startinfo[:dwX] = si_hash[
|
204
|
-
startinfo[:dwY] = si_hash[
|
205
|
-
startinfo[:dwXSize] = si_hash[
|
206
|
-
startinfo[:dwYSize] = si_hash[
|
207
|
-
startinfo[:dwXCountChars] = si_hash[
|
208
|
-
startinfo[:dwYCountChars] = si_hash[
|
209
|
-
startinfo[:dwFillAttribute] = si_hash[
|
210
|
-
startinfo[:dwFlags] = si_hash[
|
211
|
-
startinfo[:wShowWindow] = si_hash[
|
205
|
+
startinfo[:lpDesktop] = si_hash["desktop"] if si_hash["desktop"]
|
206
|
+
startinfo[:lpTitle] = si_hash["title"] if si_hash["title"]
|
207
|
+
startinfo[:dwX] = si_hash["x"] if si_hash["x"]
|
208
|
+
startinfo[:dwY] = si_hash["y"] if si_hash["y"]
|
209
|
+
startinfo[:dwXSize] = si_hash["x_size"] if si_hash["x_size"]
|
210
|
+
startinfo[:dwYSize] = si_hash["y_size"] if si_hash["y_size"]
|
211
|
+
startinfo[:dwXCountChars] = si_hash["x_count_chars"] if si_hash["x_count_chars"]
|
212
|
+
startinfo[:dwYCountChars] = si_hash["y_count_chars"] if si_hash["y_count_chars"]
|
213
|
+
startinfo[:dwFillAttribute] = si_hash["fill_attribute"] if si_hash["fill_attribute"]
|
214
|
+
startinfo[:dwFlags] = si_hash["startf_flags"] if si_hash["startf_flags"]
|
215
|
+
startinfo[:wShowWindow] = si_hash["sw_flags"] if si_hash["sw_flags"]
|
212
216
|
startinfo[:cbReserved2] = 0
|
213
|
-
startinfo[:hStdInput] = si_hash[
|
214
|
-
startinfo[:hStdOutput] = si_hash[
|
215
|
-
startinfo[:hStdError] = si_hash[
|
217
|
+
startinfo[:hStdInput] = si_hash["stdin"] if si_hash["stdin"]
|
218
|
+
startinfo[:hStdOutput] = si_hash["stdout"] if si_hash["stdout"]
|
219
|
+
startinfo[:hStdError] = si_hash["stderr"] if si_hash["stderr"]
|
216
220
|
end
|
217
221
|
|
218
222
|
app = nil
|
219
223
|
cmd = nil
|
220
224
|
|
221
225
|
# Convert strings to wide character strings if present
|
222
|
-
if hash[
|
223
|
-
app = hash[
|
226
|
+
if hash["app_name"]
|
227
|
+
app = hash["app_name"].to_wide_string
|
224
228
|
end
|
225
229
|
|
226
|
-
if hash[
|
227
|
-
cmd = hash[
|
230
|
+
if hash["command_line"]
|
231
|
+
cmd = hash["command_line"].to_wide_string
|
228
232
|
end
|
229
233
|
|
230
|
-
if hash[
|
231
|
-
cwd = hash[
|
234
|
+
if hash["cwd"]
|
235
|
+
cwd = hash["cwd"].to_wide_string
|
232
236
|
end
|
233
237
|
|
234
|
-
inherit = hash[
|
235
|
-
|
236
|
-
if hash['with_logon']
|
237
|
-
logon = hash['with_logon'].to_wide_string
|
238
|
-
|
239
|
-
if hash['password']
|
240
|
-
passwd = hash['password'].to_wide_string
|
241
|
-
else
|
242
|
-
raise ArgumentError, 'password must be specified if with_logon is used'
|
243
|
-
end
|
244
|
-
|
245
|
-
if hash['domain']
|
246
|
-
domain = hash['domain'].to_wide_string
|
247
|
-
end
|
248
|
-
|
249
|
-
hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT
|
238
|
+
inherit = hash["inherit"] ? 1 : 0
|
250
239
|
|
251
|
-
|
252
|
-
|
240
|
+
if hash["with_logon"]
|
241
|
+
logon, passwd, domain = format_creds_from_hash(hash)
|
253
242
|
|
254
|
-
|
255
|
-
GetProcessWindowStation(), # Window station handle
|
256
|
-
UOI_NAME, # Information to get
|
257
|
-
winsta_name, # Buffer to receive information
|
258
|
-
winsta_name.size, # Size of buffer
|
259
|
-
return_size # Size filled into buffer
|
260
|
-
)
|
261
|
-
|
262
|
-
unless bool
|
263
|
-
raise SystemCallError.new("GetUserObjectInformationA", FFI.errno)
|
264
|
-
end
|
243
|
+
hash["creation_flags"] |= CREATE_UNICODE_ENVIRONMENT
|
265
244
|
|
266
|
-
winsta_name =
|
245
|
+
winsta_name = get_windows_station_name
|
267
246
|
|
268
247
|
# If running in the service windows station must do a log on to get
|
269
|
-
# to the interactive desktop.
|
248
|
+
# to the interactive desktop. The running process user account must have
|
270
249
|
# the 'Replace a process level token' permission. This is necessary as
|
271
250
|
# the logon (which happens with CreateProcessWithLogon) must have an
|
272
251
|
# interactive windows station to attach to, which is created with the
|
273
|
-
# LogonUser
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
end
|
289
|
-
|
290
|
-
token = token.read_ulong
|
291
|
-
|
292
|
-
begin
|
293
|
-
bool = CreateProcessAsUserW(
|
294
|
-
token, # User token handle
|
295
|
-
app, # App name
|
296
|
-
cmd, # Command line
|
297
|
-
process_security, # Process attributes
|
298
|
-
thread_security, # Thread attributes
|
299
|
-
inherit, # Inherit handles
|
300
|
-
hash['creation_flags'], # Creation Flags
|
301
|
-
env, # Environment
|
302
|
-
cwd, # Working directory
|
303
|
-
startinfo, # Startup Info
|
304
|
-
procinfo # Process Info
|
305
|
-
)
|
306
|
-
ensure
|
307
|
-
CloseHandle(token)
|
308
|
-
end
|
309
|
-
|
310
|
-
unless bool
|
311
|
-
raise SystemCallError.new("CreateProcessAsUserW (You must hold the 'Replace a process level token' permission)", FFI.errno)
|
312
|
-
end
|
252
|
+
# LogonUser call with the LOGON32_LOGON_INTERACTIVE flag.
|
253
|
+
#
|
254
|
+
# User Access Control (UAC) only applies to interactive logons, so we
|
255
|
+
# can simulate running a command 'elevated' by running it under a separate
|
256
|
+
# logon as a batch process.
|
257
|
+
if hash["elevated"] || winsta_name =~ /^Service-0x0-.*$/i
|
258
|
+
logon_type = if hash["elevated"]
|
259
|
+
LOGON32_LOGON_BATCH
|
260
|
+
else
|
261
|
+
LOGON32_LOGON_INTERACTIVE
|
262
|
+
end
|
263
|
+
|
264
|
+
token = logon_user(logon, domain, passwd, logon_type)
|
265
|
+
|
266
|
+
create_process_as_user(token, app, cmd, process_security, thread_security, hash["creation_flags"], env, cwd, startinfo, procinfo)
|
313
267
|
else
|
314
268
|
bool = CreateProcessWithLogonW(
|
315
269
|
logon, # User
|
@@ -318,7 +272,7 @@ module Process
|
|
318
272
|
LOGON_WITH_PROFILE, # Logon flags
|
319
273
|
app, # App name
|
320
274
|
cmd, # Command line
|
321
|
-
hash[
|
275
|
+
hash["creation_flags"], # Creation flags
|
322
276
|
env, # Environment
|
323
277
|
cwd, # Working directory
|
324
278
|
startinfo, # Startup Info
|
@@ -336,7 +290,7 @@ module Process
|
|
336
290
|
process_security, # Process attributes
|
337
291
|
thread_security, # Thread attributes
|
338
292
|
inherit, # Inherit handles?
|
339
|
-
hash[
|
293
|
+
hash["creation_flags"], # Creation flags
|
340
294
|
env, # Environment
|
341
295
|
cwd, # Working directory
|
342
296
|
startinfo, # Startup Info
|
@@ -350,7 +304,7 @@ module Process
|
|
350
304
|
|
351
305
|
# Automatically close the process and thread handles in the
|
352
306
|
# PROCESS_INFORMATION struct unless explicitly told not to.
|
353
|
-
if hash[
|
307
|
+
if hash["close_handles"]
|
354
308
|
CloseHandle(procinfo[:hProcess])
|
355
309
|
CloseHandle(procinfo[:hThread])
|
356
310
|
# Clear these fields so callers don't attempt to close the handle
|
@@ -367,5 +321,90 @@ module Process
|
|
367
321
|
procinfo[:dwThreadId]
|
368
322
|
)
|
369
323
|
end
|
324
|
+
|
325
|
+
def logon_user(user, domain, passwd, type, provider = LOGON32_PROVIDER_DEFAULT)
|
326
|
+
token = FFI::MemoryPointer.new(:ulong)
|
327
|
+
|
328
|
+
bool = LogonUserW(
|
329
|
+
user, # User
|
330
|
+
domain, # Domain
|
331
|
+
passwd, # Password
|
332
|
+
type, # Logon Type
|
333
|
+
provider, # Logon Provider
|
334
|
+
token # User token handle
|
335
|
+
)
|
336
|
+
|
337
|
+
unless bool
|
338
|
+
if (FFI.errno == ERROR_LOGON_TYPE_NOT_GRANTED) && (type == LOGON32_LOGON_BATCH)
|
339
|
+
user_utf8 = user.encode( "UTF-8", invalid: :replace, undef: :replace, replace: "" ).delete("\0")
|
340
|
+
raise SystemCallError.new("LogonUserW (User '#{user_utf8}' must hold 'Log on as a batch job' permissions.)", FFI.errno)
|
341
|
+
else
|
342
|
+
raise SystemCallError.new("LogonUserW", FFI.errno)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
token.read_ulong
|
347
|
+
end
|
348
|
+
|
349
|
+
def create_process_as_user(token, app, cmd, process_security, thread_security, inherit, creation_flags, env, cwd, startinfo, procinfo)
|
350
|
+
bool = CreateProcessAsUserW(
|
351
|
+
token, # User token handle
|
352
|
+
app, # App name
|
353
|
+
cmd, # Command line
|
354
|
+
process_security, # Process attributes
|
355
|
+
thread_security, # Thread attributes
|
356
|
+
inherit, # Inherit handles
|
357
|
+
creation_flags, # Creation Flags
|
358
|
+
env, # Environment
|
359
|
+
cwd, # Working directory
|
360
|
+
startinfo, # Startup Info
|
361
|
+
procinfo # Process Info
|
362
|
+
)
|
363
|
+
|
364
|
+
unless bool
|
365
|
+
if FFI.errno == ERROR_PRIVILEGE_NOT_HELD
|
366
|
+
raise SystemCallError.new("CreateProcessAsUserW (User '#{::ENV['USERNAME']}' must hold the 'Replace a process level token' and 'Adjust Memory Quotas for a process' permissions. Logoff the user after adding this right to make it effective.)", FFI.errno)
|
367
|
+
else
|
368
|
+
raise SystemCallError.new("CreateProcessAsUserW failed.", FFI.errno)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
ensure
|
372
|
+
CloseHandle(token)
|
373
|
+
end
|
374
|
+
|
375
|
+
def get_windows_station_name
|
376
|
+
winsta_name = FFI::MemoryPointer.new(:char, 256)
|
377
|
+
return_size = FFI::MemoryPointer.new(:ulong)
|
378
|
+
|
379
|
+
bool = GetUserObjectInformationA(
|
380
|
+
GetProcessWindowStation(), # Window station handle
|
381
|
+
UOI_NAME, # Information to get
|
382
|
+
winsta_name, # Buffer to receive information
|
383
|
+
winsta_name.size, # Size of buffer
|
384
|
+
return_size # Size filled into buffer
|
385
|
+
)
|
386
|
+
|
387
|
+
unless bool
|
388
|
+
raise SystemCallError.new("GetUserObjectInformationA", FFI.errno)
|
389
|
+
end
|
390
|
+
|
391
|
+
winsta_name.read_string(return_size.read_ulong)
|
392
|
+
end
|
393
|
+
|
394
|
+
def format_creds_from_hash(hash)
|
395
|
+
logon = hash["with_logon"].to_wide_string
|
396
|
+
|
397
|
+
if hash["password"]
|
398
|
+
passwd = hash["password"].to_wide_string
|
399
|
+
else
|
400
|
+
raise ArgumentError, "password must be specified if with_logon is used"
|
401
|
+
end
|
402
|
+
|
403
|
+
if hash["domain"]
|
404
|
+
domain = hash["domain"].to_wide_string
|
405
|
+
end
|
406
|
+
|
407
|
+
[ logon, passwd, domain ]
|
408
|
+
end
|
370
409
|
end
|
371
410
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
gemspec = eval(File.read(File.expand_path("../mixlib-shellout.gemspec", __FILE__)))
|
2
2
|
|
3
|
-
gemspec.platform = Gem::Platform.new(
|
3
|
+
gemspec.platform = Gem::Platform.new(%w{universal mingw32})
|
4
4
|
|
5
5
|
gemspec.add_dependency "win32-process", "~> 0.8.2"
|
6
6
|
gemspec.add_dependency "wmi-lite", "~> 1.0"
|
data/mixlib-shellout.gemspec
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
$:.unshift(File.dirname(__FILE__) +
|
2
|
-
require
|
1
|
+
$:.unshift(File.dirname(__FILE__) + "/lib")
|
2
|
+
require "mixlib/shellout/version"
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
|
-
s.name =
|
5
|
+
s.name = "mixlib-shellout"
|
6
6
|
s.version = Mixlib::ShellOut::VERSION
|
7
7
|
s.platform = Gem::Platform::RUBY
|
8
8
|
s.extra_rdoc_files = ["README.md", "LICENSE" ]
|
@@ -12,13 +12,14 @@ Gem::Specification.new do |s|
|
|
12
12
|
s.email = "info@chef.io"
|
13
13
|
s.homepage = "https://www.chef.io/"
|
14
14
|
|
15
|
-
s.required_ruby_version = ">=
|
15
|
+
s.required_ruby_version = ">= 2.2"
|
16
16
|
|
17
17
|
s.add_development_dependency "rspec", "~> 3.0"
|
18
|
+
s.add_development_dependency "chefstyle"
|
18
19
|
|
19
20
|
s.bindir = "bin"
|
20
21
|
s.executables = []
|
21
|
-
s.require_path =
|
22
|
-
s.files = %w
|
23
|
-
|
22
|
+
s.require_path = "lib"
|
23
|
+
s.files = %w{Gemfile Rakefile LICENSE README.md} + Dir.glob("*.gemspec") +
|
24
|
+
Dir.glob("lib/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) }
|
24
25
|
end
|