package org.jruby.util.io;

import jnr.constants.platform.Errno;
import jnr.constants.platform.Fcntl;
import jnr.constants.platform.OpenFlags;
import jnr.enxio.channels.NativeDeviceChannel;
import jnr.posix.SpawnAttribute;
import jnr.posix.SpawnFileAction;
import org.jcodings.transcode.EConvFlags;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBasicObject;
import org.jruby.RubyClass;
import org.jruby.RubyFile;
import org.jruby.RubyFixnum;
import org.jruby.RubyHash;
import org.jruby.RubyIO;
import org.jruby.RubyNumeric;
import org.jruby.RubyProcess;
import org.jruby.RubyString;
import org.jruby.RubySymbol;
import org.jruby.api.API;
import org.jruby.exceptions.RaiseException;
import org.jruby.ext.fcntl.FcntlLibrary;
import org.jruby.platform.Platform;
import org.jruby.runtime.Arity;
import org.jruby.runtime.Block;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;
import org.jruby.util.ShellLauncher;
import org.jruby.util.StringSupport;
import org.jruby.util.TypeConverter;
import org.jruby.util.cli.Options;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Port of MRI's popen+exec logic.
 */
public class PopenExecutor {
    /**
     * Check properties and runtime state to determine whether a native popen is possible.
     *
     * @param runtime current runtime
     * @return true if popen can use native code, false otherwise
     */
    public static boolean nativePopenAvailable(Ruby runtime) {
        return Options.NATIVE_POPEN.load() && runtime.getPosix().isNative() && !Platform.IS_WINDOWS;
    }

    // MRI: check_pipe_command
    public static IRubyObject checkPipeCommand(ThreadContext context, IRubyObject filenameOrCommand) {
        RubyString filenameStr = filenameOrCommand.convertToString();
        ByteList filenameByteList = filenameStr.getByteList();
        final int[] chlen = {0};

        if (EncodingUtils.encAscget(
                filenameByteList.getUnsafeBytes(),
                filenameByteList.getBegin(),
                filenameByteList.getBegin() + filenameByteList.getRealSize(),
                chlen,
                filenameByteList.getEncoding()) == '|') {
            return filenameStr.makeShared(context.runtime, chlen[0], filenameByteList.length() - 1).infectBy(filenameOrCommand);
        }
        return context.nil;
    }

    // MRI: rb_f_spawn
    public static RubyFixnum spawn(ThreadContext context, IRubyObject[] argv) {
        Ruby runtime = context.runtime;
        long pid = 0;
        String[] errmsg = { null };
        ExecArg eargp;
        IRubyObject fail_str;

        eargp = execargNew(context, argv, true);
        execargFixup(context, runtime, eargp);
        fail_str = eargp.use_shell ? eargp.command_name : eargp.command_name;

        PopenExecutor executor = new PopenExecutor();
        pid = executor.spawnProcess(context, runtime, eargp, errmsg);

        if (pid == -1) {
            if (errmsg[0] == null) {
                throw runtime.newErrnoFromErrno(executor.errno, fail_str.toString());
            }
            throw runtime.newErrnoFromErrno(executor.errno, errmsg[0]);
        }
        return runtime.newFixnum(pid);
    }

    // MRI: rb_spawn_internal
    public long spawnInternal(ThreadContext context, IRubyObject[] argv, String[] errmsg) {
        ExecArg eargp;
        long ret;

        eargp = execargNew(context, argv, true);
        execargFixup(context, context.runtime, eargp);
        ret = spawnProcess(context, context.runtime, eargp, errmsg);
        return ret;
    }

    // MRI: rb_spawn_process
    long spawnProcess(ThreadContext context, Ruby runtime, ExecArg eargp, String[] errmsg) {
        long pid;
        RubyString prog;
        ExecArg sarg = new ExecArg();

        prog = eargp.use_shell ? eargp.command_name : eargp.command_name;

        if (eargp.chdir_given()) {
            // we can'd do chdir with posix_spawn, so we should be set to use_shell and now
            // just need to add chdir to the cmd
            prog = (RubyString)prog.strDup(runtime).prepend(context, RubyString.newString(runtime, "cd '" + eargp.chdir_dir + "'; "));
            eargp.chdir_dir = null;
            eargp.chdir_given_clear();

            // create new pgroup to prevent orphaned processes when the parent is killed
            eargp.attributes.add(SpawnAttribute.pgroup(0));
            eargp.attributes.add(SpawnAttribute.flags((short)SpawnAttribute.SETPGROUP));
        }

        if (execargRunOptions(context, runtime, eargp, sarg, errmsg) < 0) {
            return -1;
        }

        if (prog != null && !eargp.use_shell) {
            String[] argv = eargp.argv_str.argv;
            if (argv.length > 0) {
                argv[0] = prog.toString();
            }
        }
        if (eargp.use_shell) {
            pid = procSpawnSh(runtime, prog.toString(), eargp);
        }
        else {
            String[] argv = eargp.argv_str.argv;
            pid = procSpawnCmd(runtime, argv, prog.toString(), eargp);
        }
        if (pid == -1) {
            context.setLastExitStatus(new RubyProcess.RubyStatus(runtime, runtime.getProcStatus(), 0x7f << 8, 0));
            if (errno == null || errno == Errno.__UNKNOWN_CONSTANT__) {
                errno = Errno.valueOf(runtime.getPosix().errno());
            }
        }

        execargRunOptions(context, runtime, sarg, null, errmsg);

        return pid;
    }

    // TODO: win32
//    #if defined(_WIN32)
//    #define proc_spawn_cmd_internal(argv, prog) rb_w32_uaspawn(P_NOWAIT, (prog), (argv))
//            #else
    long procSpawnCmdInternal(Ruby runtime, String[] argv, String prog, ExecArg eargp) {
        long status;

        if (prog == null)
            prog = argv[0];
        prog = dlnFindExeR(runtime, prog, eargp.path_env);
        if (prog == null) {
            errno = Errno.ENOENT;
            return -1;
        }

//        System.out.println(Arrays.asList(prog,
//                eargp.fileActions,
//                eargp.attributes,
//                Arrays.asList(argv),
//                eargp.envp_str == null ? Collections.EMPTY_LIST : Arrays.asList(eargp.envp_str)));
        // MRI does not do this check, but posix_spawn does not reliably ENOENT for bad filenames like ''
        if (prog == null || prog.length() == 0) {
            errno = Errno.ENOENT;
            return -1;
        }

        status = runtime.getPosix().posix_spawnp(
                prog,
                eargp.fileActions,
                eargp.attributes,
                Arrays.asList(argv),
                eargp.envp_str == null ? Collections.EMPTY_LIST : Arrays.asList(eargp.envp_str));
        if (status == -1) {
            if (runtime.getPosix().errno() == Errno.ENOEXEC.intValue()) {
                //String[] newArgv = new String[argv.length + 1];
                //newArgv[1] = prog;
                //newArgv[0] = "sh";
                status = runtime.getPosix().posix_spawnp(
                        "/bin/sh",
                        eargp.fileActions,
                        eargp.attributes,
                        Arrays.asList(argv),
                        eargp.envp_str == null ? Collections.EMPTY_LIST : Arrays.asList(eargp.envp_str));
                if (status == -1) errno = Errno.ENOEXEC;
            } else {
                errno = Errno.valueOf(runtime.getPosix().errno());
            }
        }
        return status;
    }


    long procSpawnCmd(Ruby runtime, String[] argv, String prog, ExecArg eargp) {
        long pid = -1;

        if (argv.length > 0 && argv[0] != null) {
            // TODO: win32
//            #if defined(_WIN32)
//            DWORD flags = 0;
//            if (eargp->new_pgroup_given && eargp->new_pgroup_flag) {
//                flags = CREATE_NEW_PROCESS_GROUP;
//            }
//            pid = rb_w32_uaspawn_flags(P_NOWAIT, prog ? RSTRING_PTR(prog) : 0, argv, flags);
//            #else
            pid = procSpawnCmdInternal(runtime, argv, prog, eargp);
        }
        return pid;
    }

    // TODO: win32 version
//    #if defined(_WIN32)
//    #define proc_spawn_sh(str) rb_w32_uspawn(P_NOWAIT, (str), 0)
//            #else
    long procSpawnSh(Ruby runtime, String str, ExecArg eargp) {
        long status;

        String shell = dlnFindExeR(runtime, "sh", eargp.path_env);

//        System.out.println("before: " + shell + ", fa=" + eargp.fileActions + ", a=" + eargp.attributes + ", argv=" + Arrays.asList("sh", "-c", str));
        status = runtime.getPosix().posix_spawnp(
                shell != null ? shell : "/bin/sh",
                eargp.fileActions,
                eargp.attributes,
                Arrays.asList("sh", "-c", str),
                eargp.envp_str == null ? Collections.EMPTY_LIST : Arrays.asList(eargp.envp_str));

        if (status == -1) errno = Errno.valueOf(runtime.getPosix().errno());

        return status;
    }

    // pipe_open_s
    public static IRubyObject pipeOpen(ThreadContext context, IRubyObject prog, String modestr, int fmode, IOEncodable convconfig) {
        IRubyObject[] argv = {prog};
        ExecArg execArg = null;

        if (!isPopenFork(context.runtime, (RubyString)prog))
            execArg = execargNew(context, argv, true);
        return new PopenExecutor().pipeOpen(context, execArg, modestr, fmode, convconfig);
    }

