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
@@ -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
|