Android 执行 Linux 命令:无限读取的优雅处理与主动终止

102 阅读3分钟

问题背景

在开发 Android 应用时,我们经常需要执行 Linux 命令并处理其输出。但当命令产生​​无限输出流​​时,我们需要:

  1. 实时处理输出内容

  2. 在满足特定条件时主动终止命令

  3. 区分处理不同类型的事件(成功、错误、普通日志)

本文将介绍如何优雅解决这些问题,并提供完整的实现方案。

核心实现

1. 命令执行与流处理

private void execShellCommand(String path) {
    String command = "cat " + path;
    Process process = null;
    
    try {
        process = Runtime.getRuntime().exec(command);
        
        // 处理错误流(防止阻塞)
        new Thread(() -> {
            try (BufferedReader errorReader = new BufferedReader(
                new InputStreamReader(process.getErrorStream()))) {
                
                String errorLine;
                while ((errorLine = errorReader.readLine()) != null) {
                    handleErrorLog("test_result error: " + errorLine);
                }
            } catch (IOException e) {
                Log.e(TAG, "Error reading error stream: ", e);
            }
        }).start();
        
        // 处理输入流
        new Thread(() -> {
            try (BufferedReader inputReader = new BufferedReader(
                new InputStreamReader(process.getInputStream()))) {
                
                String line;
                while ((line = inputReader.readLine()) != null) {
                    handleOutputLog(line);
                    
                    // 检查终止条件
                    if (line.contains("success")) {
                        handleSuccessLog("Status: SUCCESS");
                        terminateProcess(process);
                        break;
                    }
                }
            } catch (IOException e) {
                Log.e(TAG, "Error reading input stream: ", e);
            }
        }).start();
        
        // 等待进程结束
        int exitCode = process.waitFor();
        Log.w(TAG, "test_result exit with code: " + exitCode);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

2. 优雅终止进程

@RequiresApi(api = Build.VERSION_CODES.O)
private void terminateProcess(Process process) {
    if (process == null) return;
    
    try {
        // 尝试发送 Ctrl+C 信号
        OutputStream os = process.getOutputStream();
        os.write(3); // ASCII 3 = ETX (End of Text)
        os.flush();
        
        // 等待短暂时间
        Thread.sleep(100);
        
        if (process.isAlive()) {
            // 强制终止
            process.destroyForcibly();
            Log.w(TAG, "进程被强制终止");
        }
    } catch (Exception e) {
        Log.e(TAG, "终止进程失败", e);
        process.destroyForcibly();
    }
}

3. 事件处理与 UI 更新

// 使用 Handler 在主线程更新 UI
private final Handler mLogHandler = new LogHandler(Looper.getMainLooper());

private static class LogHandler extends Handler {
    public LogHandler(Looper looper) {
        super(looper);
    }
    
    @Override
    public void handleMessage(@NonNull Message msg) {
        switch (msg.what) {
            case COMMAND_UPDATE_LOG:
                Log.d(TAG, "普通日志: " + msg.obj);
                break;
            case COMMAND_ERROR_LOG:
                Log.e(TAG, "错误日志: " + msg.obj);
                break;
            case COMMAND_SUCCESS_LOG:
                Log.i(TAG, "成功日志: " + msg.obj);
                break;
        }
    }
}

// 处理不同类型的事件
private void handleOutputLog(String message) {
    Message msg = new Message();
    msg.what = COMMAND_UPDATE_LOG;
    msg.obj = message;
    mLogHandler.sendMessage(msg);
}

private void handleErrorLog(String message) {
    Message msg = new Message();
    msg.what = COMMAND_ERROR_LOG;
    msg.obj = message;
    mLogHandler.sendMessage(msg);
}

private void handleSuccessLog(String message) {
    Message msg = new Message();
    msg.what = COMMAND_SUCCESS_LOG;
    msg.obj = message;
    mLogHandler.sendMessage(msg);
}

关键设计要点

1. 多线程流处理

线程功能重要性
​主线程​UI 更新和用户交互
​输入流线程​读取命令标准输出
​错误流线程​读取命令错误输出

2. 事件分类处理

事件类型标识处理方式
普通日志COMMAND_UPDATE_LOG记录并显示
错误日志COMMAND_ERROR_LOG记录并告警
成功事件COMMAND_SUCCESS_LOG终止进程并通知

完整实现代码

package com.android.myapplication;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.Button;

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

public class MainActivity extends AppCompatActivity {

    private Button button;
    private static final String TAG = "CommandExecutor";
    private static final int COMMAND_UPDATE_LOG = 1;
    private static final int COMMAND_ERROR_LOG = 2;
    private static final int COMMAND_SUCCESS_LOG = 3;
    
    private final Handler mLogHandler = new LogHandler(Looper.getMainLooper());
    
    private static class LogHandler extends Handler {
        public LogHandler(Looper looper) {
            super(looper);
        }
        
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what) {
                case COMMAND_UPDATE_LOG:
                    Log.d(TAG, "普通日志: " + msg.obj);
                    break;
                case COMMAND_ERROR_LOG:
                    Log.e(TAG, "错误日志: " + msg.obj);
                    break;
                case COMMAND_SUCCESS_LOG:
                    Log.i(TAG, "成功日志: " + msg.obj);
                    break;
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        button = findViewById(R.id.button);
        button.setOnClickListener(v -> {
            execShellCommand("/data/local/tmp/test_path");
        });
    }
    
    private void execShellCommand(String path) {
        String command = "cat " + path;
        
        try {
            Process process = Runtime.getRuntime().exec(command);
            
            // 处理错误流(防止阻塞)
            new Thread(() -> {
                try (BufferedReader errorReader = new BufferedReader(
                    new InputStreamReader(process.getErrorStream()))) {
                    
                    String errorLine;
                    while ((errorLine = errorReader.readLine()) != null) {
                        handleErrorLog("错误: " + errorLine);
                    }
                } catch (IOException e) {
                    Log.e(TAG, "读取错误流失败: ", e);
                }
            }).start();
            
            // 处理输入流
            new Thread(() -> {
                try (BufferedReader inputReader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()))) {
                    
                    String line;
                    while ((line = inputReader.readLine()) != null) {
                        handleOutputLog(line);
                        
                        // 检查终止条件
                        if (line.contains("success")) {
                            handleSuccessLog("状态: 成功");
                            terminateProcess(process);
                            break;
                        }
                    }
                } catch (IOException e) {
                    Log.e(TAG, "读取输入流失败: ", e);
                }
            }).start();
            
            // 等待进程结束
            int exitCode = process.waitFor();
            Log.w(TAG, "命令退出代码: " + exitCode);
        } catch (Exception e) {
            Log.e(TAG, "执行命令失败", e);
        }
    }
    
    @RequiresApi(api = Build.VERSION_CODES.O)
    private void terminateProcess(Process process) {
        if (process == null) return;
        
        try {
            // 尝试优雅终止
            OutputStream os = process.getOutputStream();
            os.write(3); // 发送 Ctrl+C 信号
            os.flush();
            
            // 短暂等待
            Thread.sleep(100);
            
            if (process.isAlive()) {
                // 强制终止
                process.destroyForcibly();
                Log.w(TAG, "进程被强制终止");
            }
        } catch (Exception e) {
            Log.e(TAG, "终止进程失败", e);
            process.destroyForcibly();
        }
    }
    
    private void handleOutputLog(String message) {
        Message msg = new Message();
        msg.what = COMMAND_UPDATE_LOG;
        msg.obj = message;
        mLogHandler.sendMessage(msg);
    }
    
    private void handleErrorLog(String message) {
        Message msg = new Message();
        msg.what = COMMAND_ERROR_LOG;
        msg.obj = message;
        mLogHandler.sendMessage(msg);
    }
    
    private void handleSuccessLog(String message) {
        Message msg = new Message();
        msg.what = COMMAND_SUCCESS_LOG;
        msg.obj = message;
        mLogHandler.sendMessage(msg);
    }
}