    // rb_io_s_popen
    public static IRubyObject popen(ThreadContext context, IRubyObject[] argv, RubyClass klass, Block block) {
        Ruby runtime = context.runtime;
        String modestr;
        IRubyObject pname, port, tmp, opt = context.nil, env = context.nil;
        Object pmode = EncodingUtils.vmodeVperm(null, null);
        ExecArg eargp;
        int[] oflags_p = {0}, fmode_p = {0};
        IOEncodable.ConvConfig convconfig = new IOEncodable.ConvConfig();
        int argc = argv.length;

        if (argc > 1 && !(opt = TypeConverter.checkHashType(runtime, argv[argc - 1])).isNil()) --argc;
        if (argc > 1 && !(env = TypeConverter.checkHashType(runtime, argv[0])).isNil()) {
            --argc;
            argv = Arrays.copyOfRange(argv, 1, argc + 1);
        }
        switch (argc) {
            case 2:
                EncodingUtils.vmode(pmode, argv[1]);
            case 1:
                pname = argv[0];
                break;
            default: {
                int ex = opt.isNil() ? 0 : 1;
                Arity.raiseArgumentError(runtime, argc + ex, 1 + ex, 2 + ex);
                return null; // not reached
            }
        }

        tmp = TypeConverter.checkArrayType(runtime, pname);
        if (!tmp.isNil()) {
//            int len = ((RubyArray)tmp).size();
//            #if SIZEOF_LONG > SIZEOF_INT
//            if (len > INT_MAX) {
//                throw runtime.newArgumentError("too many arguments");
//            }
//            #endif
            tmp = ((RubyArray)tmp).aryDup();
            //            RBASIC_CLEAR_CLASS(tmp);
            eargp = execargNew(context, ((RubyArray)tmp).toJavaArray(), false);
            ((RubyArray)tmp).clear();
        } else {
            pname = pname.convertToString();
            eargp = null;
            if (!isPopenFork(runtime, (RubyString)pname)) {
                IRubyObject[] pname_p = {pname};
                eargp = execargNew(context, pname_p, true);
                pname = pname_p[0];
            }
        }
        if (eargp != null) {
            if (!opt.isNil())
                opt = execargExtractOptions(context, runtime, eargp, (RubyHash)opt);
            if (!env.isNil())
                execargSetenv(context, runtime, eargp, env);
        }
        EncodingUtils.extractModeEncoding(context, convconfig, pmode, opt, oflags_p, fmode_p);
        modestr = OpenFile.ioOflagsModestr(runtime, oflags_p[0]);

        port = new PopenExecutor().pipeOpen(context, eargp, modestr, fmode_p[0], convconfig);
//        This is cleanup for failure to exec in the child.
//        if (port.isNil()) {
//            /* child */
//            if (rb_block_given_p()) {
//                rb_yield(Qnil);
//                rb_io_flush(rb_stdout);
//                rb_io_flush(rb_stderr);
//                _exit(0);
//            }
//            return Qnil;
//        }
        ((RubyBasicObject)port).setMetaClass(klass);
        return RubyIO.ensureYieldClose(context, port, block);
    }

    static void execargSetenv(ThreadContext context, Ruby runtime, ExecArg eargp, IRubyObject env) {
        eargp.env_modification = !env.isNil() ? checkExecEnv(context, (RubyHash)env, eargp) : null;
    }

    // MRI: rb_check_exec_env
    public static RubyArray checkExecEnv(ThreadContext context, RubyHash hash, ExecArg pathArg) {
        Ruby runtime = context.runtime;
        RubyArray env;

        env = runtime.newArray();
        for (Map.Entry<IRubyObject, IRubyObject> entry : (Set<Map.Entry<IRubyObject, IRubyObject>>)hash.directEntrySet()) {
            IRubyObject key = entry.getKey();
            IRubyObject val = entry.getValue();
            String k;

            k = StringSupport.checkEmbeddedNulls(runtime, key).toString();
            if (k.indexOf('=') != -1)
                throw runtime.newArgumentError("environment name contains a equal : " + k);

            if (!val.isNil())
                val = StringSupport.checkEmbeddedNulls(runtime, val);

            key = key.convertToString().export(context);
            if (!val.isNil()) val = val.convertToString().export(context);

            if (key.convertToString().toString().equalsIgnoreCase("PATH")) {
                pathArg.path_env = val;
            }

            env.push(runtime.newArray(key, val));
        }

        return env;
    }

    // MRI: execarg_extract_options
    static IRubyObject execargExtractOptions(ThreadContext context, Ruby runtime, ExecArg eargp, RubyHash opthash) {
        return handleOptionsCommon(context, runtime, eargp, opthash, false);
    }

    // MRI: check_exec_options
    static void checkExecOptions(ThreadContext context, Ruby runtime, RubyHash opthash, ExecArg eargp) {
        handleOptionsCommon(context, runtime, eargp, opthash, true);
    }

    static IRubyObject handleOptionsCommon(ThreadContext context, Ruby runtime, ExecArg eargp, RubyHash opthash, boolean raise) {
        if (opthash.isEmpty())
            return null;

        RubyHash nonopts = null;

        for (Map.Entry<IRubyObject, IRubyObject> entry : (Set<Map.Entry<IRubyObject, IRubyObject>>)opthash.directEntrySet()) {
            IRubyObject key = entry.getKey();
            IRubyObject val = entry.getValue();

            if (execargAddopt(context, runtime, eargp, key, val) != ST_CONTINUE) {
                if (raise) {
                    if (key instanceof RubySymbol) {
                        switch (key.toString()) {
                            case "gid" :
                                throw runtime.newNotImplementedError("popen does not support :gid option in JRuby");
                            case "uid" :
                                throw runtime.newNotImplementedError("popen does not support :uid option in JRuby");
                            default :
                                throw runtime.newArgumentError("wrong exec option symbol: " + key);
                        }
                    }
                    else {
                        throw runtime.newArgumentError("wrong exec option: " + key);
                    }
                }

                if (nonopts == null) nonopts = RubyHash.newHash(runtime);
                nonopts.op_aset(context, key, val);
            }
        }
        return nonopts != null ? nonopts : context.nil;
    }

    // MRI: is_popen_fork
    static boolean isPopenFork(Ruby runtime, RubyString prog) {
        if (prog.size() == 1 && prog.getByteList().get(0) == '-') {
            throw runtime.newNotImplementedError("fork() function is unimplemented on JRuby");
        }
        return false;
    }

    // MRI: DO_SPAWN macro in pipe_open
    private long DO_SPAWN(Ruby runtime, ExecArg eargp, String cmd, String[] args, String[] envp) {
        if (eargp.use_shell) {
            return procSpawnSh(runtime, eargp, cmd, envp);
        }

//        System.out.println(Arrays.asList(
//                cmd,
//                eargp.fileActions,
//                eargp.attributes,
//                args == null ? Collections.EMPTY_LIST : Arrays.asList(args),
//                envp == null ? Collections.EMPTY_LIST : Arrays.asList(envp)));
        // MRI does not do this check, but posix_spawn does not reliably ENOENT for bad filenames like ''
        if (cmd == null || cmd.length() == 0) {
            errno = Errno.ENOENT;
            return -1;
        }
        long ret = runtime.getPosix().posix_spawnp(
                cmd,
                eargp.fileActions,
                eargp.attributes,
                args == null ? Collections.EMPTY_LIST : Arrays.asList(args),
                envp == null ? Collections.EMPTY_LIST : Arrays.asList(envp));

        if (ret == -1) {
            errno = Errno.valueOf(runtime.getPosix().errno());
        }

        return ret;
    }

    // MRI: Basically doing sh processing from proc_exec_sh but for non-fork path
    private long procSpawnSh(Ruby runtime, ExecArg eargp, String str, String[] envp) {
        char[] sChars;
        int s = 0;

        sChars = str.toCharArray();
        while (s < sChars.length && (sChars[s] == ' ' || sChars[s] == '\t' || sChars[s] == '\n'))
            s++;

        if (s >= sChars.length) {
            errno = Errno.ENOENT;
            return -1;
        }

        // TODO: Windows
        if (Platform.IS_WINDOWS) { // #ifdef _WIN32
//            rb_w32_uspawn(P_OVERLAY, (char *)str, 0);
            return -1;
        } else {
//            #if defined(__CYGWIN32__) || defined(__EMX__)
//            {
//                char fbuf[MAXPATHLEN];
//                char *shell = dln_find_exe_r("sh", 0, fbuf, sizeof(fbuf));
//                int status = -1;
//                if (shell)
//                    execl(shell, "sh", "-c", str, (char *) NULL);
//                else
//                status = system(str);
//                if (status != -1)
//                    exit(status);
//            }
//            #else
            long ret = runtime.getPosix().posix_spawnp(
                    "/bin/sh",
                    eargp.fileActions,
                    eargp.attributes,
                    Arrays.asList("sh", "-c", str),
                    envp == null ? Collections.EMPTY_LIST : Arrays.asList(envp));

            if (ret == -1) {
                errno = Errno.valueOf(runtime.getPosix().errno());
            }

            return ret;
        }
    }

    private static class PopenArg {
        ExecArg eargp;
        int modef;
    }

    private static String[] ARGVSTR2ARGV(byte[][] argv_str) {
        String[] argv = new String[argv_str.length];
        for (int i = 0; i < argv_str.length; i++) {
            // FIXME: probably should be using a specific encoding
            if (argv_str[i] == null) continue; // placeholder for /bin/sh, but unsure where it's supposed to be added
            argv[i] = new String(argv_str[i]);
        }
        return argv;
    }

    private Errno errno = null;

