Jetbrains remote development
为了调试 Spice 方便,稍微折腾了一下 Clion 的远程开发功能。

Jetbrains 比较典型的远程开发方式有两种,一种是 remote with local sources,一种是 remote with gateway。

remote with local sources 关键的配置是工具链的配置,如下图所示:

f18ecdb665920762228233e02b31de08.png

需要注意的是 clion 的 remote with local sources 当前并不支持 compilation database 工程(其实仅支持 cmake 与 makefile),因此这种方式有一定的局限性。

Remote development
https://www.jetbrains.com/help/clion/remote-development.html

Support compilation database projects for remote mode and Docker
https://youtrack.jetbrains.com/issue/CPP-16202/Support-compilation-database-projects-for-remote-mode-and-Docker

remote with gateway

在远端机器手工运行 remote-dev-server

$ cd ~/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/bin/
$ ./remote-dev-server.sh run ~/working/spice-streaming-agent/

Join link: tcp://127.0.0.1:5990#jt=629f9945-4f76-42ee-b8f1-78a758678a80&p=CL&fp=531765D68754567EB27F06E4EA40F0D5690E7719FB400285A9F7395394840D21&cb=232.9921.42&newUi=true&jb=17.0.8b1000.22

Http link: https://code-with-me.jetbrains.com/remoteDev#idePath=%2Fhome%2Frunsisi%2F.cache%2FJetBrains%2FRemoteDev%2Fdist%2F7d288fc846b32_CLion-233.8264.3&projectPath=%2Fhome%2Frunsisi%2Fworking%2Fspice-streaming-agent&host=ubuntu-kvm&port=22&user=runsisi&type=ssh&deploy=false&newUi=true

Gateway link: jetbrains-gateway://connect#idePath=%2Fhome%2Frunsisi%2F.cache%2FJetBrains%2FRemoteDev%2Fdist%2F7d288fc846b32_CLion-233.8264.3&projectPath=%2Fhome%2Frunsisi%2Fworking%2Fspice-streaming-agent&host=ubuntu-kvm&port=22&user=runsisi&type=ssh&deploy=false&newUi=true

在本地机器连接远端服务:

edb43022e6c080b67d88b30db6672cce.png

0bd68413ecb37b2165074811e8537f33.png

root 权限运行/调试程序

remote with gateway 在使用 root 权限运行/调试程序时存在问题:

$ cat ~/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/log/idea.log

2023-10-03 14:09:59,060 [ 600254]   WARN - #c.i.u.i.p.ProcessHandshakeLauncher - Reading handshake failed
2023-10-03 14:09:59,061 [ 600255]   WARN - #c.i.u.i.p.ProcessHandshakeLauncher - Process finished with exit code 1
2023-10-03 14:09:59,061 [ 600255]   WARN - #c.i.u.i.p.ProcessHandshakeLauncher - Process stderr:
Exception in thread "main" java.io.IOException: Cannot run program "/home/runsisi/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.7463.temp.jbr/bin/java": error=0, Failed to exec spawn helper: pid: 10786, exit value: 1
        at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1143)
        at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1073)
        at com.intellij.execution.process.mediator.daemon.DaemonProcessMainKt.trampoline(DaemonProcessMain.kt:39)
        at com.intellij.execution.process.mediator.daemon.DaemonProcessMainKt.main(DaemonProcessMain.kt:99)
Caused by: java.io.IOException: error=0, Failed to exec spawn helper: pid: 10786, exit value: 1
        at java.base/java.lang.ProcessImpl.forkAndExec(Native Method)
        at java.base/java.lang.ProcessImpl.<init>(ProcessImpl.java:314)
        at java.base/java.lang.ProcessImpl.start(ProcessImpl.java:244)
        at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1110)
        ... 3 more

Elevation for remote process doesn’t work : Failed to launch elevation service using ‘pkexec’
https://youtrack.jetbrains.com/issue/GTW-2139/Elevation-for-remote-process-doesnt-work-Failed-to-launch-elevation-service-using-pkexec

Cannot run program java, failed to exec spawn helper, exit value: 1
https://youtrack.jetbrains.com/issue/IDEA-304440

