module Process
def create(args)
def create(args) create3(args).first end
def create3(args)
def create3(args) unless args.is_a?(Hash) raise TypeError, "hash keyword arguments expected" end valid_keys = %w{ app_name command_line inherit creation_flags cwd environment startup_info thread_inherit process_inherit close_handles with_logon domain password elevated } valid_si_keys = %w{ startf_flags desktop title x y x_size y_size x_count_chars y_count_chars fill_attribute sw_flags stdin stdout stderr } # Set default values hash = { "app_name" => nil, "creation_flags" => 0, "close_handles" => true, } # Validate the keys, and convert symbols and case to lowercase strings. args.each do |key, val| key = key.to_s.downcase unless valid_keys.include?(key) raise ArgumentError, "invalid key '#{key}'" end hash[key] = val end si_hash = {} # If the startup_info key is present, validate its subkeys hash["startup_info"]&.each do |key, val| key = key.to_s.downcase unless valid_si_keys.include?(key) raise ArgumentError, "invalid startup_info key '#{key}'" end si_hash[key] = val end # The +command_line+ key is mandatory unless the +app_name+ key # is specified. unless hash["command_line"] if hash["app_name"] hash["command_line"] = hash["app_name"] hash["app_name"] = nil else raise ArgumentError, "command_line or app_name must be specified" end end env = nil # Retrieve the environment variables for the specified user. if hash["with_logon"] logon, passwd, domain = format_creds_from_hash(hash) logon_type = hash["elevated"] ? LOGON32_LOGON_BATCH : LOGON32_LOGON_INTERACTIVE token = logon_user(logon, domain, passwd, logon_type) logon_ptr = FFI::MemoryPointer.from_string(logon) profile = PROFILEINFO.new.tap do |dat| dat[:dwSize] = dat.size dat[:dwFlags] = 1 dat[:lpUserName] = logon_ptr end load_user_profile(token, profile.pointer) env_list = retrieve_environment_variables(token) end # The env string should be passed as a string of ';' separated paths. if hash["environment"] env = env_list.nil? ? hash["environment"] : merge_env_variables(env_list, hash["environment"]) unless env.respond_to?(:join) env = hash["environment"].split(File::PATH_SEPARATOR) end env = env.map { |e| e + 0.chr }.join("") + 0.chr env.to_wide_string! if hash["with_logon"] end # Process SECURITY_ATTRIBUTE structure process_security = nil if hash["process_inherit"] process_security = SECURITY_ATTRIBUTES.new process_security[:nLength] = 12 process_security[:bInheritHandle] = 1 end # Thread SECURITY_ATTRIBUTE structure thread_security = nil if hash["thread_inherit"] thread_security = SECURITY_ATTRIBUTES.new thread_security[:nLength] = 12 thread_security[:bInheritHandle] = 1 end # Automatically handle stdin, stdout and stderr as either IO objects # or file descriptors. This won't work for StringIO, however. It also # will not work on JRuby because of the way it handles internal file # descriptors. # %w{stdin stdout stderr}.each do |io| if si_hash[io] if si_hash[io].respond_to?(:fileno) handle = get_osfhandle(si_hash[io].fileno) else handle = get_osfhandle(si_hash[io]) end if handle == INVALID_HANDLE_VALUE ptr = FFI::MemoryPointer.new(:int) if windows_version >= 6 && get_errno(ptr) == 0 errno = ptr.read_int else errno = FFI.errno end raise SystemCallError.new("get_osfhandle", errno) end # Most implementations of Ruby on Windows create inheritable # handles by default, but some do not. RF bug #26988. bool = SetHandleInformation( handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT ) raise SystemCallError.new("SetHandleInformation", FFI.errno) unless bool si_hash[io] = handle si_hash["startf_flags"] ||= 0 si_hash["startf_flags"] |= STARTF_USESTDHANDLES hash["inherit"] = true end end procinfo = PROCESS_INFORMATION.new startinfo = STARTUPINFO.new unless si_hash.empty? startinfo[:cb] = startinfo.size startinfo[:lpDesktop] = si_hash["desktop"] if si_hash["desktop"] startinfo[:lpTitle] = si_hash["title"] if si_hash["title"] startinfo[:dwX] = si_hash["x"] if si_hash["x"] startinfo[:dwY] = si_hash["y"] if si_hash["y"] startinfo[:dwXSize] = si_hash["x_size"] if si_hash["x_size"] startinfo[:dwYSize] = si_hash["y_size"] if si_hash["y_size"] startinfo[:dwXCountChars] = si_hash["x_count_chars"] if si_hash["x_count_chars"] startinfo[:dwYCountChars] = si_hash["y_count_chars"] if si_hash["y_count_chars"] startinfo[:dwFillAttribute] = si_hash["fill_attribute"] if si_hash["fill_attribute"] startinfo[:dwFlags] = si_hash["startf_flags"] if si_hash["startf_flags"] startinfo[:wShowWindow] = si_hash["sw_flags"] if si_hash["sw_flags"] startinfo[:cbReserved2] = 0 startinfo[:hStdInput] = si_hash["stdin"] if si_hash["stdin"] startinfo[:hStdOutput] = si_hash["stdout"] if si_hash["stdout"] startinfo[:hStdError] = si_hash["stderr"] if si_hash["stderr"] end app = nil cmd = nil # Convert strings to wide character strings if present if hash["app_name"] app = hash["app_name"].to_wide_string end if hash["command_line"] cmd = hash["command_line"].to_wide_string end if hash["cwd"] cwd = hash["cwd"].to_wide_string end inherit = hash["inherit"] ? 1 : 0 if hash["with_logon"] logon, passwd, domain = format_creds_from_hash(hash) hash["creation_flags"] |= CREATE_UNICODE_ENVIRONMENT winsta_name = get_windows_station_name # If running in the service windows station must do a log on to get # to the interactive desktop. The running process user account must have # the 'Replace a process level token' permission. This is necessary as # the logon (which happens with CreateProcessWithLogon) must have an # interactive windows station to attach to, which is created with the # LogonUser call with the LOGON32_LOGON_INTERACTIVE flag. # # User Access Control (UAC) only applies to interactive logons, so we # can simulate running a command 'elevated' by running it under a separate # logon as a batch process. if hash["elevated"] || winsta_name =~ /^Service-0x0-.*$/i logon_type = hash["elevated"] ? LOGON32_LOGON_BATCH : LOGON32_LOGON_INTERACTIVE token = logon_user(logon, domain, passwd, logon_type) logon_ptr = FFI::MemoryPointer.from_string(logon) profile = PROFILEINFO.new.tap do |dat| dat[:dwSize] = dat.size dat[:dwFlags] = 1 dat[:lpUserName] = logon_ptr end if logon_has_roaming_profile? msg = %w{ Mixlib does not currently support executing commands as users configured with Roaming Profiles. [%s] }.join(" ") % logon.encode("UTF-8").unpack("A*") raise UnsupportedFeature.new(msg) end load_user_profile(token, profile.pointer) create_process_as_user(token, app, cmd, process_security, thread_security, inherit, hash["creation_flags"], env, cwd, startinfo, procinfo) else create_process_with_logon(logon, domain, passwd, LOGON_WITH_PROFILE, app, cmd, hash["creation_flags"], env, cwd, startinfo, procinfo) end else create_process(app, cmd, process_security, thread_security, inherit, hash["creation_flags"], env, cwd, startinfo, procinfo) end # Automatically close the process and thread handles in the # PROCESS_INFORMATION struct unless explicitly told not to. if hash["close_handles"] CloseHandle(procinfo[:hProcess]) CloseHandle(procinfo[:hThread]) # Clear these fields so callers don't attempt to close the handle # which can result in the wrong handle being closed or an # exception in some circumstances. procinfo[:hProcess] = 0 procinfo[:hThread] = 0 end process = ProcessInfo.new( procinfo[:hProcess], procinfo[:hThread], procinfo[:dwProcessId], procinfo[:dwThreadId] ) [ process, profile, token ] end
def create_environment_block(env_pointer, token)
-
(Boolean)
- true if successfully retrieves the environment variables for the specified user.
Parameters:
-
token
(Integer
) -- User token handle. -
env_pointer
(Pointer
) -- The environment block is an array of null-terminated Unicode strings.
def create_environment_block(env_pointer, token) unless CreateEnvironmentBlock(env_pointer, token, false) raise SystemCallError.new("CreateEnvironmentBlock", FFI.errno) end true end
def create_process(app, cmd, process_security, thread_security, inherit,
def create_process(app, cmd, process_security, thread_security, inherit, creation_flags, env, cwd, startinfo, procinfo) bool = CreateProcessW( app, # App name cmd, # Command line process_security, # Process attributes thread_security, # Thread attributes inherit, # Inherit handles? creation_flags, # Creation flags env, # Environment cwd, # Working directory startinfo, # Startup Info procinfo # Process Info ) unless bool raise SystemCallError.new("CreateProcessW", FFI.errno) end end
def create_process_as_user(token, app, cmd, process_security,
def create_process_as_user(token, app, cmd, process_security, thread_security, inherit, creation_flags, env, cwd, startinfo, procinfo) bool = CreateProcessAsUserW( token, # User token handle app, # App name cmd, # Command line process_security, # Process attributes thread_security, # Thread attributes inherit, # Inherit handles creation_flags, # Creation Flags env, # Environment cwd, # Working directory startinfo, # Startup Info procinfo # Process Info ) unless bool msg = case FFI.errno when ERROR_PRIVILEGE_NOT_HELD [ %{CreateProcessAsUserW (User '%s' 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.)}, ].join(" ") % ::ENV["USERNAME"] else "CreateProcessAsUserW failed." end raise SystemCallError.new(msg, FFI.errno) end end
def create_process_with_logon(logon, domain, passwd, logon_flags, app, cmd,
def create_process_with_logon(logon, domain, passwd, logon_flags, app, cmd, creation_flags, env, cwd, startinfo, procinfo) bool = CreateProcessWithLogonW( logon, # User domain, # Domain passwd, # Password logon_flags, # Logon flags app, # App name cmd, # Command line creation_flags, # Creation flags env, # Environment cwd, # Working directory startinfo, # Startup Info procinfo # Process Info ) unless bool raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno) end end
def destroy_environment_block(env_pointer)
-
(Boolean)
- true if successfully frees environment variables created by the CreateEnvironmentBlock function.
Parameters:
-
env_pointer
(Pointer
) -- The environment block is an array of null-terminated Unicode strings.
def destroy_environment_block(env_pointer) unless DestroyEnvironmentBlock(env_pointer) raise SystemCallError.new("DestroyEnvironmentBlock", FFI.errno) end true end
def environment_list_to_hash(env_var)
-
(Hash)
- Converted an array to hash.
Parameters:
-
env_var
(Array
) -- Environment variables.
def environment_list_to_hash(env_var) Hash[ env_var.map { |pair| pair.split("=", 2) } ] end
def format_creds_from_hash(hash)
def format_creds_from_hash(hash) logon = hash["with_logon"].to_wide_string if hash["password"] passwd = hash["password"].to_wide_string else raise ArgumentError, "password must be specified if with_logon is used" end if hash["domain"] domain = hash["domain"].to_wide_string end [ logon, passwd, domain ] end
def get_profile_type
def get_profile_type ptr = FFI::MemoryPointer.new(:uint) unless GetProfileType(ptr) raise SystemCallError.new("GetProfileType", FFI.errno) end ptr.read_uint end
def get_windows_station_name
def get_windows_station_name winsta_name = FFI::MemoryPointer.new(:char, 256) return_size = FFI::MemoryPointer.new(:ulong) bool = GetUserObjectInformationA( GetProcessWindowStation(), # Window station handle UOI_NAME, # Information to get winsta_name, # Buffer to receive information winsta_name.size, # Size of buffer return_size # Size filled into buffer ) unless bool raise SystemCallError.new("GetUserObjectInformationA", FFI.errno) end winsta_name.read_string(return_size.read_ulong) end
def load_user_profile(token, profile_ptr)
def load_user_profile(token, profile_ptr) unless LoadUserProfileW(token, profile_ptr) raise SystemCallError.new("LoadUserProfileW", FFI.errno) end true end
def logon_has_roaming_profile?
def logon_has_roaming_profile? get_profile_type >= 2 end
def logon_user(user, domain, passwd, type, provider = LOGON32_PROVIDER_DEFAULT)
def logon_user(user, domain, passwd, type, provider = LOGON32_PROVIDER_DEFAULT) token = FFI::MemoryPointer.new(:ulong) bool = LogonUserW( user, # User domain, # Domain passwd, # Password type, # Logon Type provider, # Logon Provider token # User token handle ) unless bool if (FFI.errno == ERROR_LOGON_TYPE_NOT_GRANTED) && (type == LOGON32_LOGON_BATCH) user_utf8 = user.encode( "UTF-8", invalid: :replace, undef: :replace, replace: "" ).delete("\0") raise SystemCallError.new("LogonUserW (User '#{user_utf8}' must hold 'Log on as a batch job' permissions.)", FFI.errno) else raise SystemCallError.new("LogonUserW", FFI.errno) end end token.read_ulong end
def merge_env_variables(fetched_env, current_env)
-
(Array
- Merged environment variables.)
Parameters:
-
current_env
(Array
) -- current environment variables. -
fetched_env
(Array
) -- environment variables of specified user.
def merge_env_variables(fetched_env, current_env) env_hash_1 = environment_list_to_hash(fetched_env) env_hash_2 = environment_list_to_hash(current_env) merged_env = env_hash_2.merge(env_hash_1) merged_env.map { |k, v| "#{k}=#{v}" } end
def retrieve_environment_variables(token)
-
token
(Integer
) -- User token handle.
def retrieve_environment_variables(token) env_list = [] env_pointer = FFI::MemoryPointer.new(:pointer) create_environment_block(env_pointer, token) str_ptr = env_pointer.read_pointer offset = 0 loop do new_str_pointer = str_ptr + offset break if new_str_pointer.read_string(2) == ENVIRONMENT_BLOCK_ENDS environment = new_str_pointer.read_wstring env_list << environment offset = offset + environment.length * 2 + 2 end # To free the buffer when we have finished with the environment block destroy_environment_block(str_ptr) env_list end
def unload_user_profile(token, profile)
def unload_user_profile(token, profile) if profile[:hProfile] == 0 warn "\n\nWARNING: Profile not loaded\n" else unless UnloadUserProfile(token, profile[:hProfile]) raise SystemCallError.new("UnloadUserProfile", FFI.errno) end end true end