    // MRI: pipe_open
    private RubyIO pipeOpen(ThreadContext context, ExecArg eargp, String modestr, int fmode, IOEncodable convconfig) {
        final Ruby runtime = context.runtime;
        IRubyObject prog = eargp != null ? (eargp.use_shell ? eargp.command_name : eargp.command_name) : null;
        long pid = 0;
        OpenFile fptr;
        RubyIO port;
        OpenFile write_fptr;
        IRubyObject write_port;
        PosixShim posix = new PosixShim(runtime);

        Errno e = null;

        String[] args = null;
        String[] envp = null;

        ExecArg sargp = new ExecArg();
        int fd;
        int write_fd = -1;
        String cmd = null;

        if (prog != null)
            cmd = StringSupport.checkEmbeddedNulls(runtime, prog).toString();

        if (eargp.chdir_given()) {
            // we can'd do chdir with posix_spawn, so we should be set to use_shell and now
            // just need to add chdir to the cmd
            cmd = "cd '" + eargp.chdir_dir + "'; " + cmd;
            eargp.chdir_dir = null;
            eargp.chdir_given_clear();
        }

        if (eargp != null && !eargp.use_shell) {
            args = eargp.argv_str.argv;
        }
        int[] pair = {-1,-1}, writePair = {-1, -1};
        switch (fmode & (OpenFile.READABLE|OpenFile.WRITABLE)) {
            case OpenFile.READABLE | OpenFile.WRITABLE:
                if (API.rb_pipe(runtime, writePair) == -1)
                    throw runtime.newErrnoFromErrno(posix.getErrno(), prog.toString());
                if (API.rb_pipe(runtime, pair) == -1) {
                    e = posix.getErrno();
                    runtime.getPosix().close(writePair[1]);
                    runtime.getPosix().close(writePair[0]);
                    posix.setErrno(e);
                    throw runtime.newErrnoFromErrno(posix.getErrno(), prog.toString());
                }

                if (eargp != null) prepareStdioRedirects(runtime, pair, writePair, eargp);

                break;
            case OpenFile.READABLE:
                if (API.rb_pipe(runtime, pair) == -1)
                    throw runtime.newErrnoFromErrno(posix.getErrno(), prog.toString());

                if (eargp != null) prepareStdioRedirects(runtime, pair, null, eargp);

                break;
            case OpenFile.WRITABLE:
                if (API.rb_pipe(runtime, pair) == -1)
                    throw runtime.newErrnoFromErrno(posix.getErrno(), prog.toString());

                if (eargp != null) prepareStdioRedirects(runtime, null, pair, eargp);

                break;
            default:
                throw runtime.newSystemCallError(prog.toString());
        }
        if (eargp != null) {
            try {
                execargFixup(context, runtime, eargp);
            } catch (RaiseException re) { // if (state)
                if (writePair[0] != -1) runtime.getPosix().close(writePair[0]);
                if (writePair[1] != -1) runtime.getPosix().close(writePair[1]);
                if (pair[0] != -1) runtime.getPosix().close(pair[0]);
                if (pair[1] != -1) runtime.getPosix().close(pair[1]);
                execargParentEnd(runtime, eargp);
                throw re;
            }
            execargRunOptions(context, runtime, eargp, sargp, null);
            if (eargp.envp_str != null) envp = eargp.envp_str;
            while ((pid = DO_SPAWN(runtime, eargp, cmd, args, envp)) == -1) {
	            /* exec failed */
                switch (e = errno) {
                    case EAGAIN:
                    case EWOULDBLOCK:
                        try {Thread.sleep(1000);} catch (InterruptedException ie) {}
                        continue;
                }
                break;
            }
            if (eargp != null)
                execargRunOptions(context, runtime, sargp, null, null);
            execargParentEnd(runtime, eargp);
        }
        else {
            throw runtime.newNotImplementedError("spawn without exec args (probably a bug)");
        }

        /* parent */
        if (pid == -1) {
            runtime.getPosix().close(pair[1]);
            runtime.getPosix().close(pair[0]);
            if ((fmode & (OpenFile.READABLE|OpenFile.WRITABLE)) == (OpenFile.READABLE|OpenFile.WRITABLE)) {
                runtime.getPosix().close(pair[1]);
                runtime.getPosix().close(pair[0]);
            }
            errno = e;
            throw runtime.newErrnoFromErrno(errno, prog.toString());
        }
        if ((fmode & OpenFile.READABLE) != 0 && (fmode & OpenFile.WRITABLE) != 0) {
            runtime.getPosix().close(pair[1]);
            fd = pair[0];
            runtime.getPosix().close(writePair[0]);
            write_fd = writePair[1];
        }
        else if ((fmode & OpenFile.READABLE) != 0) {
            runtime.getPosix().close(pair[1]);
            fd = pair[0];
        }
        else {
            runtime.getPosix().close(pair[0]);
            fd = pair[1];
        }

        port = (RubyIO) runtime.getIO().allocate();
        fptr = port.MakeOpenFile();
        fptr.setChannel(new NativeDeviceChannel(fd));
        fptr.setMode(fmode | (OpenFile.SYNC|OpenFile.DUPLEX));
        if (convconfig != null) {
            fptr.encs.copy(convconfig);
            if (Platform.IS_WINDOWS) { // #if defined(RUBY_TEST_CRLF_ENVIRONMENT) || defined(_WIN32)
                if ((fptr.encs.ecflags & EncodingUtils.ECONV_DEFAULT_NEWLINE_DECORATOR) != 0) {
                    fptr.encs.ecflags |= EConvFlags.UNIVERSAL_NEWLINE_DECORATOR;
                }
            }
        }
        else {
            if (fptr.NEED_NEWLINE_DECORATOR_ON_READ()) {
                fptr.encs.ecflags |= EConvFlags.UNIVERSAL_NEWLINE_DECORATOR;
            }
            if (EncodingUtils.TEXTMODE_NEWLINE_DECORATOR_ON_WRITE != 0) {
                if (fptr.NEED_NEWLINE_DECORATOR_ON_WRITE()) {
                    fptr.encs.ecflags |= EncodingUtils.TEXTMODE_NEWLINE_DECORATOR_ON_WRITE;
                }
            }
        }
        final long finalPid = pid;
        fptr.setPid(pid);
        fptr.setProcess(new POSIXProcess(runtime, finalPid));

        if (write_fd != -1) {
            write_port = runtime.getIO().allocate();
            write_fptr = ((RubyIO)write_port).MakeOpenFile();
            write_fptr.setChannel(new NativeDeviceChannel(write_fd));
            write_fptr.setMode((fmode & ~OpenFile.READABLE)| OpenFile.SYNC|OpenFile.DUPLEX);
            fptr.setMode(fptr.getMode() & ~OpenFile.WRITABLE);
            fptr.tiedIOForWriting = (RubyIO)write_port;
            port.setInstanceVariable("@tied_io_for_writing", write_port);
        }

//        fptr.setFinalizer(fptr.PIPE_FINALIZE);

        // TODO?
//        pipeAddFptr(fptr);
        return port;
    }

    private void prepareStdioRedirects(Ruby runtime, int[] readPipe, int[] writePipe, ExecArg eargp) {
        // We insert these redirects directly into fd_dup2 so that chained redirection can be
        // validated and set up properly by the execargFixup logic.
        // The closes do not appear to be part of MRI's logic (they close the fd before exec/spawn),
        // so rather than using execargAddopt we do them directly here.

        if (readPipe != null) {
            // dup our read pipe's write end into stdout
            int readPipeWriteFD = readPipe[1];
            eargp.fd_dup2 = checkExecRedirect1(runtime, eargp.fd_dup2, runtime.newFixnum(1), runtime.newFixnum(readPipeWriteFD));

            // close the other end of the pipe in the child
            int readPipeReadFD = readPipe[0];
            eargp.fileActions.add(SpawnFileAction.close(readPipeReadFD));
        }

        if (writePipe != null) {
            // dup our write pipe's read end into stdin
            int writePipeReadFD = writePipe[0];
            eargp.fd_dup2 = checkExecRedirect1(runtime, eargp.fd_dup2, runtime.newFixnum(0), runtime.newFixnum(writePipeReadFD));

            // close the other end of the pipe in the child
            int writePipeWriteFD = writePipe[1];
            eargp.fileActions.add(SpawnFileAction.close(writePipeWriteFD));
        }
    }

    static int run_exec_pgroup(Ruby runtime, ExecArg eargp, ExecArg sargp, String[] errmsg) {
        /*
         * If FD_CLOEXEC is available, rb_fork waits the child's execve.
         * So setpgid is done in the child when rb_fork is returned in the parent.
         * No race condition, even without setpgid from the parent.
         * (Is there an environment which has setpgid but no FD_CLOEXEC?)
         */
        int ret = 0;
        long pgroup;

        pgroup = eargp.pgroup_pgid;
        if (pgroup == -1) {
            // inherit parent's process group (default behavior)
            return ret;
        }

        eargp.attributes.add(SpawnAttribute.pgroup(pgroup));
        eargp.attributes.add(SpawnAttribute.flags((short)SpawnAttribute.SETPGROUP));

        return ret;
    }

    static int run_exec_rlimit(Ruby runtime, RubyArray ary, ExecArg sargp, String[] errmsg) {
        throw runtime.newNotImplementedError("changing rlimit in child is not supported");
        /* Not supported by posix_spawn
        long i;
        for (i = 0; i < ary.size(); i++) {
            IRubyObject elt = ary.eltOk(i);
            int rtype = RubyNumeric.num2int(((RubyArray)elt).eltOk(0));

            struct rlimit rlim;
            if (sargp != null) {
                IRubyObject tmp, newary;
                if (runtime.getPosix().getrlimit(rtype, &rlim) == -1) {
                    if (errmsg != null) errmsg[0] = "getrlimit";
                    return -1;
                }
                tmp = runtime.newArray(((RubyArray)elt).eltOk(0),
                        runtime.newFixnum(rlim.rlim_cur),
                        runtime.newFixnum(rlim.rlim_max));
                if (sargp.rlimit_limits == null)
                    newary = sargp.rlimit_limits = runtime.newArray();
                else
                    newary = sargp.rlimit_limits;
                ((RubyArray)newary).push(tmp);
            }
            rlim.rlim_cur = RubyNumeric.num2int(((RubyArray)elt).eltOk(1));
            rlim.rlim_max = RubyNumeric.num2int(((RubyArray)elt).eltOk(2));
            */

            // we can't setrlimit in parent
//            if (runtime.getPosix().setrlimit(rtype, &rlim) == -1) { /* hopefully async-signal-safe */
//                if (errmsg != null) errmsg[0] = "setrlimit";
//                return -1;
//            }
//        }
//        return 0;
    }

    static void saveEnv(ThreadContext context, Ruby runtime, ExecArg sargp) {
        // We don't need to save env in parent because we let posix_spawn set it up
//        if (sargp == null)
//            return;
//        if (sargp.env_modification == null) {
//            RubyHash env = runtime.getENV();
//            if (!env.isNil()) {
//                final RubyArray ary = runtime.newArray();
//                BlockCallback SaveEnvBody = new BlockCallback() {
//                    @Override
//                    public IRubyObject call(ThreadContext context, IRubyObject[] args, Block block) {
//                        ary.push(args[0].dup());
//                        return context.nil;
//                    }
//                };
//                env.each(context, CallBlock.newCallClosure(env, runtime.getHash(),
//                        Arity.OPTIONAL, SaveEnvBody, context));
//                sargp.env_modification = ary;
//            }
//            sargp.unsetenv_others_given_set();
//            sargp.unsetenv_others_do_set();
//        }
    }