Java, Failed to exec spawn helper error since moving to Java 14 on linux
https://stackoverflow.com/questions/61301818/java-failed-to-exec-spawn-helper-error-since-moving-to-java-14-on-linux

从系统日志可以看到如下打印:

$ sudo journalctl -f

10月 03 15:05:43 ubuntu-kvm pkexec[28997]: pam_unix(polkit-1:session): session opened for user root(uid=0) by (uid=1000)
10月 03 15:05:43 ubuntu-kvm pkexec[28997]: runsisi: Executing command [USER=root] [TTY=unknown] [CWD=/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/plugins/remote-dev-server/selfcontained/fontconfig] [COMMAND=/home/runsisi/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.23781.temp.jbr/bin/java -Djna.boot.library.path=/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/jna/amd64 -cp /home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/app-client.jar:/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/util-8.jar:/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/grpc.jar:/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/lib-client.jar:/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/protobuf.jar com.intellij.execution.process.mediator.daemon.DaemonProcessMainKt --trampoline --daemonize --leader-pid=23974 --handshake-file=/tmp/6dcb3028-f19e-4888-a556-f84f2678b4da --token-encrypt-rsa=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQU8GW2jUJgnA0GTebKipFuS6er9CARhEDIfRqOWueP3aWL9a3617uhWn9tCm/Osp79HggMj2uT/katqqdK0E2vLOaOEOfF7ASN7Wygo/Z1pPRfiEuhVv+Y033TgjtBRU+KnDAeFMo1vSSEuGJD6+rT+B67eAWCZVgU7THoHH4twIDAQAB]

手工执行该命令:

$ touch /tmp/6dcb3028-f19e-4888-a556-f84f2678b4da

$ pkexec /home/runsisi/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.23781.temp.jbr/bin/java -Djna.boot.library.path=/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/jna/amd64 -cp /home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/app-client.jar:/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/util-8.jar:/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/grpc.jar:/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/lib-client.jar:/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/protobuf.jar com.intellij.execution.process.mediator.daemon.DaemonProcessMainKt --trampoline --daemonize --leader-pid=23974 --handshake-file=/tmp/6dcb3028-f19e-4888-a556-f84f2678b4da --token-encrypt-rsa=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQU8GW2jUJgnA0GTebKipFuS6er9CARhEDIfRqOWueP3aWL9a3617uhWn9tCm/Osp79HggMj2uT/katqqdK0E2vLOaOEOfF7ASN7Wygo/Z1pPRfiEuhVv+Y033TgjtBRU+KnDAeFMo1vSSEuGJD6+rT+B67eAWCZVgU7THoHH4twIDAQAB

This command is not for general use and should only be run as the result of a call to
ProcessBuilder.start() or Runtime.exec() in a java application
Exception in thread "main" java.io.IOException: Cannot run program "/home/runsisi/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.23781.temp.jbr/bin/java": error=0, Failed to exec spawn helper: pid: 30224, exit value: 1
        at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1143)
        at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1073)
        at com.intellij.execution.process.mediator.daemon.DaemonProcessMainKt.trampoline(DaemonProcessMain.kt:39)
        at com.intellij.execution.process.mediator.daemon.DaemonProcessMainKt.main(DaemonProcessMain.kt:99)
Caused by: java.io.IOException: error=0, Failed to exec spawn helper: pid: 30224, exit value: 1
        at java.base/java.lang.ProcessImpl.forkAndExec(Native Method)
        at java.base/java.lang.ProcessImpl.<init>(ProcessImpl.java:314)
        at java.base/java.lang.ProcessImpl.start(ProcessImpl.java:244)
        at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1110)
        ... 3 more

其中错误日志是 jspawnhelper 打印的:

$ ~/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/jbr/lib/jspawnhelper
This command is not for general use and should only be run as the result of a call to
ProcessBuilder.start() or Runtime.exec() in a java application

增加 -Djdk.lang.Process.launchMechanism=vfork 选项(或者 =fork)可以让手工执行的命令行正常运行(这样作为子进程的 jspawnhelper 才能从 argv[0] 得到 java 父进程传递的参数):

