001/* 002 * Copyright 2025 devteam@scivics-lab.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; 019 020import java.io.File; 021import java.io.FileInputStream; 022import java.io.FileNotFoundException; 023import java.io.IOException; 024import java.io.InputStream; 025import java.util.concurrent.ExecutionException; 026import java.util.logging.Level; 027import java.util.logging.Logger; 028 029import org.json.JSONArray; 030import org.json.JSONObject; 031 032import com.github.lalyos.jfiglet.FigletFont; 033import com.scivicslab.pojoactor.core.ActionResult; 034import com.scivicslab.pojoactor.workflow.IIActorRef; 035import com.scivicslab.pojoactor.workflow.IIActorSystem; 036 037/** 038 * Interpreter-interfaced actor reference for {@link NodeInterpreter} instances. 039 * 040 * <p>This class provides a concrete implementation of {@link IIActorRef} 041 * specifically for {@link NodeInterpreter} objects. It handles action invocations 042 * by name, supporting both workflow execution actions (inherited from Interpreter) 043 * and infrastructure actions (SSH command execution).</p> 044 * 045 * <p><strong>Supported actions:</strong></p> 046 * <p><em>Workflow actions (from Interpreter):</em></p> 047 * <ul> 048 * <li>{@code execCode} - Executes the loaded workflow code</li> 049 * <li>{@code readYaml} - Reads a YAML workflow definition from a file path</li> 050 * <li>{@code readJson} - Reads a JSON workflow definition from a file path</li> 051 * <li>{@code readXml} - Reads an XML workflow definition from a file path</li> 052 * <li>{@code reset} - Resets the interpreter state</li> 053 * </ul> 054 * <p><em>Infrastructure actions (Node-specific):</em></p> 055 * <ul> 056 * <li>{@code executeCommand} - Executes a command and reports to accumulator (default)</li> 057 * <li>{@code executeCommandQuiet} - Executes a command without reporting</li> 058 * <li>{@code executeSudoCommand} - Executes sudo command and reports to accumulator (default)</li> 059 * <li>{@code executeSudoCommandQuiet} - Executes sudo command without reporting</li> 060 * </ul> 061 * 062 * <p><strong>Example YAML Workflow:</strong></p> 063 * <pre>{@code 064 * name: deploy-application 065 * steps: 066 * - states: ["0", "1"] 067 * actions: 068 * - actor: this 069 * method: executeCommand 070 * arguments: 071 * - "apt-get update" 072 * - states: ["1", "end"] 073 * actions: 074 * - actor: this 075 * method: executeCommand 076 * arguments: 077 * - "ls -la" 078 * }</pre> 079 * 080 * @author devteam@scivics-lab.com 081 */ 082public class NodeIIAR extends IIActorRef<NodeInterpreter> { 083 084 Logger logger = null; 085 086 /** 087 * Constructs a new NodeIIAR with the specified actor name and node interpreter object. 088 * 089 * @param actorName the name of this actor 090 * @param object the {@link NodeInterpreter} instance managed by this actor reference 091 */ 092 public NodeIIAR(String actorName, NodeInterpreter object) { 093 super(actorName, object); 094 logger = Logger.getLogger(actorName); 095 } 096 097 /** 098 * Constructs a new NodeIIAR with the specified actor name, node interpreter object, 099 * and actor system. 100 * 101 * @param actorName the name of this actor 102 * @param object the {@link NodeInterpreter} instance managed by this actor reference 103 * @param system the actor system managing this actor 104 */ 105 public NodeIIAR(String actorName, NodeInterpreter object, IIActorSystem system) { 106 super(actorName, object, system); 107 logger = Logger.getLogger(actorName); 108 109 // Set the selfActorRef in the Interpreter (NodeInterpreter extends Interpreter) 110 object.setSelfActorRef(this); 111 } 112 113 /** 114 * Invokes an action on the node by name with the given arguments. 115 * 116 * <p>This method dispatches to specialized handler methods based on the action type:</p> 117 * <ul> 118 * <li>Workflow actions: {@link #handleWorkflowAction}</li> 119 * <li>Command execution actions: {@link #handleCommandAction}</li> 120 * <li>Utility actions: {@link #handleUtilityAction}</li> 121 * </ul> 122 * 123 * @param actionName the name of the action to execute 124 * @param arg the argument string (file path for read operations, JSON array for commands) 125 * @return an {@link ActionResult} indicating success or failure with a message 126 */ 127 @Override 128 public ActionResult callByActionName(String actionName, String arg) { 129 logger.fine(String.format("actionName = %s, args = %s", actionName, arg)); 130 131 try { 132 // Workflow execution actions (from Interpreter) 133 ActionResult workflowResult = handleWorkflowAction(actionName, arg); 134 if (workflowResult != null) { 135 return workflowResult; 136 } 137 138 // Node-specific actions (SSH command execution) 139 ActionResult commandResult = handleCommandAction(actionName, arg); 140 if (commandResult != null) { 141 return commandResult; 142 } 143 144 // Utility actions 145 ActionResult utilityResult = handleUtilityAction(actionName, arg); 146 if (utilityResult != null) { 147 return utilityResult; 148 } 149 150 // Document workflow actions 151 ActionResult documentResult = handleDocumentAction(actionName, arg); 152 if (documentResult != null) { 153 return documentResult; 154 } 155 156 // Unknown action 157 logger.log(Level.SEVERE, String.format("Unknown action: actorName = %s, action = %s, arg = %s", 158 this.getName(), actionName, arg)); 159 return new ActionResult(false, "Unknown action: " + actionName); 160 161 } catch (InterruptedException e) { 162 String message = "Interrupted: " + e.getMessage(); 163 logger.warning(String.format("%s: %s", this.getName(), message)); 164 return new ActionResult(false, message); 165 } catch (ExecutionException e) { 166 String message = extractRootCauseMessage(e); 167 logger.warning(String.format("%s: %s", this.getName(), message)); 168 return new ActionResult(false, message); 169 } 170 } 171 172 /** 173 * Handles workflow-related actions (from Interpreter). 174 * 175 * @param actionName the action name 176 * @param arg the argument string 177 * @return ActionResult if handled, null if not a workflow action 178 */ 179 private ActionResult handleWorkflowAction(String actionName, String arg) 180 throws InterruptedException, ExecutionException { 181 182 switch (actionName) { 183 case "execCode": 184 return this.ask(n -> n.execCode()).get(); 185 186 case "readYaml": 187 return handleReadYaml(arg); 188 189 case "readJson": 190 return handleReadJson(arg); 191 192 case "readXml": 193 return handleReadXml(arg); 194 195 case "reset": 196 this.tell(n -> n.reset()).get(); 197 return new ActionResult(true, "Interpreter reset successfully"); 198 199 case "runUntilEnd": 200 int maxIterations = parseMaxIterations(arg, 10000); 201 return this.ask(n -> n.runUntilEnd(maxIterations)).get(); 202 203 case "call": 204 JSONArray callArgs = new JSONArray(arg); 205 String callWorkflowFile = callArgs.getString(0); 206 return this.ask(n -> n.call(callWorkflowFile)).get(); 207 208 case "runWorkflow": 209 JSONArray runArgs = new JSONArray(arg); 210 String runWorkflowFile = runArgs.getString(0); 211 int runMaxIterations = runArgs.length() > 1 ? runArgs.getInt(1) : 10000; 212 logger.fine(String.format("Running workflow: %s (maxIterations=%d)", runWorkflowFile, runMaxIterations)); 213 ActionResult result = this.object.runWorkflow(runWorkflowFile, runMaxIterations); 214 logger.fine(String.format("Workflow completed: success=%s, result=%s", result.isSuccess(), result.getResult())); 215 return result; 216 217 case "apply": 218 return this.ask(n -> n.apply(arg)).get(); 219 220 default: 221 return null; // Not a workflow action 222 } 223 } 224 225 /** 226 * Handles SSH command execution actions. 227 * 228 * @param actionName the action name 229 * @param arg the argument string 230 * @return ActionResult if handled, null if not a command action 231 */ 232 private ActionResult handleCommandAction(String actionName, String arg) 233 throws InterruptedException, ExecutionException { 234 235 switch (actionName) { 236 case "executeCommandQuiet": 237 return executeCommandQuiet(arg); 238 239 case "executeSudoCommandQuiet": 240 return executeSudoCommandQuiet(arg); 241 242 case "executeCommand": 243 return executeCommandWithReport(arg); 244 245 case "executeSudoCommand": 246 return executeSudoCommandWithReport(arg); 247 248 default: 249 return null; // Not a command action 250 } 251 } 252 253 /** 254 * Handles utility actions (sleep, print, doNothing). 255 * 256 * @param actionName the action name 257 * @param arg the argument string 258 * @return ActionResult if handled, null if not a utility action 259 */ 260 private ActionResult handleUtilityAction(String actionName, String arg) { 261 switch (actionName) { 262 case "sleep": 263 try { 264 long millis = Long.parseLong(arg); 265 Thread.sleep(millis); 266 return new ActionResult(true, "Slept for " + millis + "ms"); 267 } catch (NumberFormatException e) { 268 logger.log(Level.SEVERE, String.format("Invalid sleep duration: %s", arg), e); 269 return new ActionResult(false, "Invalid sleep duration: " + arg); 270 } catch (InterruptedException e) { 271 return new ActionResult(false, "Sleep interrupted: " + e.getMessage()); 272 } 273 274 case "print": 275 System.out.println(arg); 276 return new ActionResult(true, "Printed: " + arg); 277 278 case "doNothing": 279 return new ActionResult(true, arg); 280 281 default: 282 return null; // Not a utility action 283 } 284 } 285 286 /** 287 * Handles document workflow actions (detect, clone, build, deploy). 288 * 289 * @param actionName the action name 290 * @param arg the argument string 291 * @return ActionResult if handled, null if not a document action 292 */ 293 private ActionResult handleDocumentAction(String actionName, String arg) 294 throws InterruptedException, ExecutionException { 295 296 switch (actionName) { 297 case "detectDocumentChanges": 298 return this.ask(n -> { 299 try { 300 String docListPath = extractCommandFromArgs(arg); 301 return n.detectDocumentChanges(docListPath); 302 } catch (IOException e) { 303 return new ActionResult(false, "Error detecting changes: " + e.getMessage()); 304 } 305 }).get(); 306 307 case "cloneChangedDocuments": 308 return this.ask(n -> { 309 try { 310 String docListPath = extractCommandFromArgs(arg); 311 return n.cloneChangedDocuments(docListPath); 312 } catch (IOException e) { 313 return new ActionResult(false, "Error cloning documents: " + e.getMessage()); 314 } 315 }).get(); 316 317 case "buildChangedDocuments": 318 return this.ask(n -> { 319 try { 320 String docListPath = extractCommandFromArgs(arg); 321 return n.buildChangedDocuments(docListPath); 322 } catch (IOException e) { 323 return new ActionResult(false, "Error building documents: " + e.getMessage()); 324 } 325 }).get(); 326 327 case "deployChangedDocuments": 328 return this.ask(n -> { 329 try { 330 String docListPath = extractCommandFromArgs(arg); 331 return n.deployChangedDocuments(docListPath); 332 } catch (IOException e) { 333 return new ActionResult(false, "Error deploying documents: " + e.getMessage()); 334 } 335 }).get(); 336 337 default: 338 return null; // Not a document action 339 } 340 } 341 342 // --- Helper methods for workflow actions --- 343 344 private ActionResult handleReadYaml(String arg) throws InterruptedException, ExecutionException { 345 try { 346 String overlayPath = this.object.getOverlayDir(); 347 if (overlayPath != null) { 348 java.nio.file.Path yamlPath = java.nio.file.Path.of(arg); 349 java.nio.file.Path overlayDir = java.nio.file.Path.of(overlayPath); 350 this.tell(n -> { 351 try { 352 n.readYaml(yamlPath, overlayDir); 353 } catch (IOException e) { 354 throw new RuntimeException(e); 355 } 356 }).get(); 357 return new ActionResult(true, "YAML loaded with overlay: " + overlayPath); 358 } else { 359 try (InputStream input = new FileInputStream(new File(arg))) { 360 this.tell(n -> n.readYaml(input)).get(); 361 return new ActionResult(true, "YAML loaded successfully"); 362 } 363 } 364 } catch (FileNotFoundException e) { 365 logger.log(Level.SEVERE, String.format("file not found: %s", arg), e); 366 return new ActionResult(false, "File not found: " + arg); 367 } catch (IOException e) { 368 logger.log(Level.SEVERE, String.format("IOException: %s", arg), e); 369 return new ActionResult(false, "IO error: " + arg); 370 } catch (RuntimeException e) { 371 if (e.getCause() instanceof IOException) { 372 logger.log(Level.SEVERE, String.format("IOException: %s", arg), e.getCause()); 373 return new ActionResult(false, "IO error: " + arg); 374 } 375 throw e; 376 } 377 } 378 379 private ActionResult handleReadJson(String arg) throws InterruptedException, ExecutionException { 380 try (InputStream input = new FileInputStream(new File(arg))) { 381 this.tell(n -> { 382 try { 383 n.readJson(input); 384 } catch (IOException e) { 385 throw new RuntimeException(e); 386 } 387 }).get(); 388 return new ActionResult(true, "JSON loaded successfully"); 389 } catch (FileNotFoundException e) { 390 logger.log(Level.SEVERE, String.format("file not found: %s", arg), e); 391 return new ActionResult(false, "File not found: " + arg); 392 } catch (IOException e) { 393 logger.log(Level.SEVERE, String.format("IOException: %s", arg), e); 394 return new ActionResult(false, "IO error: " + arg); 395 } 396 } 397 398 private ActionResult handleReadXml(String arg) throws InterruptedException, ExecutionException { 399 try (InputStream input = new FileInputStream(new File(arg))) { 400 this.tell(n -> { 401 try { 402 n.readXml(input); 403 } catch (Exception e) { 404 throw new RuntimeException(e); 405 } 406 }).get(); 407 return new ActionResult(true, "XML loaded successfully"); 408 } catch (FileNotFoundException e) { 409 logger.log(Level.SEVERE, String.format("file not found: %s", arg), e); 410 return new ActionResult(false, "File not found: " + arg); 411 } catch (Exception e) { 412 logger.log(Level.SEVERE, String.format("Exception: %s", arg), e); 413 return new ActionResult(false, "Error: " + arg); 414 } 415 } 416 417 private int parseMaxIterations(String arg, int defaultValue) { 418 if (arg != null && !arg.isEmpty() && !arg.equals("[]")) { 419 try { 420 JSONArray args = new JSONArray(arg); 421 if (args.length() > 0) { 422 return args.getInt(0); 423 } 424 } catch (Exception e) { 425 // Use default if parsing fails 426 } 427 } 428 return defaultValue; 429 } 430 431 // --- Helper methods for command actions --- 432 433 private ActionResult executeCommandQuiet(String arg) throws InterruptedException, ExecutionException { 434 String command = extractCommandFromArgs(arg); 435 Node.CommandResult result = this.ask(n -> { 436 try { 437 return n.executeCommand(command); 438 } catch (IOException e) { 439 throw new RuntimeException(e); 440 } 441 }).get(); 442 443 return new ActionResult(result.isSuccess(), 444 String.format("exitCode=%d, stdout='%s', stderr='%s'", 445 result.getExitCode(), result.getStdout(), result.getStderr())); 446 } 447 448 private ActionResult executeSudoCommandQuiet(String arg) throws InterruptedException, ExecutionException { 449 String command = extractCommandFromArgs(arg); 450 Node.CommandResult result = this.ask(n -> { 451 try { 452 return n.executeSudoCommand(command); 453 } catch (IOException e) { 454 throw new RuntimeException(e); 455 } 456 }).get(); 457 458 return new ActionResult(result.isSuccess(), 459 String.format("exitCode=%d, stdout='%s', stderr='%s'", 460 result.getExitCode(), result.getStdout(), result.getStderr())); 461 } 462 463 private ActionResult executeCommandWithReport(String arg) throws InterruptedException, ExecutionException { 464 String command = extractCommandFromArgs(arg); 465 Node.CommandResult result = this.ask(n -> { 466 try { 467 return n.executeCommand(command); 468 } catch (IOException e) { 469 throw new RuntimeException(e); 470 } 471 }).get(); 472 473 reportToAccumulator(result); 474 return new ActionResult(result.isSuccess(), combineOutput(result)); 475 } 476 477 private ActionResult executeSudoCommandWithReport(String arg) throws InterruptedException, ExecutionException { 478 String command = extractCommandFromArgs(arg); 479 Node.CommandResult result = this.ask(n -> { 480 try { 481 return n.executeSudoCommand(command); 482 } catch (IOException e) { 483 throw new RuntimeException(e); 484 } 485 }).get(); 486 487 reportToAccumulator(result); 488 return new ActionResult(result.isSuccess(), combineOutput(result)); 489 } 490 491 /** 492 * Reports command result to the accumulator actor if available. 493 */ 494 private void reportToAccumulator(Node.CommandResult result) { 495 IIActorSystem sys = (IIActorSystem) this.system(); 496 IIActorRef<?> accumulator = sys.getIIActor("accumulator"); 497 if (accumulator != null) { 498 JSONObject reportArg = new JSONObject(); 499 reportArg.put("source", this.getName()); 500 reportArg.put("type", this.object.getCurrentVertexYaml()); 501 reportArg.put("data", combineOutput(result)); 502 accumulator.callByActionName("add", reportArg.toString()); 503 } 504 } 505 506 /** 507 * Combines stdout and stderr into a single output string. 508 */ 509 private String combineOutput(Node.CommandResult result) { 510 String output = result.getStdout().trim(); 511 String stderr = result.getStderr().trim(); 512 if (!stderr.isEmpty()) { 513 output = output.isEmpty() ? stderr : output + "\n[stderr]\n" + stderr; 514 } 515 return output; 516 } 517 518 /** 519 * Extracts the root cause message from an ExecutionException. 520 */ 521 private String extractRootCauseMessage(ExecutionException e) { 522 Throwable cause = e.getCause(); 523 while (cause != null && cause.getCause() != null) { 524 cause = cause.getCause(); 525 } 526 return cause != null ? cause.getMessage() : e.getMessage(); 527 } 528 529 /** 530 * Extracts a command string from JSON array arguments. 531 * 532 * <p>Expects arguments in the format: {@code ["command string"]}</p> 533 * 534 * @param arg the JSON array argument string 535 * @return the extracted command string 536 * @throws IllegalArgumentException if the argument format is invalid 537 */ 538 private String extractCommandFromArgs(String arg) { 539 try { 540 JSONArray jsonArray = new JSONArray(arg); 541 if (jsonArray.length() == 0) { 542 throw new IllegalArgumentException("Command arguments cannot be empty"); 543 } 544 return jsonArray.getString(0); 545 } catch (Exception e) { 546 throw new IllegalArgumentException( 547 "Invalid command argument format. Expected JSON array with command string: " + arg, e); 548 } 549 } 550 551}