    static int run_exec_dup2(Ruby runtime, RubyArray ary, ExecArg eargp, ExecArg sargp, String[] errmsg) {
        int n, i;
        int ret;
        int extra_fd = -1;
        run_exec_dup2_fd_pair[] pairs = eargp.dup2_tmpbuf;

        n = ary.size();

        /* initialize oldfd and newfd: O(n) */
        for (i = 0; i < n; i++) {
            IRubyObject elt = ary.eltOk(i);
            pairs[i].oldfd = RubyNumeric.fix2int(((RubyArray)elt).eltOk(1));
            pairs[i].newfd = RubyNumeric.fix2int(((RubyArray)elt).eltOk(0)); /* unique */
            pairs[i].older_index = -1;
        }

        /* sort the table by oldfd: O(n log n) */
        if (sargp == null)
            Arrays.sort(pairs, intcmp); /* hopefully async-signal-safe */
        else
            Arrays.sort(pairs, intrcmp);

        /* initialize older_index and num_newer: O(n log n) */
        for (i = 0; i < n; i++) {
            int newfd = pairs[i].newfd;
            run_exec_dup2_fd_pair key = new run_exec_dup2_fd_pair();
            key.oldfd = newfd;
            int found = Arrays.binarySearch(pairs, key, intcmp); /* hopefully async-signal-safe */
            pairs[i].num_newer = 0;
            if (found >= 0) {
                while (found > 0 && pairs[found-1].oldfd == newfd) found--;
                while (found < n && pairs[found].oldfd == newfd) {
                    pairs[i].num_newer++;
                    pairs[found].older_index = i;
                    found++;
                }
            }
        }

        /* non-cyclic redirection: O(n) */
        for (i = 0; i < n; i++) {
            int j = i;
            while (j != -1 && pairs[j].oldfd != -1 && pairs[j].num_newer == 0) {
                if (saveRedirectFd(runtime, pairs[j].newfd, sargp, errmsg) < 0) /* async-signal-safe */
                    return -1;

                // This always succeeds because we just defer it to posix_spawn.
                redirectDup2(eargp, pairs[j].oldfd, pairs[j].newfd); /* async-signal-safe */
                pairs[j].oldfd = -1;
                j = (int) pairs[j].older_index;
                if (j != -1) pairs[j].num_newer--;
            }
        }

        /* cyclic redirection: O(n) */
        for (i = 0; i < n; i++) {
            int j;
            if (pairs[i].oldfd == -1)
                continue;
            if (pairs[i].oldfd == pairs[i].newfd) { /* self cycle */
                int fd = pairs[i].oldfd;
                ret = runtime.getPosix().fcntl(fd, Fcntl.F_GETFD); /* async-signal-safe */
                if (ret == -1) {
                    if (errmsg != null) errmsg[0] = "fcntl(F_GETFD)";
                    return -1;
                }
                if ((ret & FcntlLibrary.FD_CLOEXEC) != 0) {
                    ret &= ~FcntlLibrary.FD_CLOEXEC;
                    ret = runtime.getPosix().fcntlInt(fd, Fcntl.F_SETFD, ret); /* async-signal-safe */
                    if (ret == -1) {
                        if (errmsg != null) errmsg[0] = "fcntl(F_SETFD)";
                        return -1;
                    }
                }
                pairs[i].oldfd = -1;
                continue;
            }
            if (extra_fd == -1) {
                extra_fd = redirectDup(runtime, pairs[i].oldfd); /* async-signal-safe */
                if (extra_fd == -1) {
                    if (errmsg != null) errmsg[0] = "dup";
                    return -1;
                }
//                rb_update_max_fd(extra_fd);
            }
            else {
                // This always succeeds because we just defer it to posix_spawn.
                redirectDup2(eargp, pairs[i].oldfd, extra_fd); /* async-signal-safe */
            }
            pairs[i].oldfd = extra_fd;
            j = pairs[i].older_index;
            pairs[i].older_index = -1;
            while (j != -1) {
                // This always succeeds because we just defer it to posix_spawn.
                redirectDup2(eargp, pairs[j].oldfd, pairs[j].newfd); /* async-signal-safe */
                pairs[j].oldfd = -1;
                j = pairs[j].older_index;
            }
        }
        if (extra_fd != -1) {
            ret = redirectClose(runtime, eargp, extra_fd, sargp != null); /* async-signal-safe */
            if (ret == -1) {
                if (errmsg != null) errmsg[0] = "close";
                return -1;
            }
        }

        return 0;
    }

    static int redirectDup(Ruby runtime, int oldfd)
    {
        // Partial impl of rb_cloexec_fcntl_dup
        int ret;
        ret = runtime.getPosix().dup(oldfd);
        int flags = runtime.getPosix().fcntl(ret, Fcntl.F_GETFD);
        runtime.getPosix().fcntlInt(ret, Fcntl.F_SETFD, flags | FcntlLibrary.FD_CLOEXEC);
        return ret;
    }

    static int redirectCloexecDup(Ruby runtime, int oldfd)
    {
        int ret = redirectDup(runtime, oldfd);

        int flags = runtime.getPosix().fcntl(ret, Fcntl.F_GETFD);
        runtime.getPosix().fcntlInt(ret, Fcntl.F_SETFD, flags | FcntlLibrary.FD_CLOEXEC);

        return ret;
    }

    static void redirectDup2(ExecArg eargp, int oldfd, int newfd)
    {
        eargp.fileActions.add(SpawnFileAction.dup(oldfd, newfd));
    }

    static int redirectClose(Ruby runtime, ExecArg eargp, int fd, boolean forChild)
    {
        if (forChild) {
            eargp.fileActions.add(SpawnFileAction.close(fd));
            return 0;
        } else {
            return runtime.getPosix().close(fd);
        }
    }

    static void redirectOpen(ExecArg eargp, int fd, String pathname, int flags, int perm)
    {
        eargp.fileActions.add(SpawnFileAction.open(pathname, fd, flags, perm));
    }

    static int saveRedirectFd(Ruby runtime, int fd, ExecArg sargp, String[] errmsg) {
        // This logic is to restore the parent's fd. Since we let posix_spawn do dup2 for us in the
        // child, it's not necessary for us to fix up the parent.

        if (false && sargp != null) {
            RubyArray newary;
            int save_fd = redirectCloexecDup(runtime, fd);
            if (save_fd == -1) {
                if (runtime.getPosix().errno() == Errno.EBADF.intValue())
                    return 0;
                if (errmsg != null) errmsg[0] = "dup";
                return -1;
            }
//            rb_update_max_fd(save_fd);

            newary = sargp.fd_dup2;
            if (newary == null) {
                newary = runtime.newArray();
                sargp.fd_dup2 = newary;
            }
            newary.push(runtime.newArray(runtime.newFixnum(fd), runtime.newFixnum(save_fd)));

            newary = sargp.fd_close;
            if (newary == null) {
                newary = runtime.newArray();
                sargp.fd_close = newary;
            }
            newary.push(runtime.newArray(runtime.newFixnum(save_fd), runtime.getNil()));
        }

        return 0;
    }

    int execargRunOptions(ThreadContext context, Ruby runtime, ExecArg eargp, ExecArg sargp, String[] errmsg) {
        IRubyObject obj;

        if (sargp != null) {
            /* assume that sargp is always NULL on fork-able environments */
            sargp.redirect_fds = context.nil;
        }

//        #ifdef HAVE_SETPGID
        if (eargp.pgroup_given()) {
            if (run_exec_pgroup(runtime, eargp, sargp, errmsg) == -1) /* async-signal-safe */
                return -1;
        }
//        #endif

//        #if defined(HAVE_SETRLIMIT) && defined(RLIM2NUM)
        obj = eargp.rlimit_limits;
        if (obj != null) {
            throw runtime.newNotImplementedError("setting rlimit in child is unsupported");
//            if (run_exec_rlimit(runtime, (RubyArray)obj, sargp, errmsg) == -1) /* hopefully async-signal-safe */
//                return -1;
        }
//        #endif

//        #if !defined(HAVE_FORK)
        boolean clearEnv = false;
        if (eargp.unsetenv_others_given() && eargp.unsetenv_others_do()) {
            // only way to do this is manually build a list of env assignments that clear all parent values
            throw runtime.newNotImplementedError("clearing env in child is not supported");
//            saveEnv(context, runtime, sargp);

            // we can't clear env in parent process
//            runtime.getENV().clear();
        }

        RubyArray env = eargp.env_modification;
        if (env != null) {
            eargp.envp_str = ShellLauncher.getModifiedEnv(runtime, env, clearEnv);
        }
//        #endif

        if (eargp.umask_given()) {
            throw runtime.newNotImplementedError("setting umask in child is unsupported");
//            int mask = eargp.umask_mask;
//            SpawnAttribute.
//            int oldmask = runtime.getPosix().umask(mask); /* never fail */ /* async-signal-safe */
//            if (sargp != null) {
//                sargp.umask_given_set();
//                sargp.umask_mask = oldmask;
//            }
        }

        obj = eargp.fd_dup2;
        if (obj != null) {
            if (run_exec_dup2(runtime, (RubyArray)obj, eargp, sargp, errmsg) == -1) /* hopefully async-signal-safe */
                return -1;
        }

        obj = eargp.fd_close;
        if (obj != null) {
            if (sargp != null)
                runtime.getWarnings().warn("cannot close fd before spawn");
            else {
                if (run_exec_close(runtime, (RubyArray)obj, eargp, errmsg) == -1) /* async-signal-safe */
                    return -1;
            }
        }

        obj = eargp.fd_dup2_child;
        if (obj != null) {
            if (run_exec_dup2_child(runtime, (RubyArray)obj, eargp, sargp, errmsg) == -1) /* async-signal-safe */
                return -1;
        }

        if (eargp.chdir_given()) {
            // should have been set up in pipe_open, so we just raise here
            throw new RuntimeException("BUG: chdir not supported in posix_spawn; should have been made into chdir");
            // we can't chdir in the parent
//            if (sargp != null) {
//                String cwd = runtime.getCurrentDirectory();
//                sargp.chdir_given_set();
//                sargp.chdir_dir = cwd;
//            }
//            if (chdir(RSTRING_PTR(eargp.chdir_dir)) == -1) { /* async-signal-safe */
//                ERRMSG("chdir");
//                return -1;
//            }
        }

//        #ifdef HAVE_SETGID
        if (eargp.gid_given()) {
            throw runtime.newNotImplementedError("setgid in the child is not supported");
            // we can't setgid in the parent
//            if (setgid(eargp.gid) < 0) {
//                ERRMSG("setgid");
//                return -1;
//            }
        }
//        #endif
//        #ifdef HAVE_SETUID
        if (eargp.uid_given()) {
            throw runtime.newNotImplementedError("setuid in the child is not supported");
            // we can't setuid in the parent
//            if (setuid(eargp.uid) < 0) {
//                ERRMSG("setuid");
//                return -1;
//            }
        }
//        #endif

//        if (sargp != null) {
//            IRubyObject ary = sargp.fd_dup2;
//            if (ary != null) {
//                int len = runExecDup2TmpbufSize(((RubyArray)ary).size());
//                run_exec_dup2_fd_pair[] tmpbuf = new run_exec_dup2_fd_pair[len];
//                for (int i = 0; i < tmpbuf.length; i++) tmpbuf[i] = new run_exec_dup2_fd_pair();
//                sargp.dup2_tmpbuf = tmpbuf;
//            }
//        }

        return 0;
    }

