Jetbrains 比较典型的远程开发方式有两种,一种是 remote with local sources,一种是 remote with gateway。
remote with local sources 关键的配置是工具链的配置,如下图所示:
需要注意的是 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
在本地机器连接远端服务:
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
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