mixlib-shellout 3.2.7-x64-mingw-ucrt

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.
@@ -0,0 +1,629 @@
1
+ #
2
+ # Author:: Daniel DeLeo (<dan@chef.io>)
3
+ # Author:: John Keiser (<jkeiser@chef.io>)
4
+ # Copyright:: Copyright (c) Chef Software Inc.
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require "win32/process"
21
+ require "ffi/win32/extensions"
22
+
23
+ # Add new constants for Logon
24
+ module Process::Constants
25
+
26
+ LOGON32_LOGON_INTERACTIVE = 0x00000002
27
+ LOGON32_LOGON_BATCH = 0x00000004
28
+ LOGON32_PROVIDER_DEFAULT = 0x00000000
29
+ UOI_NAME = 0x00000002
30
+
31
+ WAIT_OBJECT_0 = 0
32
+ WAIT_TIMEOUT = 0x102
33
+ WAIT_ABANDONED = 128
34
+ WAIT_ABANDONED_0 = WAIT_ABANDONED
35
+ WAIT_FAILED = 0xFFFFFFFF
36
+
37
+ ERROR_PRIVILEGE_NOT_HELD = 1314
38
+ ERROR_LOGON_TYPE_NOT_GRANTED = 0x569
39
+
40
+ # Only documented in Userenv.h ???
41
+ # - ZERO (type Local) is assumed, no docs found
42
+ WIN32_PROFILETYPE_LOCAL = 0x00
43
+ WIN32_PROFILETYPE_PT_TEMPORARY = 0x01
44
+ WIN32_PROFILETYPE_PT_ROAMING = 0x02
45
+ WIN32_PROFILETYPE_PT_MANDATORY = 0x04
46
+ WIN32_PROFILETYPE_PT_ROAMING_PREEXISTING = 0x08
47
+
48
+ # The environment block list ends with two nulls (\0\0).
49
+ ENVIRONMENT_BLOCK_ENDS = "\0\0".freeze
50
+ end
51
+
52
+ # Structs required for data handling
53
+ module Process::Structs
54
+
55
+ class PROFILEINFO < FFI::Struct
56
+ layout(
57
+ :dwSize, :dword,
58
+ :dwFlags, :dword,
59
+ :lpUserName, :pointer,
60
+ :lpProfilePath, :pointer,
61
+ :lpDefaultPath, :pointer,
62
+ :lpServerName, :pointer,
63
+ :lpPolicyPath, :pointer,
64
+ :hProfile, :handle
65
+ )
66
+ end
67
+
68
+ end
69
+
70
+ # Define the functions needed to check with Service windows station
71
+ module Process::Functions
72
+ ffi_lib :userenv
73
+
74
+ attach_pfunc :GetProfileType,
75
+ [:pointer], :bool
76
+
77
+ attach_pfunc :LoadUserProfileW,
78
+ %i{handle pointer}, :bool
79
+
80
+ attach_pfunc :UnloadUserProfile,
81
+ %i{handle handle}, :bool
82
+
83
+ attach_pfunc :CreateEnvironmentBlock,
84
+ %i{pointer ulong bool}, :bool
85
+
86
+ attach_pfunc :DestroyEnvironmentBlock,
87
+ %i{pointer}, :bool
88
+
89
+ ffi_lib :advapi32
90
+
91
+ attach_pfunc :LogonUserW,
92
+ %i{buffer_in buffer_in buffer_in ulong ulong pointer}, :bool
93
+
94
+ attach_pfunc :CreateProcessAsUserW,
95
+ %i{ulong buffer_in buffer_inout pointer pointer int
96
+ ulong buffer_in buffer_in pointer pointer}, :bool
97
+
98
+ ffi_lib :user32
99
+
100
+ attach_pfunc :GetProcessWindowStation,
101
+ [], :ulong
102
+
103
+ attach_pfunc :GetUserObjectInformationA,
104
+ %i{ulong uint buffer_out ulong pointer}, :bool
105
+ end
106
+
107
+ # Override Process.create to check for running in the Service window station and doing
108
+ # a full logon with LogonUser, instead of a CreateProcessWithLogon
109
+ # Cloned from https://github.com/djberg96/win32-process/blob/ffi/lib/win32/process.rb
110
+ # as of 2015-10-15 from commit cc066e5df25048f9806a610f54bf5f7f253e86f7
111
+ module Process
112
+
113
+ class UnsupportedFeature < StandardError; end
114
+
115
+ # Explicitly reopen singleton class so that class/constant declarations from
116
+ # extensions are visible in Modules.nesting.
117
+ class << self
118
+
119
+ def create(args)
120
+ create3(args).first
121
+ end
122
+
123
+ def create3(args)
124
+ unless args.is_a?(Hash)
125
+ raise TypeError, "hash keyword arguments expected"
126
+ end
127
+
128
+ valid_keys = %w{
129
+ app_name command_line inherit creation_flags cwd environment
130
+ startup_info thread_inherit process_inherit close_handles with_logon
131
+ domain password elevated
132
+ }
133
+
134
+ valid_si_keys = %w{
135
+ startf_flags desktop title x y x_size y_size x_count_chars
136
+ y_count_chars fill_attribute sw_flags stdin stdout stderr
137
+ }
138
+
139
+ # Set default values
140
+ hash = {
141
+ "app_name" => nil,
142
+ "creation_flags" => 0,
143
+ "close_handles" => true,
144
+ }
145
+
146
+ # Validate the keys, and convert symbols and case to lowercase strings.
147
+ args.each do |key, val|
148
+ key = key.to_s.downcase
149
+ unless valid_keys.include?(key)
150
+ raise ArgumentError, "invalid key '#{key}'"
151
+ end
152
+
153
+ hash[key] = val
154
+ end
155
+
156
+ si_hash = {}
157
+
158
+ # If the startup_info key is present, validate its subkeys
159
+ hash["startup_info"]&.each do |key, val|
160
+ key = key.to_s.downcase
161
+ unless valid_si_keys.include?(key)
162
+ raise ArgumentError, "invalid startup_info key '#{key}'"
163
+ end
164
+
165
+ si_hash[key] = val
166
+ end
167
+
168
+ # The +command_line+ key is mandatory unless the +app_name+ key
169
+ # is specified.
170
+ unless hash["command_line"]
171
+ if hash["app_name"]
172
+ hash["command_line"] = hash["app_name"]
173
+ hash["app_name"] = nil
174
+ else
175
+ raise ArgumentError, "command_line or app_name must be specified"
176
+ end
177
+ end
178
+
179
+ env = nil
180
+
181
+ # Retrieve the environment variables for the specified user.
182
+ if hash["with_logon"]
183
+ logon, passwd, domain = format_creds_from_hash(hash)
184
+ logon_type = hash["elevated"] ? LOGON32_LOGON_BATCH : LOGON32_LOGON_INTERACTIVE
185
+ token = logon_user(logon, domain, passwd, logon_type)
186
+ logon_ptr = FFI::MemoryPointer.from_string(logon)
187
+ profile = PROFILEINFO.new.tap do |dat|
188
+ dat[:dwSize] = dat.size
189
+ dat[:dwFlags] = 1
190
+ dat[:lpUserName] = logon_ptr
191
+ end
192
+
193
+ load_user_profile(token, profile.pointer)
194
+ env_list = retrieve_environment_variables(token)
195
+ end
196
+
197
+ # The env string should be passed as a string of ';' separated paths.
198
+ if hash["environment"]
199
+ env = env_list.nil? ? hash["environment"] : merge_env_variables(env_list, hash["environment"])
200
+
201
+ unless env.respond_to?(:join)
202
+ env = hash["environment"].split(File::PATH_SEPARATOR)
203
+ end
204
+
205
+ env = env.map { |e| e + 0.chr }.join("") + 0.chr
206
+ env.to_wide_string! if hash["with_logon"]
207
+ end
208
+
209
+ # Process SECURITY_ATTRIBUTE structure
210
+ process_security = nil
211
+
212
+ if hash["process_inherit"]
213
+ process_security = SECURITY_ATTRIBUTES.new
214
+ process_security[:nLength] = 12
215
+ process_security[:bInheritHandle] = 1
216
+ end
217
+
218
+ # Thread SECURITY_ATTRIBUTE structure
219
+ thread_security = nil
220
+
221
+ if hash["thread_inherit"]
222
+ thread_security = SECURITY_ATTRIBUTES.new
223
+ thread_security[:nLength] = 12
224
+ thread_security[:bInheritHandle] = 1
225
+ end
226
+
227
+ # Automatically handle stdin, stdout and stderr as either IO objects
228
+ # or file descriptors. This won't work for StringIO, however. It also
229
+ # will not work on JRuby because of the way it handles internal file
230
+ # descriptors.
231
+ #
232
+ %w{stdin stdout stderr}.each do |io|
233
+ if si_hash[io]
234
+ if si_hash[io].respond_to?(:fileno)
235
+ handle = get_osfhandle(si_hash[io].fileno)
236
+ else
237
+ handle = get_osfhandle(si_hash[io])
238
+ end
239
+
240
+ if handle == INVALID_HANDLE_VALUE
241
+ ptr = FFI::MemoryPointer.new(:int)
242
+
243
+ if windows_version >= 6 && get_errno(ptr) == 0
244
+ errno = ptr.read_int
245
+ else
246
+ errno = FFI.errno
247
+ end
248
+
249
+ raise SystemCallError.new("get_osfhandle", errno)
250
+ end
251
+
252
+ # Most implementations of Ruby on Windows create inheritable
253
+ # handles by default, but some do not. RF bug #26988.
254
+ bool = SetHandleInformation(
255
+ handle,
256
+ HANDLE_FLAG_INHERIT,
257
+ HANDLE_FLAG_INHERIT
258
+ )
259
+
260
+ raise SystemCallError.new("SetHandleInformation", FFI.errno) unless bool
261
+
262
+ si_hash[io] = handle
263
+ si_hash["startf_flags"] ||= 0
264
+ si_hash["startf_flags"] |= STARTF_USESTDHANDLES
265
+ hash["inherit"] = true
266
+ end
267
+ end
268
+
269
+ procinfo = PROCESS_INFORMATION.new
270
+ startinfo = STARTUPINFO.new
271
+
272
+ unless si_hash.empty?
273
+ startinfo[:cb] = startinfo.size
274
+ startinfo[:lpDesktop] = si_hash["desktop"] if si_hash["desktop"]
275
+ startinfo[:lpTitle] = si_hash["title"] if si_hash["title"]
276
+ startinfo[:dwX] = si_hash["x"] if si_hash["x"]
277
+ startinfo[:dwY] = si_hash["y"] if si_hash["y"]
278
+ startinfo[:dwXSize] = si_hash["x_size"] if si_hash["x_size"]
279
+ startinfo[:dwYSize] = si_hash["y_size"] if si_hash["y_size"]
280
+ startinfo[:dwXCountChars] = si_hash["x_count_chars"] if si_hash["x_count_chars"]
281
+ startinfo[:dwYCountChars] = si_hash["y_count_chars"] if si_hash["y_count_chars"]
282
+ startinfo[:dwFillAttribute] = si_hash["fill_attribute"] if si_hash["fill_attribute"]
283
+ startinfo[:dwFlags] = si_hash["startf_flags"] if si_hash["startf_flags"]
284
+ startinfo[:wShowWindow] = si_hash["sw_flags"] if si_hash["sw_flags"]
285
+ startinfo[:cbReserved2] = 0
286
+ startinfo[:hStdInput] = si_hash["stdin"] if si_hash["stdin"]
287
+ startinfo[:hStdOutput] = si_hash["stdout"] if si_hash["stdout"]
288
+ startinfo[:hStdError] = si_hash["stderr"] if si_hash["stderr"]
289
+ end
290
+
291
+ app = nil
292
+ cmd = nil
293
+
294
+ # Convert strings to wide character strings if present
295
+ if hash["app_name"]
296
+ app = hash["app_name"].to_wide_string
297
+ end
298
+
299
+ if hash["command_line"]
300
+ cmd = hash["command_line"].to_wide_string
301
+ end
302
+
303
+ if hash["cwd"]
304
+ cwd = hash["cwd"].to_wide_string
305
+ end
306
+
307
+ inherit = hash["inherit"] ? 1 : 0
308
+
309
+ if hash["with_logon"]
310
+
311
+ logon, passwd, domain = format_creds_from_hash(hash)
312
+
313
+ hash["creation_flags"] |= CREATE_UNICODE_ENVIRONMENT
314
+
315
+ winsta_name = get_windows_station_name
316
+
317
+ # If running in the service windows station must do a log on to get
318
+ # to the interactive desktop. The running process user account must have
319
+ # the 'Replace a process level token' permission. This is necessary as
320
+ # the logon (which happens with CreateProcessWithLogon) must have an
321
+ # interactive windows station to attach to, which is created with the
322
+ # LogonUser call with the LOGON32_LOGON_INTERACTIVE flag.
323
+ #
324
+ # User Access Control (UAC) only applies to interactive logons, so we
325
+ # can simulate running a command 'elevated' by running it under a separate
326
+ # logon as a batch process.
327
+ if hash["elevated"] || winsta_name =~ /^Service-0x0-.*$/i
328
+
329
+ logon_type = hash["elevated"] ? LOGON32_LOGON_BATCH : LOGON32_LOGON_INTERACTIVE
330
+ token = logon_user(logon, domain, passwd, logon_type)
331
+ logon_ptr = FFI::MemoryPointer.from_string(logon)
332
+ profile = PROFILEINFO.new.tap do |dat|
333
+ dat[:dwSize] = dat.size
334
+ dat[:dwFlags] = 1
335
+ dat[:lpUserName] = logon_ptr
336
+ end
337
+
338
+ if logon_has_roaming_profile?
339
+ msg = %w{
340
+ Mixlib does not currently support executing commands as users
341
+ configured with Roaming Profiles. [%s]
342
+ }.join(" ") % logon.encode("UTF-8").unpack("A*")
343
+ raise UnsupportedFeature.new(msg)
344
+ end
345
+
346
+ load_user_profile(token, profile.pointer)
347
+
348
+ create_process_as_user(token, app, cmd, process_security,
349
+ thread_security, inherit, hash["creation_flags"], env,
350
+ cwd, startinfo, procinfo)
351
+
352
+ else
353
+
354
+ create_process_with_logon(logon, domain, passwd, LOGON_WITH_PROFILE,
355
+ app, cmd, hash["creation_flags"], env, cwd, startinfo, procinfo)
356
+
357
+ end
358
+
359
+ else
360
+
361
+ create_process(app, cmd, process_security, thread_security, inherit,
362
+ hash["creation_flags"], env, cwd, startinfo, procinfo)
363
+
364
+ end
365
+
366
+ # Automatically close the process and thread handles in the
367
+ # PROCESS_INFORMATION struct unless explicitly told not to.
368
+ if hash["close_handles"]
369
+ CloseHandle(procinfo[:hProcess])
370
+ CloseHandle(procinfo[:hThread])
371
+ # Clear these fields so callers don't attempt to close the handle
372
+ # which can result in the wrong handle being closed or an
373
+ # exception in some circumstances.
374
+ procinfo[:hProcess] = 0
375
+ procinfo[:hThread] = 0
376
+ end
377
+
378
+ process = ProcessInfo.new(
379
+ procinfo[:hProcess],
380
+ procinfo[:hThread],
381
+ procinfo[:dwProcessId],
382
+ procinfo[:dwThreadId]
383
+ )
384
+
385
+ [ process, profile, token ]
386
+ end
387
+
388
+ # See Process::Constants::WIN32_PROFILETYPE
389
+ def logon_has_roaming_profile?
390
+ get_profile_type >= 2
391
+ end
392
+
393
+ def get_profile_type
394
+ ptr = FFI::MemoryPointer.new(:uint)
395
+ unless GetProfileType(ptr)
396
+ raise SystemCallError.new("GetProfileType", FFI.errno)
397
+ end
398
+
399
+ ptr.read_uint
400
+ end
401
+
402
+ def load_user_profile(token, profile_ptr)
403
+ unless LoadUserProfileW(token, profile_ptr)
404
+ raise SystemCallError.new("LoadUserProfileW", FFI.errno)
405
+ end
406
+
407
+ true
408
+ end
409
+
410
+ def unload_user_profile(token, profile)
411
+ if profile[:hProfile] == 0
412
+ warn "\n\nWARNING: Profile not loaded\n"
413
+ else
414
+ unless UnloadUserProfile(token, profile[:hProfile])
415
+ raise SystemCallError.new("UnloadUserProfile", FFI.errno)
416
+ end
417
+ end
418
+ true
419
+ end
420
+
421
+ # Retrieves the environment variables for the specified user.
422
+ #
423
+ # @param env_pointer [Pointer] The environment block is an array of null-terminated Unicode strings.
424
+ # @param token [Integer] User token handle.
425
+ # @return [Boolean] true if successfully retrieves the environment variables for the specified user.
426
+ #
427
+ def create_environment_block(env_pointer, token)
428
+ unless CreateEnvironmentBlock(env_pointer, token, false)
429
+ raise SystemCallError.new("CreateEnvironmentBlock", FFI.errno)
430
+ end
431
+
432
+ true
433
+ end
434
+
435
+ # Frees environment variables created by the CreateEnvironmentBlock function.
436
+ #
437
+ # @param env_pointer [Pointer] The environment block is an array of null-terminated Unicode strings.
438
+ # @return [Boolean] true if successfully frees environment variables created by the CreateEnvironmentBlock function.
439
+ #
440
+ def destroy_environment_block(env_pointer)
441
+ unless DestroyEnvironmentBlock(env_pointer)
442
+ raise SystemCallError.new("DestroyEnvironmentBlock", FFI.errno)
443
+ end
444
+
445
+ true
446
+ end
447
+
448
+ def create_process_as_user(token, app, cmd, process_security,
449
+ thread_security, inherit, creation_flags, env, cwd, startinfo, procinfo)
450
+
451
+ bool = CreateProcessAsUserW(
452
+ token, # User token handle
453
+ app, # App name
454
+ cmd, # Command line
455
+ process_security, # Process attributes
456
+ thread_security, # Thread attributes
457
+ inherit, # Inherit handles
458
+ creation_flags, # Creation Flags
459
+ env, # Environment
460
+ cwd, # Working directory
461
+ startinfo, # Startup Info
462
+ procinfo # Process Info
463
+ )
464
+
465
+ unless bool
466
+ msg = case FFI.errno
467
+ when ERROR_PRIVILEGE_NOT_HELD
468
+ [
469
+ %{CreateProcessAsUserW (User '%s' must hold the 'Replace a process},
470
+ %{level token' and 'Adjust Memory Quotas for a process' permissions.},
471
+ %{Logoff the user after adding this right to make it effective.)},
472
+ ].join(" ") % ::ENV["USERNAME"]
473
+ else
474
+ "CreateProcessAsUserW failed."
475
+ end
476
+ raise SystemCallError.new(msg, FFI.errno)
477
+ end
478
+ end
479
+
480
+ def create_process_with_logon(logon, domain, passwd, logon_flags, app, cmd,
481
+ creation_flags, env, cwd, startinfo, procinfo)
482
+
483
+ bool = CreateProcessWithLogonW(
484
+ logon, # User
485
+ domain, # Domain
486
+ passwd, # Password
487
+ logon_flags, # Logon flags
488
+ app, # App name
489
+ cmd, # Command line
490
+ creation_flags, # Creation flags
491
+ env, # Environment
492
+ cwd, # Working directory
493
+ startinfo, # Startup Info
494
+ procinfo # Process Info
495
+ )
496
+
497
+ unless bool
498
+ raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno)
499
+ end
500
+ end
501
+
502
+ def create_process(app, cmd, process_security, thread_security, inherit,
503
+ creation_flags, env, cwd, startinfo, procinfo)
504
+
505
+ bool = CreateProcessW(
506
+ app, # App name
507
+ cmd, # Command line
508
+ process_security, # Process attributes
509
+ thread_security, # Thread attributes
510
+ inherit, # Inherit handles?
511
+ creation_flags, # Creation flags
512
+ env, # Environment
513
+ cwd, # Working directory
514
+ startinfo, # Startup Info
515
+ procinfo # Process Info
516
+ )
517
+
518
+ unless bool
519
+ raise SystemCallError.new("CreateProcessW", FFI.errno)
520
+ end
521
+ end
522
+
523
+ def logon_user(user, domain, passwd, type, provider = LOGON32_PROVIDER_DEFAULT)
524
+ token = FFI::MemoryPointer.new(:ulong)
525
+
526
+ bool = LogonUserW(
527
+ user, # User
528
+ domain, # Domain
529
+ passwd, # Password
530
+ type, # Logon Type
531
+ provider, # Logon Provider
532
+ token # User token handle
533
+ )
534
+
535
+ unless bool
536
+ if (FFI.errno == ERROR_LOGON_TYPE_NOT_GRANTED) && (type == LOGON32_LOGON_BATCH)
537
+ user_utf8 = user.encode( "UTF-8", invalid: :replace, undef: :replace, replace: "" ).delete("\0")
538
+ raise SystemCallError.new("LogonUserW (User '#{user_utf8}' must hold 'Log on as a batch job' permissions.)", FFI.errno)
539
+ else
540
+ raise SystemCallError.new("LogonUserW", FFI.errno)
541
+ end
542
+ end
543
+
544
+ token.read_ulong
545
+ end
546
+
547
+ def get_windows_station_name
548
+ winsta_name = FFI::MemoryPointer.new(:char, 256)
549
+ return_size = FFI::MemoryPointer.new(:ulong)
550
+
551
+ bool = GetUserObjectInformationA(
552
+ GetProcessWindowStation(), # Window station handle
553
+ UOI_NAME, # Information to get
554
+ winsta_name, # Buffer to receive information
555
+ winsta_name.size, # Size of buffer
556
+ return_size # Size filled into buffer
557
+ )
558
+
559
+ unless bool
560
+ raise SystemCallError.new("GetUserObjectInformationA", FFI.errno)
561
+ end
562
+
563
+ winsta_name.read_string(return_size.read_ulong)
564
+ end
565
+
566
+ def format_creds_from_hash(hash)
567
+ logon = hash["with_logon"].to_wide_string
568
+
569
+ if hash["password"]
570
+ passwd = hash["password"].to_wide_string
571
+ else
572
+ raise ArgumentError, "password must be specified if with_logon is used"
573
+ end
574
+
575
+ if hash["domain"]
576
+ domain = hash["domain"].to_wide_string
577
+ end
578
+
579
+ [ logon, passwd, domain ]
580
+ end
581
+
582
+ # Retrieves the environment variables for the specified user.
583
+ #
584
+ # @param token [Integer] User token handle.
585
+ # @return env_list [Array<String>] Environment variables of specified user.
586
+ #
587
+ def retrieve_environment_variables(token)
588
+ env_list = []
589
+ env_pointer = FFI::MemoryPointer.new(:pointer)
590
+ create_environment_block(env_pointer, token)
591
+ str_ptr = env_pointer.read_pointer
592
+ offset = 0
593
+ loop do
594
+ new_str_pointer = str_ptr + offset
595
+ break if new_str_pointer.read_string(2) == ENVIRONMENT_BLOCK_ENDS
596
+
597
+ environment = new_str_pointer.read_wstring
598
+ env_list << environment
599
+ offset = offset + environment.length * 2 + 2
600
+ end
601
+
602
+ # To free the buffer when we have finished with the environment block
603
+ destroy_environment_block(str_ptr)
604
+ env_list
605
+ end
606
+
607
+ # Merge environment variables of specified user and current environment variables.
608
+ #
609
+ # @param fetched_env [Array<String>] environment variables of specified user.
610
+ # @param current_env [Array<String>] current environment variables.
611
+ # @return [Array<String>] Merged environment variables.
612
+ #
613
+ def merge_env_variables(fetched_env, current_env)
614
+ env_hash_1 = environment_list_to_hash(fetched_env)
615
+ env_hash_2 = environment_list_to_hash(current_env)
616
+ merged_env = env_hash_2.merge(env_hash_1)
617
+ merged_env.map { |k, v| "#{k}=#{v}" }
618
+ end
619
+
620
+ # Convert an array to a hash.
621
+ #
622
+ # @param env_var [Array<String>] Environment variables.
623
+ # @return [Hash] Converted an array to hash.
624
+ #
625
+ def environment_list_to_hash(env_var)
626
+ Hash[ env_var.map { |pair| pair.split("=", 2) } ]
627
+ end
628
+ end
629
+ end