executeCommand static method
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);
}