    /* This function should be async-signal-safe.  Actually it is. */
    static int run_exec_close(Ruby runtime, RubyArray ary, ExecArg eargp, String[] errmsg) {
        long i;
        int ret;

        for (i = 0; i < ary.size(); i++) {
            RubyArray elt = (RubyArray)ary.eltOk(i);
            int fd = RubyNumeric.fix2int(elt.eltOk(0));
            ret = redirectClose(runtime, eargp, fd, true); /* async-signal-safe */
            if (ret == -1) {
                if (errmsg != null) errmsg[0] = "close";
                return -1;
            }
        }
        return 0;
    }

    /* This function should be async-signal-safe when sargp is NULL.  Actually it is. */
    static int run_exec_dup2_child(Ruby runtime, RubyArray ary, ExecArg eargp, ExecArg sargp, String[] errmsg) {
        long i;
        int ret;

        for (i = 0; i < ary.size(); i++) {
            RubyArray elt = (RubyArray)ary.eltOk(i);
            int newfd = RubyNumeric.fix2int(elt.eltOk(0));
            int oldfd = RubyNumeric.fix2int(elt.eltOk(1));

            // Don't have to save in parent, since we let posix_spawn dup2
//            if (saveRedirectFd(runtime, newfd, sargp, errmsg) < 0) /* async-signal-safe */
//                return -1;

            // This always succeeds
            redirectDup2(eargp, oldfd, newfd); /* async-signal-safe */
//            rb_update_max_fd(newfd);
        }
        return 0;
    }

    private static class run_exec_dup2_fd_pair {
        int oldfd;
        int newfd;
        int older_index;
        int num_newer;
    };

    static int runExecDup2TmpbufSize(int n) {
        return n;
    }

    static void execargFixup(ThreadContext context, Ruby runtime, ExecArg eargp) {
        execargParentStart(context, runtime, eargp);
    }

    static void execargParentStart(ThreadContext context, Ruby runtime, ExecArg eargp) {
        try {
            execargParentStart1(context, runtime, eargp);
        } catch (RaiseException re) {
            execargParentEnd(runtime, eargp);
            throw re;
        }
    }

    static void execargParentStart1(ThreadContext context, Ruby runtime, ExecArg eargp) {
        boolean unsetenv_others;
        RubyArray envopts;
        RubyArray<RubyArray> ary;

        eargp.redirect_fds = checkExecFds(context, runtime, eargp);

        ary = eargp.fd_open;
        if (ary != null) {
            long i;
            for (i = 0; i < ary.size(); i++) {
                RubyArray<RubyArray> elt = ary.eltOk(i);
                int fd = RubyNumeric.fix2int(elt.eltOk(0));
                RubyArray param = elt.eltOk(1);
                IRubyObject vpath = param.eltOk(0);
                int flags = RubyNumeric.num2int(param.eltOk(1));
                int perm = RubyNumeric.num2int(param.eltOk(2));
                IRubyObject fd2v = param.entry(3);
                int fd2;
                if (fd2v.isNil()) {
                    RubyIO.Sysopen open_data = new RubyIO.Sysopen();
                    vpath = RubyFile.get_path(context, vpath);
                    // TODO
//                    vpath = rb_str_encode_ospath(vpath);
                    while (true) {
                        open_data.fname = vpath.toString();
                        open_data.oflags = flags;
                        open_data.perm = perm;
                        ChannelFD ret;
                        open_data.errno = Errno.EINTR;
                        ret = open_func(runtime, open_data);
//                    rb_thread_call_without_gvl2(open_func, (void *)&open_data, RUBY_UBF_IO, 0);
                        if (ret == null) {
                            if (open_data.errno == Errno.EINTR) {
                                context.pollThreadEvents();
                                continue;
                            }
                            runtime.newErrnoFromInt(open_data.errno.intValue(), vpath.toString());
                        }
                        // We're in the fully-native process logic, so this should be a native stream
                        fd2 = ((ChannelFD) ret).realFileno;
//                        rb_update_max_fd(fd2);
                        param.store(3, runtime.newFixnum(fd2));
                        context.pollThreadEvents();
                        break;
                    }
                }
                else {
                    fd2 = RubyNumeric.num2int(fd2v);
                }
                execargAddopt(context, runtime, eargp, runtime.newFixnum(fd), runtime.newFixnum(fd2));
            }
        }

        ary = eargp.fd_dup2;
        if (ary != null) {
            int len = runExecDup2TmpbufSize(ary.size());
            run_exec_dup2_fd_pair[] tmpbuf = new run_exec_dup2_fd_pair[len];
            for (int i = 0; i < tmpbuf.length; i++) tmpbuf[i] = new run_exec_dup2_fd_pair();
            eargp.dup2_tmpbuf = tmpbuf;
        }

        IRubyObject envtbl;
        unsetenv_others = eargp.unsetenv_others_given() && eargp.unsetenv_others_do();
        envopts = eargp.env_modification;
        if (unsetenv_others || envopts != null) {
            if (unsetenv_others) {
                envtbl = RubyHash.newHash(runtime);
            }
            else {
                envtbl = runtime.getObject().getConstant("ENV");
                envtbl = TypeConverter.convertToType(envtbl, runtime.getHash(), "to_hash").dup();
            }
            if (envopts != null) {
                RubyHash stenv = (RubyHash)envtbl;
                long i;
                for (i = 0; i < envopts.size(); i++) {
                    IRubyObject pair = envopts.eltOk(i);
                    IRubyObject key = ((RubyArray)pair).eltOk(0);
                    IRubyObject val = ((RubyArray)pair).eltOk(1);
                    if (val.isNil()) {
                        IRubyObject stkey = key;
                        stenv.fastDelete(stkey);
                    }
                    else {
                        stenv.op_aset(context, key, val);
                    }
                }
            }
        } else {
            // In MRI, they use the current env as the baseline because they fork+exec. We can't do that,
            // and posix_spawn needs a full env, so we pass even unmodified env through.
            envtbl = runtime.getObject().getConstant("ENV");
            envtbl = TypeConverter.convertToType(envtbl, runtime.getHash(), "to_hash");
        }
        buildEnvp(runtime, eargp, envtbl);
//        RB_GC_GUARD(execarg_obj);
    }

    static void execargParentEnd(Ruby runtime, ExecArg eargp) {
        int err = runtime.getPosix().errno();
        RubyArray<RubyArray> ary;

        ary = eargp.fd_open;
        if (ary != null) {
            long i;
            for (i = 0; i < ary.size(); i++) {
                RubyArray<RubyArray> elt = ary.eltOk(i);
                RubyArray param = elt.eltOk(1);
                IRubyObject fd2v;
                int fd2;
                fd2v = param.entry(3);
                if (!fd2v.isNil()) {
                    fd2 = RubyNumeric.fix2int(fd2v);
                    parentRedirectClose(runtime, fd2);
                    param.store(3, runtime.getNil());
                }
            }
        }

        runtime.getPosix().errno(err);
    }

    static ChannelFD open_func(Ruby runtime, RubyIO.Sysopen data) {
        ChannelFD ret = parentRedirectOpen(runtime, data);
        data.errno = Errno.valueOf(runtime.getPosix().errno());
        return ret;
    }

    static ChannelFD parentRedirectOpen(Ruby runtime, RubyIO.Sysopen data) {
        return RubyIO.cloexecOpen(runtime, data);
    }

    static void parentRedirectClose(Ruby runtime, int fd) {
        // close_unless_reserved
        if (fd > 2) runtime.getPosix().close(fd);
    }

    private static void buildEnvp(Ruby runtime, ExecArg eargp, IRubyObject envtbl) {
        String[] envp_str;
        List<String> envp_buf;
        envp_buf = new ArrayList();
        for (Map.Entry<IRubyObject, IRubyObject> entry : (Set<Map.Entry<IRubyObject, IRubyObject>>)((RubyHash)envtbl).directEntrySet()) {
            IRubyObject key = entry.getKey();
            IRubyObject val = entry.getValue();

            envp_buf.add(StringSupport.checkEmbeddedNulls(runtime, key).toString()
                    + "="
                    + StringSupport.checkEmbeddedNulls(runtime, val));
        }
        envp_str = new String[envp_buf.size()];
        envp_buf.toArray(envp_str);
        eargp.envp_str = envp_str;
        eargp.envp_buf = envp_buf;
    }

    static int checkExecFds1(ThreadContext context, Ruby runtime, ExecArg eargp, RubyHash h, int maxhint, IRubyObject ary) {
        long i;

        if (ary != null) {
            for (i = 0; i < ((RubyArray)ary).size(); i++) {
                IRubyObject elt = ((RubyArray)ary).eltOk(i);
                int fd = RubyNumeric.fix2int(((RubyArray)elt).eltOk(0));
                if (h.fastARef(runtime.newFixnum(fd)) != null) {
                    throw runtime.newArgumentError("fd " + fd + " specified twice");
                }
                if (ary == eargp.fd_open || ary == eargp.fd_dup2)
                    h.op_aset(context, runtime.newFixnum(fd), runtime.getTrue());
                else if (ary == eargp.fd_dup2_child)
                    h.op_aset(context, runtime.newFixnum(fd), ((RubyArray)elt).eltOk(1));
                else /* ary == eargp.fd_close */
                    h.op_aset(context, runtime.newFixnum(fd), runtime.newFixnum(-1));
                if (maxhint < fd)
                    maxhint = fd;
                if (ary == eargp.fd_dup2 || ary == eargp.fd_dup2_child) {
                    fd = RubyNumeric.fix2int(((RubyArray)elt).eltOk(1));
                    if (maxhint < fd)
                        maxhint = fd;
                }
            }
        }
        return maxhint;
    }

