platform/incubation/test_process
2024-12-30 21:25:45 -07:00
..
example incubate: process package 69 pass 2 fail 2024-12-30 21:08:54 -07:00
lib incubate: process package all pass 2024-12-30 21:25:45 -07:00
test incubate: process package all pass 2024-12-30 21:25:45 -07:00
pubspec.lock incubate: adding test process package 10 pass 1 fail 2024-12-30 20:39:19 -07:00
pubspec.yaml incubate: adding test process package 10 pass 1 fail 2024-12-30 20:39:19 -07:00
README.md incubate: process package all pass 2024-12-30 21:25:45 -07:00

Dart Process Handler

A Laravel-inspired process handling library for Dart that provides an elegant and powerful API for executing shell commands and managing processes.

Features

  • Fluent API for process configuration
  • Synchronous and asynchronous execution
  • Process timeouts and idle timeouts
  • Working directory and environment variables
  • Input/output handling and streaming
  • Shell command support with pipes and redirects
  • TTY mode support
  • Comprehensive error handling
  • Real-time output callbacks
  • Process status tracking and management

Installation

Add this to your package's pubspec.yaml file:

dependencies:
  test_process: ^1.0.0

Basic Usage

Simple Command Execution

import 'package:test_process/test_process.dart';

void main() async {
  final factory = Factory();
  
  // Basic command execution
  final result = await factory.command(['echo', 'Hello World']).run();
  print('Output: ${result.output()}');  // Output: Hello World
  print('Success: ${result.successful()}');  // Success: true
  
  // Using string command (executed through shell)
  final result2 = await factory.command('echo "Hello World"').run();
  print('Output: ${result2.output()}');  // Output: Hello World
}

Working Directory

void main() async {
  final factory = Factory();
  
  // Execute command in specific directory
  final result = await factory
      .command(['ls', '-l'])
      .withWorkingDirectory('/tmp')
      .run();
      
  print('Files in /tmp:');
  print(result.output());
}

Environment Variables

void main() async {
  final factory = Factory();
  
  final result = await factory
      .command(['printenv', 'MY_VAR'])
      .withEnvironment({'MY_VAR': 'Hello from env!'})
      .run();
      
  print('Environment Value: ${result.output()}');
}

Process Timeouts

void main() async {
  final factory = Factory();
  
  try {
    // Process timeout
    await factory
        .command(['sleep', '10'])
        .withTimeout(5)  // 5 second timeout
        .run();
  } on ProcessTimedOutException catch (e) {
    print('Process timed out: ${e.message}');
  }
  
  try {
    // Idle timeout (no output for specified duration)
    await factory
        .command(['tail', '-f', '/dev/null'])
        .withIdleTimeout(5)  // 5 second idle timeout
        .run();
  } on ProcessTimedOutException catch (e) {
    print('Process idle timeout: ${e.message}');
  }
}

Standard Input

void main() async {
  final factory = Factory();
  
  // String input
  final result1 = await factory
      .command(['cat'])
      .withInput('Hello from stdin!')
      .run();
  print('Input Echo: ${result1.output()}');
  
  // Byte input
  final result2 = await factory
      .command(['cat'])
      .withInput([72, 101, 108, 108, 111])  // "Hello" in bytes
      .run();
  print('Byte Input Echo: ${result2.output()}');
}

Error Handling

void main() async {
  final factory = Factory();
  
  try {
    await factory.command(['ls', 'nonexistent-file']).run();
  } on ProcessFailedException catch (e) {
    print('Command failed:');
    print('  Exit code: ${e.result.exitCode}');
    print('  Error output: ${e.result.errorOutput()}');
  }
}

Shell Commands with Pipes

void main() async {
  final factory = Factory();
  
  // Using pipes in shell command
  final result = await factory
      .command('echo "line1\nline2\nline3" | grep "line2"')
      .run();
  print('Grep Result: ${result.output()}');
  
  // Multiple commands
  final result2 = await factory
      .command('cd /tmp && ls -l | grep "log"')
      .run();
  print('Log files: ${result2.output()}');
}

Asynchronous Execution with Output Callback

void main() async {
  final factory = Factory();
  
  // Start process asynchronously
  final process = await factory
      .command(['sh', '-c', 'for i in 1 2 3; do echo $i; sleep 1; done'])
      .start((output) {
        print('Realtime Output: $output');
      });
      
  // Wait for completion
  final result = await process.wait();
  print('Final Exit Code: ${result.exitCode}');
}

Process Management