$ pkexec /home/runsisi/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.23781.temp.jbr/bin/java -Djdk.lang.Process.launchMechanism=vfork -Djna.boot.library.path=/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/jna/amd64 -cp /home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/app-client.jar:/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/util-8.jar:/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/grpc.jar:/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/lib-client.jar:/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/lib/protobuf.jar com.intellij.execution.process.mediator.daemon.DaemonProcessMainKt --trampoline --daemonize --leader-pid=23974 --handshake-file=/tmp/6dcb3028-f19e-4888-a556-f84f2678b4da --token-encrypt-rsa=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQU8GW2jUJgnA0GTebKipFuS6er9CARhEDIfRqOWueP3aWL9a3617uhWn9tCm/Osp79HggMj2uT/katqqdK0E2vLOaOEOfF7ASN7Wygo/Z1pPRfiEuhVv+Y033TgjtBRU+KnDAeFMo1vSSEuGJD6+rT+B67eAWCZVgU7THoHH4twIDAQAB

[trampoline] Started daemon process PID 134799
Started server on port 36585

同时注意到 ~/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.XXX.temp.jbr/lib 目录下(JetBrains/RemoteDev-CL/ 目录是 clion 工作目录)的 jspawnhelper 是个 shell 脚本(真实的 jspawnhelper 是一个 ELF 可执行程序):

$ cat ~/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.227183.temp.jbr/lib/jspawnhelper
#!/bin/sh
exec /lib64/ld-linux-x86-64.so.2 --library-path "/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/plugins/remote-dev-server/selfcontained/lib" "/home/runsisi/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.227183.temp.jbr/lib/jspawnhelper.bin"  "$@"

这个脚本是在 ~/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/plugins/remote-dev-server/bin/launcher.sh 中(RemoteDev/dist 目录下实际上对应的是多个版本的 headless IDE,在本地 IDE 的 plugins/remote-dev-server/bin/ 目录下同样也有这样一个脚本)临时生成的(launcher.sh 由 ~/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/bin/remote-dev-server.sh 调用):

patch_bin_file "$TEMP_JBR/lib/jspawnhelper"

使用 execv/execve 系统调用执行 shell 脚本时,如果将用户参数设置为从 argv[0] 开始(通常 argv[0] 是 shell 脚本的路径,而用户参数从 argv[1] 开始)会导致 shell 脚本无法得到正确的 $1 参数(即第一个用户参数被丢弃了,相当于执行了 shift 操作)。由于 jdk-22+3 之前的版本(jdk-21+31 进行了 backport)会通过 argv[0] 给 jspawnhelper 传递参数,而 jspawnhelper 在这里是一个 shell 脚本,导致 java 给 jspawnhelper 传递的这个参数丢掉了,从而最终 jspawnhelper 会运行失败。

jspawnhelper should not use argv[0]
https://github.com/openjdk/jdk/commit/47d00a4cbeff5d757dda9c660dfd2385c02a57d7

当指定 -Djdk.lang.Process.launchMechanism=vfork 选项(或者 =fork)时,根本不涉及执行 jspawnhelper,因此完全不可能有这个问题。

Java_java_lang_ProcessImpl_forkAndExec
https://github.com/openjdk/jdk/blob/jdk-22%2B18/src/java.base/unix/native/libjava/ProcessImpl_md.c#L613

startChild
https://github.com/openjdk/jdk/blob/jdk-22%2B18/src/java.base/unix/native/libjava/ProcessImpl_md.c#L596

childProcess
https://github.com/openjdk/jdk/blob/jdk-22%2B18/src/java.base/unix/native/libjava/childproc.c#L415

spawnChild jdk-22+2
https://github.com/openjdk/jdk/blob/jdk-22%2B2/src/java.base/unix/native/libjava/ProcessImpl_md.c#L497

spawnChild jdk-22+18
https://github.com/openjdk/jdk/blob/jdk-22%2B18/src/java.base/unix/native/libjava/ProcessImpl_md.c#L502

复现方法

注意 pid.XXX.temp.jbr/bin/java 是 java 的 shell 脚本 exec wrapper,通过 launcher.sh 生成(launcher.sh 由 remote-dev-server.sh 调用)并替换成 pid.XXX.temp.jbr/bin/java.bin(符号链接,指向原始的 java):