    static IRubyObject checkExecFds(ThreadContext context, Ruby runtime, ExecArg eargp) {
        RubyHash h = RubyHash.newHash(runtime);
        IRubyObject ary;
        int maxhint = -1;
        long i;

        maxhint = checkExecFds1(context, runtime, eargp, h, maxhint, eargp.fd_dup2);
        maxhint = checkExecFds1(context, runtime, eargp, h, maxhint, eargp.fd_close);
        maxhint = checkExecFds1(context, runtime, eargp, h, maxhint, eargp.fd_open);
        maxhint = checkExecFds1(context, runtime, eargp, h, maxhint, eargp.fd_dup2_child);

        if (eargp.fd_dup2_child != null) {
            ary = eargp.fd_dup2_child;
            for (i = 0; i < ((RubyArray)ary).size(); i++) {
                IRubyObject elt = ((RubyArray)ary).eltOk(i);
                int newfd = RubyNumeric.fix2int(((RubyArray)elt).eltOk(0));
                int oldfd = RubyNumeric.fix2int(((RubyArray)elt).eltOk(1));
                int lastfd = oldfd;
                IRubyObject val = h.fastARef(runtime.newFixnum(lastfd));
                long depth = 0;
                while (val instanceof RubyFixnum && 0 <= ((RubyFixnum)val).getIntValue()) {
                    lastfd = RubyNumeric.fix2int(val);
                    val = h.fastARef(val);
                    if (((RubyArray)ary).size() < depth)
                        throw runtime.newArgumentError("cyclic child fd redirection from " + oldfd);
                    depth++;
                }
                if (val != runtime.getTrue())
                    throw runtime.newArgumentError("child fd " + oldfd + " is not redirected");
                if (oldfd != lastfd) {
                    IRubyObject val2;
                    ((RubyArray)elt).store(1, runtime.newFixnum(lastfd));
                    h.op_aset(context, runtime.newFixnum(newfd), runtime.newFixnum(lastfd));
                    val = runtime.newFixnum(oldfd);
                    while ((val2 = h.fastARef(val)) instanceof RubyFixnum) {
                        h.op_aset(context, val, runtime.newFixnum(lastfd));
                        val = val2;
                    }
                }
            }
        }

        eargp.close_others_maxhint = maxhint;
        return h;
    }

    static int execargAddopt(ThreadContext context, Ruby runtime, ExecArg eargp, IRubyObject key, IRubyObject val) {
        String id;
//        #if defined(HAVE_SETRLIMIT) && defined(NUM2RLIM)
        int rtype;
//        #endif

//        rb_secure(2);

        boolean redirect = false;
        switch (key.getType().getClassIndex()) {
            case SYMBOL:
                id = key.toString();
//                #ifdef HAVE_SETPGID
                if (id.equals("pgroup")) {
                    long pgroup;
                    if (eargp.pgroup_given()) {
                        throw runtime.newArgumentError("pgroup option specified twice");
                    }
                    if (val == null || !val.isTrue())
                        pgroup = -1; /* asis(-1) means "don't call setpgid()". */
                    else if (val == runtime.getTrue())
                        pgroup = 0; /* new process group. */
                    else {
                        pgroup = val.convertToInteger().getLongValue();
                        if (pgroup < 0) {
                            throw runtime.newArgumentError("negative process group symbol : " + pgroup);
                        }
                    }
                    eargp.pgroup_given_set();
                    eargp.pgroup_pgid = pgroup;
                }
                else
//                #ifdef _WIN32
//                if (id.equals("new_pgroup")) {
//                    if (eargp.new_pgroup_given) {
//                        throw runtime.newArgumentError("new_pgroup option specified twice");
//                    }
//                    eargp.new_pgroup_given = 1;
//                    eargp.new_pgroup_flag = RTEST(val) ? 1 : 0;
//                }
//                else
//                #endif
//                #if defined(HAVE_SETRLIMIT) && defined(NUM2RLIM)
                if (id.startsWith("rlimit_") &&  // TODO
                        false) {
//                        (rtype = rlimitTypeByLname(id.substring(7)) != -1)) {
                    IRubyObject ary = eargp.rlimit_limits;
                    IRubyObject tmp, softlim, hardlim;
                    if (eargp.rlimit_limits == null)
                        ary = eargp.rlimit_limits = runtime.newArray();
                    else
                        ary = eargp.rlimit_limits;
                    tmp = TypeConverter.checkArrayType(runtime, val);
                    if (!tmp.isNil()) {
                        if (((RubyArray)tmp).size() == 1)
                            softlim = hardlim = ((RubyArray)tmp).eltOk(0).convertToInteger();
                        else if (((RubyArray)tmp).size() == 2) {
                            softlim = ((RubyArray)tmp).eltOk(0).convertToInteger();
                            hardlim = ((RubyArray)tmp).eltOk(1).convertToInteger();
                        }
                        else {
                            throw runtime.newArgumentError("wrong exec rlimit option");
                        }
                    }
                    else {
                        softlim = hardlim = val.convertToInteger();
                    }
                    tmp = RubyArray.newArray(runtime, runtime.newFixnum(rtype), softlim, hardlim);
                    ((RubyArray)ary).push(tmp);
                }
                else
//                #endif
                if (id.equals("unsetenv_others")) {
                    if (eargp.unsetenv_others_given()) {
                        throw runtime.newArgumentError("unsetenv_others option specified twice");
                    }
                    eargp.unsetenv_others_given_set();
                    if (!val.isNil()) {
                        eargp.unsetenv_others_do_set();
                    } else {
                        eargp.unsetenv_others_do_clear();
                    }
                }
                else if (id.equals("chdir")) {
                    if (eargp.chdir_given()) {
                        throw runtime.newArgumentError("chdir option specified twice");
                    }
                    RubyString valTmp = RubyFile.get_path(context, val);
                    eargp.chdir_given_set();
                    eargp.chdir_dir = valTmp.toString();
                }
                else if (id.equals("umask")) {
                    int cmask = val.convertToInteger().getIntValue();
                    if (eargp.umask_given()) {
                        throw runtime.newArgumentError("umask option specified twice");
                    }
                    eargp.umask_given_set();
                    eargp.umask_mask = cmask;
                }
                else if (id.equals("close_others")) {
                    if (eargp.close_others_given()) {
                        throw runtime.newArgumentError("close_others option specified twice");
                    }
                    eargp.close_others_given_set();
                    if (!val.isNil()) {
                        eargp.close_others_do_set();
                    } else {
                        eargp.close_others_do_clear();
                    }
                }
                else if (id.equals("in")) {
                    key = RubyFixnum.zero(runtime);
                    checkExecRedirect(context, runtime, key, val, eargp);
                }
                else if (id.equals("out")) {
                    key = RubyFixnum.one(runtime);
                    checkExecRedirect(context, runtime, key, val, eargp);
                }
                else if (id.equals("err")) {
                    key = RubyFixnum.two(runtime);
                    checkExecRedirect(context, runtime, key, val, eargp);
                }
                else if (id.equals("uid") && false) { // TODO
//                    #ifdef HAVE_SETUID
                    if (eargp.uid_given()) {
                        throw runtime.newArgumentError("uid option specified twice");
                    }
//                    checkUidSwitch();
                    {
//                        PREPARE_GETPWNAM;
                        eargp.uid = val.convertToInteger().getIntValue();
                        eargp.uid_given_set();
                    }
//                    #else
//                    rb_raise(rb_eNotImpError,
//                            "uid option is unimplemented on this machine");
//                    #endif
                }
                else if (id.equals("gid") && false) { // TODO
//                    #ifdef HAVE_SETGID
                    if (eargp.gid_given()) {
                        throw runtime.newArgumentError("gid option specified twice");
                    }
//                    checkGidSwitch();
                    {
//                        PREPARE_GETGRNAM;
                        eargp.gid = val.convertToInteger().getIntValue();
                        eargp.gid_given_set();
                    }
//                    #else
//                    rb_raise(rb_eNotImpError,
//                            "gid option is unimplemented on this machine");
//                    #endif
                }
                else {
                    return ST_STOP;
                }
                break;

            case INTEGER:
                if (!(key instanceof RubyFixnum)) {
                    return ST_STOP;
                }
            case FILE:
            case IO:
            case ARRAY:
                checkExecRedirect(context, runtime, key, val, eargp);
                break;

            default:
                return ST_STOP;
        }

        return ST_CONTINUE;
    }

