001/* 002 * Copyright 2025 devteam@scivicslab.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, 011 * software distributed under the License is distributed on an 012 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the 014 * specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package com.scivicslab.actoriac.mixin; 019 020import java.io.BufferedReader; 021import java.io.IOException; 022import java.io.InputStreamReader; 023import java.net.InetAddress; 024import java.util.concurrent.TimeUnit; 025 026import com.scivicslab.actoriac.Node; 027 028/** 029 * Command executor that executes commands on the local machine. 030 * 031 * <p>Used by NodeGroupInterpreter to execute commands locally when running 032 * on the control node itself.</p> 033 * 034 * @author devteam@scivicslab.com 035 * @since 2.15.0 036 */ 037public class LocalCommandExecutor implements CommandExecutor { 038 039 private static final long DEFAULT_TIMEOUT_SECONDS = 300; 040 041 private final String hostname; 042 043 /** 044 * Constructs a local command executor. 045 */ 046 public LocalCommandExecutor() { 047 String h; 048 try { 049 h = InetAddress.getLocalHost().getHostName(); 050 } catch (Exception e) { 051 h = "localhost"; 052 } 053 this.hostname = h; 054 } 055 056 @Override 057 public Node.CommandResult execute(String command) throws IOException { 058 return execute(command, null); 059 } 060 061 @Override 062 public Node.CommandResult execute(String command, Node.OutputCallback callback) throws IOException { 063 ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", command); 064 pb.redirectErrorStream(false); 065 066 Process process = pb.start(); 067 068 StringBuilder stdout = new StringBuilder(); 069 StringBuilder stderr = new StringBuilder(); 070 071 // Read stdout 072 Thread stdoutThread = new Thread(() -> { 073 try (BufferedReader reader = new BufferedReader( 074 new InputStreamReader(process.getInputStream()))) { 075 String line; 076 while ((line = reader.readLine()) != null) { 077 stdout.append(line).append("\n"); 078 if (callback != null) { 079 callback.onStdout(line); 080 } 081 } 082 } catch (IOException e) { 083 // Ignore 084 } 085 }); 086 087 // Read stderr 088 Thread stderrThread = new Thread(() -> { 089 try (BufferedReader reader = new BufferedReader( 090 new InputStreamReader(process.getErrorStream()))) { 091 String line; 092 while ((line = reader.readLine()) != null) { 093 stderr.append(line).append("\n"); 094 if (callback != null) { 095 callback.onStderr(line); 096 } 097 } 098 } catch (IOException e) { 099 // Ignore 100 } 101 }); 102 103 stdoutThread.start(); 104 stderrThread.start(); 105 106 try { 107 boolean finished = process.waitFor(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); 108 if (!finished) { 109 process.destroyForcibly(); 110 return new Node.CommandResult(stdout.toString(), "Command timed out", -1); 111 } 112 113 stdoutThread.join(1000); 114 stderrThread.join(1000); 115 116 int exitCode = process.exitValue(); 117 return new Node.CommandResult(stdout.toString().trim(), stderr.toString().trim(), exitCode); 118 119 } catch (InterruptedException e) { 120 Thread.currentThread().interrupt(); 121 process.destroyForcibly(); 122 return new Node.CommandResult(stdout.toString(), "Interrupted: " + e.getMessage(), -1); 123 } 124 } 125 126 @Override 127 public Node.CommandResult executeSudo(String command) throws IOException { 128 return executeSudo(command, null); 129 } 130 131 @Override 132 public Node.CommandResult executeSudo(String command, Node.OutputCallback callback) throws IOException { 133 String sudoPassword = System.getenv("SUDO_PASSWORD"); 134 if (sudoPassword == null || sudoPassword.isEmpty()) { 135 throw new IOException("SUDO_PASSWORD environment variable is not set"); 136 } 137 138 // Use sudo with stdin password 139 String sudoCommand = String.format("echo '%s' | sudo -S %s", 140 sudoPassword.replace("'", "'\\''"), command); 141 142 return execute(sudoCommand, callback); 143 } 144 145 @Override 146 public String getIdentifier() { 147 return hostname; 148 } 149}