$ cat ~/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.2378.temp.jbr/bin/java
#!/bin/sh
exec /lib64/ld-linux-x86-64.so.2 --library-path "/home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/plugins/remote-dev-server/selfcontained/lib" "/home/runsisi/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.2378.temp.jbr/bin/java.bin" "-Djava.home=/home/runsisi/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.2378.temp.jbr" "$@"

$ ls -l /home/runsisi/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.2378.temp.jbr/bin/java.bin
lrwxrwxrwx 1 runsisi runsisi 87 10月  7 09:54 /home/runsisi/.cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.2378.temp.jbr/bin/java.bin -> /home/runsisi/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/jbr/bin/java

该问题可以通过如下简单的方法进行复现。

jspawnhelper 增加三行打印:

fprintf(stderr, "argc: %d\n", argc);
fprintf(stderr, "argv[0]: %s\n", argv[0]);
fprintf(stderr, "argv[1]: %s\n", argv[1]);

java 测试程序 Main.java:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    public static void main(String[] args) throws IOException, InterruptedException {
        ProcessBuilder pb = new ProcessBuilder("/bin/bash", "-c", "exit 0;");

        Process p = pb.start();
        p.waitFor();

        var stdout = p.getInputStream();
        var reader = new BufferedReader(new InputStreamReader(stdout));
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }

        var stderr = p.getErrorStream();
        reader = new BufferedReader(new InputStreamReader(stderr));
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
    }
}

编译 java 测试程序:

$ javac -d . Main.java

构造本地 jdk 环境:

#!/bin/bash -e

WORK_DIR=$(cd -P $(dirname $0) && pwd -P)

IDE_HOME=/usr/lib/jvm

TEMP_JBR="$WORK_DIR/pid.$$.temp.jbr"
rm -rf "$TEMP_JBR"
cp -r --symbolic-link "$IDE_HOME/java-17-openjdk" "$TEMP_JBR"
find "$TEMP_JBR" -type d -exec chmod 755 {} \;

patch_bin_file() {
  file="$1"
  extra_arg=""
  if [ "$(basename "$file")" = "java" ]; then
    extra_arg="\"-Djava.home=$TEMP_JBR\""
  fi
  mv "$file" "$file.bin"

  cat >"$file" <<EOT
#!/bin/sh
exec "${file}.bin" $extra_arg "\$@"
EOT
  chmod 755 "$file"
}

find -L "$TEMP_JBR/bin" -type f -executable | while IFS= read -r line; do patch_bin_file "$line"; done
patch_bin_file "$TEMP_JBR/lib/jexec"
patch_bin_file "$TEMP_JBR/lib/jspawnhelper"

运行测试程序(指定 -Djava.home 选项):

❯ java -Djava.home=pid.25004.temp.jbr Main
argc: 1
argv[0]: /home/runsisi/working/java/process/out/production/process/pid.25004.temp.jbr/lib/jspawnhelper.bin
argv[1]: (null)
This command is not for general use and should only be run as the result of a call to
ProcessBuilder.start() or Runtime.exec() in a java application
Exception in thread "main" java.io.IOException: Cannot run program "/bin/bash": error=0, Failed to exec spawn helper: pid: 25400, exit value: 1
	at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1143)
	at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1073)
	at Main.main(Main.java:9)
Caused by: java.io.IOException: error=0, Failed to exec spawn helper: pid: 25400, exit value: 1
	at java.base/java.lang.ProcessImpl.forkAndExec(Native Method)
	at java.base/java.lang.ProcessImpl.<init>(ProcessImpl.java:314)
	at java.base/java.lang.ProcessImpl.start(ProcessImpl.java:244)
	at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1110)

去掉 -Djava.home 选项:

❯ java Main
argc: 1
argv[0]: 10:13
argv[1]: (null)

或者 -Djava.home 选项使用自带的 jre 目录:

❯ java -Djava.home=/usr/lib/jvm/java-17-openjdk Main
argc: 1
argv[0]: 10:13
argv[1]: (null)

增加 -Djdk.lang.Process.launchMechanism=vfork 选项(或者 =fork):

❯ java -Djava.home=pid.25004.temp.jbr -Djdk.lang.Process.launchMechanism=vfork Main

