executeCommand static method

Future<ProcessResult> executeCommand(
  1. String command, {
  2. Map<String, String>? env,
  3. String? exitCondition,
  4. String? workingDirectory,
  5. int? timeoutSeconds,
})

Execute Pipeline stages commands with improved robustness.

Supports optional workingDirectory and per-step env variables.

Implementation

static Future<ProcessResult> executeCommand(
  String command, {
  Map<String, String>? env,
  String? exitCondition,
  String? workingDirectory,
  int? timeoutSeconds,
}) async {
  if (command.trim().isEmpty) {
    print('āš ļø Error: Command is empty. Skipping execution.');
    return ProcessResult(0, 1, '', 'Command is empty');
  }

  final shellType = Platform.isWindows ? 'powershell' : 'bash';
  final shellFlag = Platform.isWindows ? '-Command' : '-c';

  print('šŸ”§ Executing Command: $command');
  if (workingDirectory != null) {
    print('   šŸ“‚ Working Directory: $workingDirectory');
  }
  if (env != null && env.isNotEmpty) {
    print('   šŸ”‘ Environment: ${env.keys.join(', ')}');
  }

  // Validate working directory
  if (workingDirectory != null) {
    final dir = Directory(workingDirectory);
    if (!dir.existsSync()) {
      print(
          'āŒ Working directory "$workingDirectory" does not exist. Aborting step.');
      return ProcessResult(
          0, 1, '', 'Working directory does not exist: $workingDirectory');
    }
  }

  // Merge environment variables (system env + step env)
  Map<String, String>? mergedEnv;
  if (env != null && env.isNotEmpty) {
    mergedEnv = Map<String, String>.from(Platform.environment);
    mergedEnv.addAll(env);
  }

  // Start the process
  final process = await Process.start(
    shellType,
    [shellFlag, command],
    environment: mergedEnv,
    workingDirectory: workingDirectory,
    runInShell: true,
  );

  // Store output in memory
  StringBuffer stdoutBuffer = StringBuffer();
  StringBuffer stderrBuffer = StringBuffer();

  // Listen to stdout
  stdout.write('šŸ“œ Output:');
  final stdoutSubscription =
      process.stdout.transform(SystemEncoding().decoder).listen((data) {
    stdout.write(data);
    stdoutBuffer.write(data);
  });

  // Listen to stderr
  final stderrSubscription =
      process.stderr.transform(SystemEncoding().decoder).listen((data) {
    stderr.write('āš ļø Error: $data');
    stderrBuffer.write(data);
  });

  // Wait for completion with optional timeout
  int exitCode;
  try {
    if (timeoutSeconds != null && timeoutSeconds > 0) {
      exitCode = await process.exitCode
          .timeout(Duration(seconds: timeoutSeconds), onTimeout: () {
        print(
            '\nā° Step timed out after ${timeoutSeconds}s. Killing process...');
        process.kill(ProcessSignal.sigterm);
        // Give it a moment to terminate gracefully
        return Future.delayed(const Duration(seconds: 2), () {
          process.kill(ProcessSignal.sigkill);
          return -1;
        });
      });
    } else {
      exitCode = await process.exitCode;
    }
  } catch (e) {
    print('\nā° Step execution error: $e');
    process.kill(ProcessSignal.sigkill);
    exitCode = -1;
  }

  // Cancel subscriptions
  await stdoutSubscription.cancel();
  await stderrSubscription.cancel();

  // Get collected output
  final stdoutData = stdoutBuffer.toString();
  final stderrData = stderrBuffer.toString();

  // Custom exit condition handling
  if (exitCondition != null) {
    try {
      final customCondition = RegExp(exitCondition);
      if (customCondition.hasMatch(stdoutData) ||
          customCondition.hasMatch(stderrData)) {
        print("āŒ Custom exit condition matched: \"$exitCondition\"");
        return ProcessResult(
            process.pid, 1, stdoutData, 'Custom exit condition matched');
      }
    } catch (e) {
      // If the regex is invalid, fall back to simple string matching
      if (stdoutData.contains(exitCondition) ||
          stderrData.contains(exitCondition)) {
        print("āŒ Custom exit condition matched: \"$exitCondition\"");
        return ProcessResult(
            process.pid, 1, stdoutData, 'Custom exit condition matched');
      }
    }
  }

  return ProcessResult(process.pid, exitCode, stdoutData, stderrData);
}