void main() async {
  final factory = Factory();
  
  // Start long-running process
  final process = await factory
      .command(['sleep', '10'])
      .start();
      
  print('Process started with PID: ${process.pid}');
  print('Is running: ${process.running()}');
  
  // Kill the process
  final killed = process.kill();  // Sends SIGTERM
  print('Kill signal sent: $killed');
  
  // Or with specific signal
  process.kill(ProcessSignal.sigint);  // Sends SIGINT
  
  final result = await process.wait();
  print('Process completed with exit code: ${result.exitCode}');
}

Output Control

void main() async {
  final factory = Factory();
  
  // Disable output
  final result = await factory
      .command(['echo', 'test'])
      .withoutOutput()
      .run();
  print('Output length: ${result.output().length}');  // Output length: 0
}

TTY Mode

void main() async {
  final factory = Factory();
  
  // Enable TTY mode for commands that require it
  final result = await factory
      .command(['ls', '--color=auto'])
      .withTty()
      .run();
  print('Color Output: ${result.output()}');
}

Advanced Usage

Custom Process Configuration

void main() async {
  final factory = Factory();
  
  final result = await factory
      .command(['my-script'])
      .withWorkingDirectory('/path/to/scripts')
      .withEnvironment({
        'NODE_ENV': 'production',
        'DEBUG': 'true'
      })
      .withTimeout(30)
      .withIdleTimeout(5)
      .withTty()
      .run();
      
  if (result.successful()) {
    print('Script completed successfully');
    print(result.output());
  }
}

Process Pool Management

void main() async {
  final factory = Factory();
  final processes = <InvokedProcess>[];
  
  // Start multiple processes
  for (var i = 0; i < 3; i++) {
    final process = await factory
        .command(['worker.sh', i.toString()])
        .start();
    processes.add(process);
  }
  
  // Wait for all processes to complete
  for (var process in processes) {
    final result = await process.wait();
    print('Worker completed with exit code: ${result.exitCode}');
  }
}

Error Output Handling

void main() async {
  final factory = Factory();
  
  try {
    final result = await factory
        .command(['some-command'])
        .run();
        
    print('Standard output:');
    print(result.output());
    
    print('Error output:');
    print(result.errorOutput());
    
  } on ProcessFailedException catch (e) {
    print('Command failed with exit code: ${e.result.exitCode}');
    print('Error details:');
    print(e.result.errorOutput());
  }
}

Infinite Process Execution

void main() async {
  final factory = Factory();
  
  // Disable timeout for long-running processes
  final process = await factory
      .command(['tail', '-f', 'logfile.log'])
      .forever()  // Disables timeout
      .start((output) {
        print('New log entry: $output');
      });
      
  // Process will run until explicitly killed
  await Future.delayed(Duration(minutes: 1));
  process.kill();
}

Error Handling

The library provides several exception types for different error scenarios:

ProcessFailedException

Thrown when a process exits with a non-zero exit code:

try {
  await factory.command(['nonexistent-command']).run();
} on ProcessFailedException catch (e) {
  print('Command failed:');
  print('Exit code: ${e.result.exitCode}');
  print('Error output: ${e.result.errorOutput()}');
  print('Standard output: ${e.result.output()}');
}

ProcessTimedOutException

Thrown when a process exceeds its timeout or idle timeout:

try {
  await factory
      .command(['sleep', '10'])
      .withTimeout(5)
      .run();
} on ProcessTimedOutException catch (e) {
  print('Process timed out:');
  print('Message: ${e.message}');
  if (e.result != null) {
    print('Partial output: ${e.result?.output()}');
  }
}

Best Practices

  1. Always handle process failures:
try {
  await factory.command(['risky-command']).run();
} on ProcessFailedException catch (e) {
  // Handle failure
} on ProcessTimedOutException catch (e) {
  // Handle timeout
}
  1. Set appropriate timeouts:
factory
    .command(['long-running-task'])
    .withTimeout(300)        // Overall timeout
    .withIdleTimeout(60)     // Idle timeout
    .run();
  1. Use output callbacks for long-running processes:
await factory
    .command(['lengthy-task'])
    .start((output) {
      // Process output in real-time
      print('Progress: $output');
    });
  1. Clean up resources:
final process = await factory.command(['server']).start();
try {
  // Do work
} finally {
  process.kill();  // Ensure process is terminated
}
  1. Use shell mode appropriately:
// For simple commands, use array form:
factory.command(['echo', 'hello']);

// For shell features (pipes, redirects), use string form:
factory.command('echo hello | grep "o"');

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

License

This project is licensed under the MIT License - see the LICENSE file for details.