❯ java -Djava.home=pid.25004.temp.jbr -Djdk.lang.Process.launchMechanism=fork Main

由于没有调用 jspawnhelper,因此没有打印输出。

显然,问题是由 -Djava.home 选项引起的,当 -Djava.home 选项指定的是原始的 jdk 路径时,jspawnhelper 指向原始 jdk 路径下的可执行文件而不是 shell 脚本,因此不存在前面提到的使用 execv/execve 系统调用执行 shell 脚本带来的问题:

// https://github.com/openjdk/jdk/blob/jdk-22%2B18/src/java.base/unix/classes/java/lang/ProcessImpl.java#L126

final class ProcessImpl extends Process {
    private static final byte[] helperpath = toCString(StaticProperty.javaHome() + "/lib/jspawnhelper");

    private ProcessImpl(final byte[] prog,
              final byte[] argBlock, final int argc,
              final byte[] envBlock, final int envc,
              final byte[] dir,
              final int[] fds,
              final boolean forceNullOutputStream,
              final boolean redirectErrorStream)
          throws IOException {

      pid = forkAndExec(launchMechanism.ordinal() + 1,
                        helperpath,
                        prog,
                        argBlock, argc,
                        envBlock, envc,
                        dir,
                        fds,
                        redirectErrorStream);
    }
}

-Djava.home 选项指定的是本地通过 cp -r --symbolic-link 拷贝得到并通过 patch_bin_file 进行了 patch 了的 jdk,由于 jspawnhelper 被 patch 成了脚本,因此带来了问题。

因此前面将 pkexec 命令行中的 pid.XXX.temp.jbr/bin/java (这是 java 的 shell 脚本 exec wrapper,与 jspawnhelper shell 脚本一样,通过 launcher.sh 生成)替换成 pid.XXX.temp.jbr/bin/java.bin(符号链接,指向原始的 java)也可以正常运行,因为执行 java.bin 时没有 java 的 shell 脚本里带的 -Djava.home 选项,从而直接从源头规避了调用了 shell 版本 jspawnhelper 所带来的问题。

规避方法一

可以修改 patch_bin_file 的实现,为 extra_arg 增加前面提到的 -Djdk.lang.Process.launchMechanism=vfork 选项:

$ vi ~/.cache/JetBrains/RemoteDev/dist/7d288fc846b32_CLion-233.8264.3/plugins/remote-dev-server/bin/launcher.sh
extra_arg="\"-Djava.home=$TEMP_JBR\" -Djdk.lang.Process.launchMechanism=vfork"

注意这个修改影响的是 java 解释器程序,因为它也是类似于 jspawnhelper 一样被 patch 成了一个 shell 脚本(见 .cache/JetBrains/RemoteDev-CL/_home_runsisi_working_spice-streaming-agent/pid.XXX.temp.jbr/bin/java)

Run with sudo failed, can not debug as root
https://youtrack.jetbrains.com/issue/GO-13971

规避方法二

直接注释掉 launcher.sh 中 patch_bin_file "$TEMP_JBR/lib/jspawnhelper" 的这一行也可以规避问题。

上述规避手段执行之后都记得结束远程的 headless IDE 后端,或者直接删除 headless IDE 后端的工程文件:

$ rm -rf ~/.cache/JetBrains/RemoteDev-CL/

jspawnhelper

jspawnhelper 没有外部依赖,可以基于如下源代码进行构建(但是需要注意 jdk 版本的差异):

In posix_spawn mode, failing to exec() jspawnhelper may not result in an error
https://bugs.openjdk.org/browse/JDK-8223777

childproc.h
https://github.com/openjdk/jdk/blob/jdk-22%2B17/src/java.base/unix/native/libjava/childproc.h

childproc.c
https://github.com/openjdk/jdk/blob/jdk-22%2B17/src/java.base/unix/native/libjava/childproc.c

jspawnhelper.c
https://github.com/openjdk/jdk/blob/jdk-22%2B17/src/java.base/unix/native/jspawnhelper/jspawnhelper.c

argv[0] 对 shell 脚本的影响

如下代码为用于测试 execv/execve 调用,通过 -p 选项可以直接指定子进程程序,此时用户参数从 argv[0] 开始,如果不指定则第一个参数为子进程程序,第二个参数开始为用户参数:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <spawn.h>
#include <sys/wait.h>
#include <errno.h>

