Skip to content

Issue with calling IO.popen(env, cmd, opts) on Windows #9352

@headius

Description

@headius

Original issue: #9295

While implementing GitHub action runs with parallel_tests on Window server, we got error:

TypeError: no implicit conversion of Hash into String
                               popen at org/jruby/RubyIO.java:4541
  execute_command_and_capture_output at C:/a/eazybi/eazybi/vendor/bundle/jruby/3.1.0/gems/parallel_tests-5.5.0/lib/parallel_tests/test/runner.rb:107
                     execute_command at C:/a/eazybi/eazybi/vendor/bundle/jruby/3.1.0/gems/parallel_tests-5.5.0/lib/parallel_tests/test/runner.rb:94
         execute_command_in_parallel at C:/a/eazybi/eazybi/vendor/bundle/jruby/3.1.0/gems/parallel_tests-5.5.0/lib/parallel_tests/cli.rb:430
                 execute_in_parallel at C:/a/eazybi/eazybi/vendor/bundle/jruby/3.1.0/gems/parallel_tests-5.5.0/lib/parallel_tests/cli.rb:61
                     call_with_index at C:/a/eazybi/eazybi/vendor/bundle/jruby/3.1.0/gems/parallel-1.27.0/lib/parallel.rb:650
                     work_in_threads at C:/a/eazybi/eazybi/vendor/bundle/jruby/3.1.0/gems/parallel-1.27.0/lib/parallel.rb:441
                with_instrumentation at C:/a/eazybi/eazybi/vendor/bundle/jruby/3.1.0/gems/parallel-1.27.0/lib/parallel.rb:660
                     work_in_threads at C:/a/eazybi/eazybi/vendor/bundle/jruby/3.1.0/gems/parallel-1.27.0/lib/parallel.rb:440
                          in_threads at C:/a/eazybi/eazybi/vendor/bundle/jruby/3.1.0/gems/parallel-1.27.0/lib/parallel.rb:219

For our case we patched the gem to call not IO.popen(env, cmd, popen_options), but IO.popen([env, *cmd], **popen_options).

Environment Information

Discovered this issue on Windows and JRuby 9.4.13.0, but it's actual also on the master branch and can be reproduced on Unix when the native popen call is disabled with -Xnative.popen=false

Other relevant info you may wish to add:
No gems needed, no framework or environment variables.

Expected Behavior

The following test code:

def test(description)
  print "#{description}... "
  begin
    result = yield
    puts "OK (#{result.inspect})"
  rescue => e
    puts "FAIL: #{e.class}: #{e.message}"
  end
end

# 1) Basic popen - no env, no options
test("cmd only") do
  IO.popen("echo hello") { |io| io.read.strip }
end

# 2) With options hash only (no env)
test("cmd + options") do
  IO.popen("echo hello", err: [:child, :out]) { |io| io.read.strip }
end

# 3) With env hash only (no options)
test("env + cmd") do
  IO.popen({"MY_VAR" => "1"}, "echo hello") { |io| io.read.strip }
end

# 4) With both env hash AND options hash
test("env + cmd + options") do
  IO.popen({"MY_VAR" => "1"}, "echo hello", err: [:child, :out]) { |io| io.read.strip }
end

# 5) Array command form with env and options
test("env + [cmd] + options") do
  IO.popen({"MY_VAR" => "1"}, ["echo", "hello"], err: [:child, :out]) { |io| io.read.strip }
end

# 6) Array of environment and command with options
test("[env, *cmd] + options") do
  IO.popen([{"MY_VAR" => "1"}, *["echo", "hello"]], err: [:child, :out]) { |io| io.read.strip }
end

When executed returns all OK as with native popen:

cmd only... OK ("hello")
cmd + options... OK ("hello")
env + cmd... OK ("hello")
env + cmd + options... OK ("hello")
env + [cmd] + options... OK ("hello")
[env, *cmd] + options... OK ("hello")

Actual Behavior

When the test executed on Windows or with native popen disabled:

cmd only... OK ("hello")
cmd + options... test_popen_env_opts.rb:18: warning: unsupported popen option: err
OK ("hello")
env + cmd... OK ("hello")
env + cmd + options... FAIL: TypeError: no implicit conversion of Hash into String
env + [cmd] + options... FAIL: TypeError: no implicit conversion of Hash into String
[env, *cmd] + options...  test_popen_env_opts.rb:38: warning: unsupported popen option: err
OK ("hello")

Possible solution

The following code n RubyIO.java popen method is responsible for the parameter processing:

        if (argc > 0 && !TypeConverter.checkHashType(runtime, args[0]).isNil()) {
            firstArg++;
            argc--;
        }

        if (argc > 0 && !(tmp = TypeConverter.checkHashType(runtime, args[argc - 1])).isNil()) {
            options = (RubyHash)tmp;
            argc--;
        }

        if (argc > 1) {
            pmode = args[firstArg + 1];
        }
  1. The first block strips environment hash from front -> firstArg = 1, argc = 2
  2. Second condition now checks args[argc-1] == args[1], not the needed args[2].
  3. As argc still is 2, the next condition is true and pmode = args[2] which is the given options hash.
  4. Later extractModeEncoding is trying to convert options hash to string and the error is raised.

The possible solution could be to change the order of the operations:

        if (argc > 1 && !(tmp = TypeConverter.checkHashType(runtime, args[argc - 1])).isNil()) {
            options = (RubyHash)tmp;
            argc--;
        }

        if (argc > 0 && !TypeConverter.checkHashType(runtime, args[0]).isNil()) {
            firstArg++;
            argc--;
        }

        if (argc > 1) {
            pmode = args[firstArg + 1];
        }

After applying these changes, the test code now executes with warnings, but that's already different issue:

cmd only... OK ("hello")
cmd + options... test_popen_env_opts.rb:18: warning: unsupported popen option: err
OK ("hello")
env + cmd... OK ("hello")
env + cmd + options... test_popen_env_opts.rb:28: warning: unsupported popen option: err
OK ("hello")
env + [cmd] + options... test_popen_env_opts.rb:33: warning: unsupported popen option: err
OK ("hello")
[env, *cmd] + options... test_popen_env_opts.rb:38: warning: unsupported popen option: err
OK ("hello")

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions