You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java使用ProcessBuilder调用openconnect建立VPN的阻塞问题求助

解决Java ProcessBuilder调用OpenConnect VPN的阻塞与交互式密码输入问题

原代码的核心问题

  • 同步读取标准输出和错误流会导致阻塞:readLine()是阻塞方法,当其中一个流无数据时,线程会卡住,无法响应另一个流的密码提示。
  • 仅处理单次密码请求:未考虑可能出现的多轮密码验证(比如sudo权限密码+VPN二次验证密码)。

修复原生ProcessBuilder方案

通过多线程分离标准输出/错误流的读取避免阻塞,同时支持多轮密码输入,示例代码如下:

import java.io.*;
import java.util.Scanner;

public class VPNConnectionHandler {
    private static Scanner consoleScanner = new Scanner(System.in);
    private static OutputStreamWriter processOutput;

    public static void main(String[] args) {
        String[] command = {"sudo", "openconnect", "--background", "--protocol=nc", "--user=user", "serveradress"};

        try {
            ProcessBuilder processBuilder = new ProcessBuilder(command);
            Process process = processBuilder.start();

            // 初始化进程输出流,用于发送密码
            processOutput = new OutputStreamWriter(process.getOutputStream());

            // 启动独立线程处理标准输出和错误输出
            new Thread(() -> handleStream(process.getInputStream())).start();
            new Thread(() -> handleStream(process.getErrorStream())).start();

            // 等待进程结束
            int exitCode = process.waitFor();
            System.out.println("Process exited with code " + exitCode);
            processOutput.close();

        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void handleStream(InputStream inputStream) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
                // 匹配所有包含密码提示的行(忽略大小写)
                if (line.toLowerCase().contains("password:")) {
                    System.out.print("请输入密码: ");
                    String password = consoleScanner.nextLine();
                    // 同步写入避免多线程冲突
                    synchronized (processOutput) {
                        processOutput.write(password + "\n");
                        processOutput.flush();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

关键说明

  • 多线程分离流读取:避免单线程因某一流无数据而阻塞,确保能及时响应所有提示。
  • 全局输出流+同步写入:保证多线程环境下密码输入的正确性,避免流操作冲突。
  • 通用密码匹配:兼容sudo和VPN的各类密码提示,支持多轮输入。

使用ExpectIt简化交互式处理

ExpectIt是专门处理交互式进程的工具,可自动匹配输出提示并发送输入,比原生代码更简洁可靠。

依赖配置(Maven)

<dependency>
    <groupId>net.sf.expectit</groupId>
    <artifactId>expectit-core</artifactId>
    <version>0.9.0</version>
</dependency>

示例代码

import net.sf.expectit.Expect;
import net.sf.expectit.ExpectBuilder;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

public class VPNExpectHandler {
    public static void main(String[] args) throws IOException, InterruptedException {
        Scanner consoleScanner = new Scanner(System.in);
        String[] command = {"sudo", "openconnect", "--protocol=nc", "--user=user", "serveradress"};

        Process process = new ProcessBuilder(command).start();

        try (Expect expect = new ExpectBuilder()
                .withOutput(process.getOutputStream())
                .withInputs(process.getInputStream(), process.getErrorStream())
                .withEchoOutput(System.out)
                .withEchoInput(System.out)
                .withTimeout(15, TimeUnit.SECONDS)
                .build()) {

            // 匹配首次密码提示(如sudo的权限验证)
            expect.expect("(?i)password:");
            System.out.print("请输入密码: ");
            expect.sendLine(consoleScanner.nextLine());

            // 尝试匹配二次密码提示(无提示则超时跳过)
            try {
                expect.expect("(?i)password:");
                System.out.print("请输入二次验证密码: ");
                expect.sendLine(consoleScanner.nextLine());
            } catch (Exception e) {
                System.out.println("未检测到二次密码提示");
            }

            // 匹配openconnect后台运行的提示,确认连接成功
            expect.expect("Backgrounding daemon");

            int exitCode = process.waitFor();
            System.out.println("Process exited with code " + exitCode);
        }
    }
}

关键说明

  • 忽略大小写匹配:通过(?i)兼容"Password:""password:"等不同大小写的提示。
  • 超时处理:通过withTimeout控制等待提示的时长,自动跳过不存在的二次验证。
  • 自动流管理:无需手动创建线程,工具会自动处理输入输出的交互逻辑。

关于inheritIO()的说明

使用inheritIO()后,进程的输入输出直接绑定到Java进程的控制台,此时Java代码无法拦截或处理输入,用户需直接在控制台输入密码。该方式适合无需代码干预的场景,但如果要通过代码控制输入流程,需禁用此配置。

内容的提问来源于stack exchange,提问作者rilent

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 15:04:53