// modified from posixspawn
// https://github.com/AlexanderOMara/posixspawn

extern char **environ;

struct argdata {
  char *path;
  bool wait;
  char **args;
};

void usage(FILE *out) {
  fprintf(
      out,
      "Usage: vfork [options...] [--] [args...]\n"
      "\n"
      "options:\n"
      "  -h         Show this help message.\n"
      "  -p <path>  Set the executable path separate from argv[0].\n"
      "  -w         Wait for child process to complete before returning.\n"
      "\n"
      "args:\n"
      "  The remaining arguments are passed to the child process.\n"
      "\n"
  );
}

struct argdata parse_args(int argc, char **argv) {
  struct argdata args;
  // Check for the minimum 1 argument.
  if (argc <= 1) {
    usage(stderr);
    exit(EXIT_FAILURE);
  }
  // Initialize the return data.
  args.path = NULL;
  args.wait = 0;
  args.args = NULL;
  // Parse arguments.
  for (int opt; (opt = getopt(argc, argv, "hvf:p:w")) != -1;) {
    switch (opt) {
      case 'h': {
        usage(stdout);
        exit(EXIT_SUCCESS);
        break;
      }
      case 'p': {
        args.path = optarg;
        break;
      }
      case 'w': {
        args.wait = 1;
        break;
      }
      default: {
        exit(EXIT_FAILURE);
      }
    }
  }
  // Compute remaining arguments, requiring at least one more.
  int args_count = argc - optind;
  if (args_count <= 0 && !args.path) {
    fprintf(stderr, "%s: no executable specified\n", argv[0]);
    exit(EXIT_FAILURE);
  }
  // Initialize memory for the arguments array, plus a null terminator.
  args.args = malloc((args_count + 1) * sizeof(char *));
  // Loop over remaining arguments, inserting them.
  for (int i = optind; i < argc; i++) {
    args.args[i - optind] = argv[i];
  }
  // Add the null terminator.
  args.args[args_count] = NULL;
  return args;
}

int main(int argc, char **argv) {
  struct argdata args = parse_args(argc, argv);
  pid_t pid = vfork();
  if (pid == -1) {
    printf("ERROR: vfork: %s\n", strerror(errno));
    exit(EXIT_FAILURE);
  } else if (pid > 0) {
    // parent
  } else {
    // child
    execve(args.path ? args.path : args.args[0], args.args, environ);
    exit(EXIT_FAILURE); // exec never returns
  }
  printf("PID: %i\n", pid);
  if (args.wait) {
    int status;
    if (waitpid(pid, &status, 0) != -1) {
      printf("EXIT: %i\n", status);
    }
    else {
      perror("ERROR: waitpid");
    }
  }
  return EXIT_SUCCESS;
}

子进程程序 child:

#include <stdio.h>

int main(int argc, const char *argv[]) {
  fprintf(stderr, "argc: %d\n", argc);
  fprintf(stderr, "argv[0]: %s\n", argv[0]);
  fprintf(stderr, "argv[1]: %s\n", argv[1]);

  return 0;
}

封装了 child 程序的 shell 脚本 child.sh:

#!/bin/bash

exec ./child "$@"

测试过程如下:

❯ ./vfork -p ./child 1 2
PID: 59560
argc: 2
argv[0]: 1
argv[1]: 2
❯ ./vfork -p ./child.sh 1 2
PID: 59573
argc: 2
argv[0]: ./child
argv[1]: 2
❯ ./vfork ./child 1 2
PID: 59587
argc: 3
argv[0]: ./child
argv[1]: 1
❯ ./vfork ./child.sh 1 2
PID: 59594
argc: 3
argv[0]: ./child
argv[1]: 1

显然,第二个测试用例证明了使用 execv/execve 系统调用执行 shell 脚本时,如果将用户参数设置为从 argv[0] 开始(通常 argv[0] 是 shell 脚本的路径,而用户参数从 argv[1] 开始)会导致 shell 脚本无法得到正确的 $1 参数(即第一个用户参数被丢弃了,相当于执行了 shift 操作)。


最后修改于 2023-11-15