    // MRI: check_exec_redirect
    static void checkExecRedirect(ThreadContext context, Ruby runtime, IRubyObject key, IRubyObject val, ExecArg eargp) {
        IRubyObject param;
        IRubyObject path, flags, perm;
        IRubyObject tmp;
        String id;

        switch (val.getMetaClass().getRealClass().getClassIndex()) {
            case SYMBOL:
                id = val.toString();
                if (id.equals("close")) {
                    param = context.nil;
                    eargp.fd_close = checkExecRedirect1(runtime, eargp.fd_close, key, param);
                }
                else if (id.equals("in")) {
                    param = runtime.newFixnum(0);
                    eargp.fd_dup2 = checkExecRedirect1(runtime, eargp.fd_dup2, key, param);
                }
                else if (id.equals("out")) {
                    param = runtime.newFixnum(1);
                    eargp.fd_dup2 = checkExecRedirect1(runtime, eargp.fd_dup2, key, param);
                }
                else if (id.equals("err")) {
                    param = runtime.newFixnum(2);
                    eargp.fd_dup2 = checkExecRedirect1(runtime, eargp.fd_dup2, key, param);
                }
                else {
                    throw runtime.newArgumentError("wrong exec redirect symbol: " + id);
                }
                break;

            case FILE:
            case IO:
                val = checkExecRedirectFd(runtime, val, false);
                /* fall through */
            case INTEGER:
                if (val instanceof RubyFixnum) {
                    param = val;
                    eargp.fd_dup2 = checkExecRedirect1(runtime, eargp.fd_dup2, key, param);
                    break;
                }

                checkExecRedirectDefault(runtime, key, val, eargp);
                break;

            case ARRAY:
                path = ((RubyArray)val).eltOk(0);
                if (((RubyArray)val).size() == 2 && path instanceof RubySymbol &&
                        path.toString().equals("child")) {
                    param = checkExecRedirectFd(runtime, ((RubyArray)val).eltOk(1), false);
                    eargp.fd_dup2_child = checkExecRedirect1(runtime, eargp.fd_dup2_child, key, param);
                }
                else {
                    path = RubyFile.get_path(context, path);
                    flags = ((RubyArray)val).eltOk(1);
                    int intFlags;
                    if (flags.isNil())
                        intFlags = OpenFlags.O_RDONLY.intValue();
                    else if (flags instanceof RubyString)
                        intFlags = OpenFile.ioModestrOflags(runtime, flags.toString());
                    else
                        intFlags = flags.convertToInteger().getIntValue();
                    flags = runtime.newFixnum(intFlags);
                    perm = ((RubyArray)val).entry(2);
                    perm = perm.isNil() ? runtime.newFixnum(0644) : perm.convertToInteger();
                    param = RubyArray.newArray(runtime,
                            ((RubyString)path).strDup(runtime).export(context),
                            flags,
                            perm);
                    eargp.fd_open = checkExecRedirect1(runtime, eargp.fd_open, key, param);
                }
                break;

            case STRING:
                path = val;
                path = RubyFile.get_path(context, path);
                if (key instanceof RubyIO)
                    key = checkExecRedirectFd(runtime, key, true);
                if (key instanceof RubyFixnum && (((RubyFixnum)key).getIntValue() == 1 || ((RubyFixnum)key).getIntValue() == 2))
                    flags = runtime.newFixnum(OpenFlags.O_WRONLY.intValue()|OpenFlags.O_CREAT.intValue()|OpenFlags.O_TRUNC.intValue());
                else
                    flags = runtime.newFixnum(OpenFlags.O_RDONLY.intValue());
                perm = runtime.newFixnum(0644);
                param = RubyArray.newArray(runtime,
                        ((RubyString)path).strDup(runtime).export(context),
                        flags,
                        perm);
                eargp.fd_open = checkExecRedirect1(runtime, eargp.fd_open, key, param);
                break;

            default:
                checkExecRedirectDefault(runtime, key, val, eargp);
        }

    }

    private static void checkExecRedirectDefault(Ruby runtime, IRubyObject key, IRubyObject val, ExecArg eargp) {
        IRubyObject tmp;
        IRubyObject param;
        tmp = val;
        val = TypeConverter.ioCheckIO(runtime, tmp);
        if (!val.isNil()) {
            val = checkExecRedirectFd(runtime, val, false);
            param = val;
            eargp.fd_dup2 = checkExecRedirect1(runtime, eargp.fd_dup2, key, param);
        }
        throw runtime.newArgumentError("wrong exec redirect action");
    }

    // MRI: check_exec_redirect_fd
    static IRubyObject checkExecRedirectFd(Ruby runtime, IRubyObject v, boolean iskey) {
        IRubyObject tmp;
        int fd;
        if (v instanceof RubyFixnum) {
            fd = RubyNumeric.fix2int(v);
        }
        else if (v instanceof RubySymbol) {
            String id = v.toString();
            if (id.equals("in"))
                fd = 0;
            else if (id.equals("out"))
                fd = 1;
            else if (id.equals("err"))
                fd = 2;
            else
                throw runtime.newArgumentError("wrong exec redirect");
        }
        else if (!(tmp = TypeConverter.convertToTypeWithCheck(v, runtime.getIO(), "to_io")).isNil()) {
            OpenFile fptr;
            fptr = ((RubyIO)tmp).getOpenFileChecked();
            if (fptr.tiedIOForWriting != null)
                throw runtime.newArgumentError("duplex IO redirection");
            fd = fptr.fd().bestFileno();
        }
        else {
            throw runtime.newArgumentError("wrong exec redirect");
        }
        if (fd < 0) {
            throw runtime.newArgumentError("negative file descriptor");
        }
        else if (Platform.IS_WINDOWS && fd >= 3 && iskey) {
            throw runtime.newArgumentError("wrong file descriptor (" + fd + ")");
        }
        return runtime.newFixnum(fd);
    }

    // MRI: check_exec_redirect1
    static RubyArray checkExecRedirect1(Ruby runtime, RubyArray ary, IRubyObject key, IRubyObject param) {
        if (ary == null) {
            ary = runtime.newArray();
        }
        if (!(key instanceof RubyArray)) {
            IRubyObject fd = checkExecRedirectFd(runtime, key, !param.isNil());
            ary.push(runtime.newArray(fd, param));
        }
        else {
            int i, n=0;
            for (i = 0 ; i < ((RubyArray)key).size(); i++) {
                IRubyObject v = ((RubyArray)key).eltOk(i);
                IRubyObject fd = checkExecRedirectFd(runtime, v, !param.isNil());
                ary.push(runtime.newArray(fd, param));
                n++;
            }
        }
        return ary;
    }

    private static final int ST_CONTINUE = 0;
    private static final int ST_STOP = 1;

    // rb_execarg_new
    public static ExecArg execargNew(ThreadContext context, IRubyObject[] argv, boolean accept_shell) {
        ExecArg eargp = new ExecArg();
        execargInit(context, argv, accept_shell, eargp);
        return eargp;
    }

    // rb_execarg_init
    private static RubyString execargInit(ThreadContext context, IRubyObject[] argv, boolean accept_shell, ExecArg eargp) {
        RubyString prog, ret;
        IRubyObject[] env_opt = {context.nil, context.nil};
        IRubyObject[][] argv_p = {argv};
        prog = execGetargs(context, argv_p, accept_shell, env_opt);
        execFillarg(context, prog, argv_p[0], env_opt[0], env_opt[1], eargp);
        ret = eargp.use_shell ? eargp.command_name : eargp.command_name;
        return ret;
    }

    // rb_exec_getargs
    private static RubyString execGetargs(ThreadContext context, IRubyObject[][] argv_p, boolean accept_shell, IRubyObject[] env_opt) {
        Ruby runtime = context.runtime;
        IRubyObject hash;
        RubyString prog;
        int beg = 0;
        int end = argv_p[0].length;

        if (end >= 1) {
            hash = TypeConverter.checkHashType(runtime, argv_p[0][end - 1]);
            if (!hash.isNil()) {
                env_opt[1] = hash;
                end--;
            }
        }

        if (end >= 1) {
            hash = TypeConverter.checkHashType(runtime, argv_p[0][0]);
            if (!hash.isNil()) {
                env_opt[0] = hash;
                beg++;
            }
        }
        argv_p[0] = Arrays.copyOfRange(argv_p[0], beg, end);
        prog = checkArgv(context, argv_p[0]);
        if (prog == null) {
            prog = (RubyString)argv_p[0][0];
            if (accept_shell && (end - beg) == 1) {
                argv_p[0] = IRubyObject.NULL_ARRAY;
            }
        }
        return prog;
    }

    // rb_check_argv
    public static RubyString checkArgv(ThreadContext context, IRubyObject[] argv) {
        Ruby runtime = context.runtime;
        IRubyObject tmp;
        RubyString prog;
        int i;

        Arity.checkArgumentCount(runtime, argv, 1, Integer.MAX_VALUE);

        prog = null;
        tmp = TypeConverter.checkArrayType(runtime, argv[0]);
        if (!tmp.isNil()) {
            if (((RubyArray)tmp).size() != 2) {
                throw runtime.newArgumentError("wrong first argument");
            }
            prog = ((RubyArray)tmp).eltOk(0).convertToString();
            argv[0] = ((RubyArray)tmp).eltOk(1);
            StringSupport.checkEmbeddedNulls(runtime, prog);
            prog = prog.strDup(runtime);
            prog.setFrozen(true);
        }
        for (i = 0; i < argv.length; i++) {
            argv[i] = argv[i].convertToString();
            argv[i] = ((RubyString)argv[i]).newFrozen();
            StringSupport.checkEmbeddedNulls(runtime, argv[i]);
        }
        //        security(name ? name : RSTRING_PTR(argv[0]));
        return prog;
    }

    private static final int posix_sh_cmd_length = 8;
    private static final String posix_sh_cmds[] = {
            "!",		/* reserved */
            ".",		/* special built-in */
            ":",		/* special built-in */
            "break",		/* special built-in */
            "case",		/* reserved */
            "continue",		/* special built-in */
            "do",		/* reserved */
            "done",		/* reserved */
            "elif",		/* reserved */
            "else",		/* reserved */
            "esac",		/* reserved */
            "eval",		/* special built-in */
            "exec",		/* special built-in */
            "exit",		/* special built-in */
            "export",		/* special built-in */
            "fi",		/* reserved */
            "for",		/* reserved */
            "if",		/* reserved */
            "in",		/* reserved */
            "readonly",		/* special built-in */
            "return",		/* special built-in */
            "set",		/* special built-in */
            "shift",		/* special built-in */
            "then",		/* reserved */
            "times",		/* special built-in */
            "trap",		/* special built-in */
            "unset",		/* special built-in */
            "until",		/* reserved */
            "while",		/* reserved */
    };

    private static final byte[] DUMMY_ARRAY = ByteList.NULL_ARRAY;

    private static void execFillarg(ThreadContext context, RubyString prog, IRubyObject[] argv, IRubyObject env, IRubyObject opthash, ExecArg eargp) {
        Ruby runtime = context.runtime;
        int argc = argv.length;

        if (!opthash.isNil()) {
            checkExecOptions(context, runtime, (RubyHash)opthash, eargp);
        }
        // add chdir if necessary
        String virtualCWD = runtime.getCurrentDirectory();
        if (!virtualCWD.equals(runtime.getPosix().getcwd())) {
            String arg = prog.toString();

            // if we're launching org.jruby.Main, adjust args to -C to new dir
            if ((arg = ShellLauncher.changeDirInsideJar(runtime, arg)) != null) {
                prog = RubyString.newString(runtime, arg);
            } else if (virtualCWD.startsWith("uri:classloader:")) {
                // can't switch to uri:classloader URL, so just run in cwd
            } else if (!eargp.chdir_given()) {
                // only if :chdir is not specified
                eargp.chdir_given_set();
                eargp.chdir_dir = virtualCWD;
            }
        }

        // restructure command as a single string if chdir and has args
        if (eargp.chdir_given() && argc > 1) {
            RubyArray array = RubyArray.newArrayMayCopy(runtime, argv);
            prog = (RubyString)array.join(context, RubyString.newString(runtime, " "));
        }

        if (!env.isNil()) {
            eargp.env_modification = checkExecEnv(context, (RubyHash) env, eargp);
        }

        prog = prog.export(context);
        // need to use shell
        eargp.use_shell = argc == 0 || eargp.chdir_given();
        if (eargp.use_shell)
            eargp.command_name = prog;
        else
            eargp.command_name = prog;

        if (!Platform.IS_WINDOWS) {
            if (eargp.use_shell) {
                byte[] pBytes;
                int p;
                ByteList first = new ByteList(DUMMY_ARRAY, false);
                boolean has_meta = false;
                /*
                 * meta characters:
                 *
                 * *    Pathname Expansion
                 * ?    Pathname Expansion
                 * {}   Grouping Commands
                 * []   Pathname Expansion
                 * <>   Redirection
                 * ()   Grouping Commands
                 * ~    Tilde Expansion
                 * &    AND Lists, Asynchronous Lists
                 * |    OR Lists, Pipelines
                 * \    Escape Character
                 * $    Parameter Expansion
                 * ;    Sequential Lists
                 * '    Single-Quotes
                 * `    Command Substitution
                 * "    Double-Quotes
                 * \n   Lists
                 *
                 * #    Comment
                 * =    Assignment preceding command name
                 * %    (used in Parameter Expansion)
                 */
                ByteList progByteList = prog.getByteList();
                pBytes = progByteList.unsafeBytes();
                for (p = 0; p < progByteList.length(); p++){
                    if (progByteList.get(p) == ' ' || progByteList.get(p) == '\t'){
                        if (first.unsafeBytes() != DUMMY_ARRAY && first.length() == 0) first.setRealSize(p - first.begin());
                    }
                    else{
                        if (first.unsafeBytes() == DUMMY_ARRAY) { first.setUnsafeBytes(pBytes); first.setBegin(p + progByteList.begin()); }
                    }
                    if (!has_meta && "*?{}[]<>()~&|\\$;'`\"\n#".indexOf(progByteList.get(p) & 0xFF) != -1)
                        has_meta = true;
                    if (first.length() == 0) {
                        if (progByteList.get(p) == '='){
                            has_meta = true;
                        }
                        else if (progByteList.get(p) == '/'){
                            first.setRealSize(0x100); /* longer than any posix_sh_cmds */
                        }
                    }
                    if (has_meta)
                        break;
                }
                if (!has_meta && first.getUnsafeBytes() != DUMMY_ARRAY) {
                    if (first.length() == 0) first.setRealSize(p - first.getBegin());
                    if (first.length() > 0 && first.length() <= posix_sh_cmd_length &&
                        Arrays.binarySearch(posix_sh_cmds, first.toString(), StringComparator.INSTANCE) >= 0)
                        has_meta = true;
                }
                if (!has_meta && !eargp.chdir_given()) {
                    /* avoid shell since no shell meta character found and no chdir needed. */
                    eargp.use_shell = false;
                }
                if (!eargp.use_shell) {
                    List<byte[]> argv_buf = new ArrayList<>();
                    pBytes = prog.getByteList().unsafeBytes();
                    p = prog.getByteList().begin();
                    int pEnd = prog.getByteList().length() + p;
                    while (p < pEnd){
                        while (p < pEnd && (pBytes[p] == ' ' || pBytes[p] == '\t'))
                            p++;
                        if (p < pEnd){
                            int w = p;
                            while (p < pEnd && pBytes[p] != ' ' && pBytes[p] != '\t')
                                p++;
                            argv_buf.add(Arrays.copyOfRange(pBytes, w, p));
                            eargp.argv_buf = argv_buf;
                        }
                    }
                    if (argv_buf.size() > 0) {
                        eargp.command_name = RubyString.newStringNoCopy(runtime, argv_buf.get(0));
                    } else {
                        eargp.command_name = RubyString.newEmptyString(runtime); // empty command will get caught below shortly
                    }
                }
            }
        }

        if (!eargp.use_shell) {
            String abspath;
            abspath = dlnFindExeR(runtime, eargp.command_name.toString(), eargp.path_env);
            if (abspath != null)
                eargp.command_abspath = StringSupport.checkEmbeddedNulls(runtime, RubyString.newString(runtime, abspath));
            else
                eargp.command_abspath = null;
        }

        if (!eargp.use_shell && eargp.argv_buf == null) {
            int i;
            ArrayList<byte[]> argv_buf = new ArrayList<>(argc);
            for (i = 0; i < argc; i++) {
                IRubyObject arg = argv[i];
                RubyString argStr = StringSupport.checkEmbeddedNulls(runtime, arg);
                argStr = argStr.export(context);
                argv_buf.add(argStr.getBytes());
            }
            eargp.argv_buf = argv_buf;
        }

        if (!eargp.use_shell) {
            ArgvStr argv_str = new ArgvStr();
            argv_str.argv = new String[eargp.argv_buf.size()];
            int i = 0;
            for (byte[] bytes : eargp.argv_buf) {
                argv_str.argv[i++] = new String(bytes);
            }
            eargp.argv_str = argv_str;
        }
    }

    private static final class StringComparator implements Comparator<String> {

        static final StringComparator INSTANCE = new StringComparator();

        public int compare(String o1, String o2) {
            int ret = o1.compareTo(o2);
            if (ret == 0 && o1.length() > o2.length()) return -1;
            return ret;
        }

    }

    private static String dlnFindExeR(Ruby runtime, String fname, IRubyObject path) {
        File exePath = ShellLauncher.findPathExecutable(runtime, fname, path);
        return exePath != null ? exePath.getAbsolutePath() : null;
    }

    private static class ArgvStr {
        String[] argv;
    }

    public static class ExecArg {
        boolean use_shell;
        RubyString command_name;
        RubyString command_abspath; /* full path string or nil */
        ArgvStr argv_str;
        List<byte[]> argv_buf;
        IRubyObject redirect_fds;
        String[] envp_str;
        List<String> envp_buf;
        run_exec_dup2_fd_pair[] dup2_tmpbuf;
        int flags;
        long pgroup_pgid = -1; /* asis(-1), new pgroup(0), specified pgroup (0<V). */
        IRubyObject rlimit_limits; /* null or [[rtype, softlim, hardlim], ...] */
        int umask_mask;
        int uid;
        int gid;
        RubyArray fd_dup2;
        RubyArray fd_close;
        RubyArray<RubyArray> fd_open;
        RubyArray fd_dup2_child;
        int close_others_maxhint;
        RubyArray env_modification; /* null or [[k1,v1], ...] */
        String chdir_dir;
        List<SpawnFileAction> fileActions = new ArrayList();
        List<SpawnAttribute> attributes = new ArrayList();
        IRubyObject path_env;

        boolean pgroup_given() {
            return (flags & 0x1) != 0;
        }

        boolean umask_given() {
            return (flags & 0x2) != 0;
        }

        boolean unsetenv_others_given() {
            return (flags & 0x4) != 0;
        }

        boolean unsetenv_others_do() {
            return (flags & 0x8) != 0;
        }

        boolean close_others_given() {
            return (flags & 0x10) != 0;
        }

        boolean close_others_do() {
            return (flags & 0x20) != 0;
        }

        boolean chdir_given() {
            return (flags & 0x40) != 0;
        }

        boolean new_pgroup_given() {
            return (flags & 0x80) != 0;
        }

        boolean new_pgroup_flag() {
            return (flags & 0x100) != 0;
        }

        boolean uid_given() {
            return (flags & 0x200) != 0;
        }

        boolean gid_given() {
            return (flags & 0x400) != 0;
        }

        void pgroup_given_set() {
            flags |= 0x1;
        }

        void umask_given_set() {
            flags |= 0x2;
        }

        void unsetenv_others_given_set() {
            flags |= 0x4;
        }

        void unsetenv_others_do_set() {
            flags |= 0x8;
        }

        void close_others_given_set() {
            flags |= 0x10;
        }

        void close_others_do_set() {
            flags |= 0x20;
        }

        void chdir_given_set() {
            flags |= 0x40;
        }

        void new_pgroup_given_set() {
            flags |= 0x80;
        }

        void new_pgroup_flag_set() {
            flags |= 0x100;
        }

        void uid_given_set() {
            flags |= 0x200;
        }

        void gid_given_set() {
            flags |= 0x400;
        }

        void pgroup_given_clear() {
            flags &= ~0x1;
        }

        void umask_given_clear() {
            flags &= ~0x2;
        }

        void unsetenv_others_given_clear() {
            flags &= ~0x4;
        }

        void unsetenv_others_do_clear() {
            flags &= ~0x8;
        }

        void close_others_given_clear() {
            flags &= ~0x10;
        }

        void close_others_do_clear() {
            flags &= ~0x20;
        }

        void chdir_given_clear() {
            flags &= ~0x40;
        }

        void new_pgroup_given_clear() {
            flags &= ~0x80;
        }

        void new_pgroup_flag_clear() {
            flags &= ~0x100;
        }

        void uid_given_clear() {
            flags &= ~0x200;
        }

        void gid_given_clear() {
            flags &= ~0x400;
        }
    }

    private static final Comparator<run_exec_dup2_fd_pair> intcmp = new Comparator<run_exec_dup2_fd_pair>() {
        @Override
        public int compare(run_exec_dup2_fd_pair o1, run_exec_dup2_fd_pair o2) {
            return Integer.compare(o1.oldfd, o2.oldfd);
        }
    };

    private static final Comparator<run_exec_dup2_fd_pair> intrcmp = new Comparator<run_exec_dup2_fd_pair>() {
        @Override
        public int compare(run_exec_dup2_fd_pair o1, run_exec_dup2_fd_pair o2) {
            return Integer.compare(o2.oldfd, o1.oldfd);
        }
    };

}
