Compare commits
11 commits
fc3131c0f9
...
52c5fe86fc
Author | SHA1 | Date | |
---|---|---|---|
![]() |
52c5fe86fc | ||
![]() |
942209900a | ||
![]() |
7e6ccf47bd | ||
![]() |
331443c512 | ||
![]() |
fc028630b9 | ||
![]() |
4dbcb45836 | ||
![]() |
bf7e607b4d | ||
![]() |
23782ce8e5 | ||
![]() |
e2be3ebd54 | ||
![]() |
4b4a321d99 | ||
![]() |
83813a4274 |
29 changed files with 2046 additions and 107 deletions
29
packages/pipeline/example/async_pipeline.dart
Normal file
29
packages/pipeline/example/async_pipeline.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
import 'package:platform_pipeline/pipeline.dart';
|
||||
import 'package:platform_container/container.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
|
||||
class AsyncGreetingPipe {
|
||||
dynamic handle(dynamic input, Function next) async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
return next('Hello, $input');
|
||||
}
|
||||
}
|
||||
|
||||
class AsyncExclamationPipe {
|
||||
dynamic handle(dynamic input, Function next) async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
return next('$input!');
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
var container = Container(MirrorsReflector());
|
||||
|
||||
var pipeline = Pipeline(container);
|
||||
var result = await pipeline
|
||||
.send('World')
|
||||
.through([AsyncGreetingPipe(), AsyncExclamationPipe()]).then(
|
||||
(result) => result.toString().toUpperCase());
|
||||
|
||||
print(result); // Should output: "HELLO, WORLD!" (after 2 seconds)
|
||||
}
|
24
packages/pipeline/example/async_simple.dart
Normal file
24
packages/pipeline/example/async_simple.dart
Normal file
|
@ -0,0 +1,24 @@
|
|||
import 'package:platform_pipeline/pipeline.dart';
|
||||
import 'package:platform_container/container.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
|
||||
class AsyncTransformPipe {
|
||||
dynamic handle(dynamic value, Function next) async {
|
||||
// Simulate async operation
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
var upperValue = (value as String).toUpperCase();
|
||||
return next(upperValue);
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
var container = Container(MirrorsReflector());
|
||||
|
||||
print('Starting pipeline...');
|
||||
|
||||
var result = await Pipeline(container)
|
||||
.send('hello')
|
||||
.through([AsyncTransformPipe()]).then((value) => value as String);
|
||||
|
||||
print(result); // Should output HELLO after 1 second
|
||||
}
|
23
packages/pipeline/example/error_handling.dart
Normal file
23
packages/pipeline/example/error_handling.dart
Normal file
|
@ -0,0 +1,23 @@
|
|||
import 'package:platform_pipeline/pipeline.dart';
|
||||
import 'package:platform_container/container.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
|
||||
class ErrorPipe {
|
||||
dynamic handle(dynamic input, Function next) {
|
||||
throw Exception('Simulated error in pipeline');
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
var container = Container(MirrorsReflector());
|
||||
var pipeline = Pipeline(container);
|
||||
|
||||
try {
|
||||
var result = await pipeline.send('World').through([ErrorPipe()]).then(
|
||||
(result) => result.toString().toUpperCase());
|
||||
|
||||
print('This should not be printed');
|
||||
} catch (e) {
|
||||
print('Caught error: $e');
|
||||
}
|
||||
}
|
57
packages/pipeline/example/http_server.dart
Normal file
57
packages/pipeline/example/http_server.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
import 'package:platform_pipeline/pipeline.dart';
|
||||
import 'package:platform_container/container.dart';
|
||||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:platform_foundation/http.dart';
|
||||
|
||||
class GreetingPipe {
|
||||
dynamic handle(dynamic input, Function next) {
|
||||
return next('Hello, $input');
|
||||
}
|
||||
}
|
||||
|
||||
class ExclamationPipe {
|
||||
dynamic handle(dynamic input, Function next) {
|
||||
return next('$input!');
|
||||
}
|
||||
}
|
||||
|
||||
class UppercasePipe {
|
||||
dynamic handle(dynamic input, Function next) async {
|
||||
await Future.delayed(
|
||||
Duration(milliseconds: 500)); // Small delay to demonstrate async
|
||||
return next(input.toString().toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
// Create application with empty reflector
|
||||
var app = Application(reflector: EmptyReflector());
|
||||
|
||||
// Create HTTP server
|
||||
var http = PlatformHttp(app);
|
||||
|
||||
// Define routes
|
||||
app.get('/', (RequestContext req, ResponseContext res) {
|
||||
res.write('Try visiting /greet/world to see the pipeline in action');
|
||||
return false;
|
||||
});
|
||||
|
||||
app.get('/greet/:name', (RequestContext req, ResponseContext res) async {
|
||||
var name = req.params['name'] ?? 'guest';
|
||||
|
||||
var pipeline = Pipeline(app.container);
|
||||
var result = await pipeline.send(name).through([
|
||||
GreetingPipe(),
|
||||
ExclamationPipe(),
|
||||
UppercasePipe(),
|
||||
]).then((result) => result);
|
||||
|
||||
res.write(result);
|
||||
return false;
|
||||
});
|
||||
|
||||
// Start server
|
||||
await http.startServer('localhost', 3000);
|
||||
print('Server running at http://localhost:3000');
|
||||
print('Visit http://localhost:3000/greet/world to see pipeline in action');
|
||||
}
|
30
packages/pipeline/example/mixed_pipes.dart
Normal file
30
packages/pipeline/example/mixed_pipes.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
import 'package:platform_pipeline/pipeline.dart';
|
||||
import 'package:platform_container/container.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
|
||||
class GreetingPipe {
|
||||
dynamic handle(dynamic input, Function next) {
|
||||
return next('Hello, $input');
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
var container = Container(MirrorsReflector());
|
||||
var pipeline = Pipeline(container);
|
||||
|
||||
print('Starting mixed pipeline...');
|
||||
|
||||
var result = await pipeline.send('World').through([
|
||||
GreetingPipe(),
|
||||
// Closure-based pipe
|
||||
(dynamic input, Function next) => next('$input!'),
|
||||
// Async closure-based pipe
|
||||
(dynamic input, Function next) async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
return next(input.toString().toUpperCase());
|
||||
},
|
||||
]).then((result) => 'Final result: $result');
|
||||
|
||||
print(
|
||||
result); // Should output: "Final result: HELLO, WORLD!" (after 1 second)
|
||||
}
|
14
packages/pipeline/example/simple.dart
Normal file
14
packages/pipeline/example/simple.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'package:platform_pipeline/pipeline.dart';
|
||||
import 'package:platform_container/container.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
|
||||
void main() async {
|
||||
var container = Container(MirrorsReflector());
|
||||
|
||||
var result = await Pipeline(container).send('Hello').through([
|
||||
(value, next) => next('$value World'),
|
||||
(value, next) => next('$value!'),
|
||||
]).then((value) => value);
|
||||
|
||||
print(result); // Should output: Hello World!
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:platform_foundation/http.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
import 'package:platform_pipeline/pipeline.dart';
|
||||
|
||||
class AsyncGreetingPipe {
|
||||
Future<dynamic> handle(String input, Function next) async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
return next('Hello, $input');
|
||||
}
|
||||
}
|
||||
|
||||
class AsyncExclamationPipe {
|
||||
Future<dynamic> handle(String input, Function next) async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
return next('$input!');
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
var app = Application(reflector: MirrorsReflector());
|
||||
var http = PlatformHttp(app);
|
||||
|
||||
app.container.registerSingleton((c) => Pipeline(c));
|
||||
|
||||
app.get('/', (req, res) async {
|
||||
var pipeline = app.container.make<Pipeline>();
|
||||
var result = await pipeline
|
||||
.send('World')
|
||||
.through(['AsyncGreetingPipe', 'AsyncExclamationPipe']).then(
|
||||
(result) => result.toUpperCase());
|
||||
|
||||
res.write(result); // Outputs: "HELLO, WORLD!" (after 2 seconds)
|
||||
});
|
||||
|
||||
await http.startServer('localhost', 3000);
|
||||
print('Server started on http://localhost:3000');
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:platform_foundation/http.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
import 'package:platform_pipeline/pipeline.dart';
|
||||
|
||||
class ErrorPipe {
|
||||
dynamic handle(String input, Function next) {
|
||||
throw Exception('Simulated error');
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
var app = Application(reflector: MirrorsReflector());
|
||||
var http = PlatformHttp(app);
|
||||
|
||||
app.container.registerSingleton((c) => Pipeline(c));
|
||||
|
||||
app.get('/', (req, res) async {
|
||||
var pipeline = app.container.make<Pipeline>();
|
||||
try {
|
||||
await pipeline
|
||||
.send('World')
|
||||
.through(['ErrorPipe']).then((result) => result.toUpperCase());
|
||||
} catch (e) {
|
||||
res.write('Error occurred: ${e.toString()}');
|
||||
return;
|
||||
}
|
||||
|
||||
res.write('This should not be reached');
|
||||
});
|
||||
|
||||
await http.startServer('localhost', 3000);
|
||||
print('Server started on http://localhost:3000');
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:platform_foundation/http.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
import 'package:platform_pipeline/pipeline.dart';
|
||||
|
||||
class GreetingPipe {
|
||||
dynamic handle(String input, Function next) {
|
||||
return next('Hello, $input');
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
var app = Application(reflector: MirrorsReflector());
|
||||
var http = PlatformHttp(app);
|
||||
|
||||
app.container.registerSingleton((c) => Pipeline(c));
|
||||
|
||||
app.get('/', (req, res) async {
|
||||
var pipeline = app.container.make<Pipeline>();
|
||||
var result = await pipeline.send('World').through([
|
||||
'GreetingPipe',
|
||||
(String input, Function next) => next('$input!'),
|
||||
(String input, Function next) async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
return next(input.toUpperCase());
|
||||
},
|
||||
]).then((result) => 'Final result: $result');
|
||||
|
||||
res.write(
|
||||
result); // Outputs: "Final result: HELLO, WORLD!" (after 1 second)
|
||||
});
|
||||
|
||||
await http.startServer('localhost', 3000);
|
||||
print('Server started on http://localhost:3000');
|
||||
}
|
7
packages/process/.gitignore
vendored
Normal file
7
packages/process/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
# https://dart.dev/guides/libraries/private-files
|
||||
# Created by `dart pub`
|
||||
.dart_tool/
|
||||
|
||||
# Avoid committing pubspec.lock for library packages; see
|
||||
# https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
pubspec.lock
|
431
packages/process/README.md
Normal file
431
packages/process/README.md
Normal file
|
@ -0,0 +1,431 @@
|
|||
# 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:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
platform_process: ^1.0.0
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Simple Command Execution
|
||||
|
||||
```dart
|
||||
import 'package:platform_process/platform_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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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:
|
||||
|
||||
```dart
|
||||
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:
|
||||
|
||||
```dart
|
||||
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:**
|
||||
```dart
|
||||
try {
|
||||
await factory.command(['risky-command']).run();
|
||||
} on ProcessFailedException catch (e) {
|
||||
// Handle failure
|
||||
} on ProcessTimedOutException catch (e) {
|
||||
// Handle timeout
|
||||
}
|
||||
```
|
||||
|
||||
2. **Set appropriate timeouts:**
|
||||
```dart
|
||||
factory
|
||||
.command(['long-running-task'])
|
||||
.withTimeout(300) // Overall timeout
|
||||
.withIdleTimeout(60) // Idle timeout
|
||||
.run();
|
||||
```
|
||||
|
||||
3. **Use output callbacks for long-running processes:**
|
||||
```dart
|
||||
await factory
|
||||
.command(['lengthy-task'])
|
||||
.start((output) {
|
||||
// Process output in real-time
|
||||
print('Progress: $output');
|
||||
});
|
||||
```
|
||||
|
||||
4. **Clean up resources:**
|
||||
```dart
|
||||
final process = await factory.command(['server']).start();
|
||||
try {
|
||||
// Do work
|
||||
} finally {
|
||||
process.kill(); // Ensure process is terminated
|
||||
}
|
||||
```
|
||||
|
||||
5. **Use shell mode appropriately:**
|
||||
```dart
|
||||
// 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.
|
0
packages/process/doc/.gitkeep
Normal file
0
packages/process/doc/.gitkeep
Normal file
151
packages/process/example/example.dart
Normal file
151
packages/process/example/example.dart
Normal file
|
@ -0,0 +1,151 @@
|
|||
import 'dart:async';
|
||||
import 'package:platform_process/platform_process.dart';
|
||||
|
||||
Future<void> runExamples() async {
|
||||
// Create a process factory
|
||||
final factory = Factory();
|
||||
|
||||
// Basic command execution
|
||||
print('\n=== Basic Command Execution ===');
|
||||
try {
|
||||
final result = await factory.command(['echo', 'Hello, World!']).run();
|
||||
print('Output: ${result.output().trim()}');
|
||||
print('Exit Code: ${result.exitCode}');
|
||||
print('Success: ${result.successful()}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
// Working directory
|
||||
print('\n=== Working Directory ===');
|
||||
try {
|
||||
final result =
|
||||
await factory.command(['pwd']).withWorkingDirectory('/tmp').run();
|
||||
print('Current Directory: ${result.output().trim()}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
// Environment variables
|
||||
print('\n=== Environment Variables ===');
|
||||
try {
|
||||
final result = await factory
|
||||
.command(['sh', '-c', 'echo \$CUSTOM_VAR']).withEnvironment(
|
||||
{'CUSTOM_VAR': 'Hello from env!'}).run();
|
||||
print('Environment Value: ${result.output().trim()}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
// Process timeout
|
||||
print('\n=== Process Timeout ===');
|
||||
try {
|
||||
await factory.command(['sleep', '5']).withTimeout(1).run();
|
||||
print('Process completed (unexpected)');
|
||||
} catch (e) {
|
||||
// Let the zone handler catch this
|
||||
}
|
||||
|
||||
// Standard input
|
||||
print('\n=== Standard Input ===');
|
||||
try {
|
||||
final result =
|
||||
await factory.command(['cat']).withInput('Hello from stdin!').run();
|
||||
print('Input Echo: ${result.output()}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
// Error handling
|
||||
print('\n=== Error Handling ===');
|
||||
try {
|
||||
await factory.command(['ls', 'nonexistent-file']).run();
|
||||
print('Command succeeded (unexpected)');
|
||||
} on ProcessFailedException catch (e) {
|
||||
print('Expected error:');
|
||||
print(' Exit code: ${e.exitCode}');
|
||||
print(' Error output: ${e.errorOutput.trim()}');
|
||||
} catch (e) {
|
||||
print('Unexpected error: $e');
|
||||
}
|
||||
|
||||
// Shell commands with pipes
|
||||
print('\n=== Shell Commands with Pipes ===');
|
||||
try {
|
||||
final result = await factory.command(
|
||||
['sh', '-c', 'echo "line1\nline2\nline3" | grep "line2"']).run();
|
||||
print('Grep Result: ${result.output().trim()}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
// Async process with output callback
|
||||
print('\n=== Async Process with Output Callback ===');
|
||||
try {
|
||||
final process = await factory.command([
|
||||
'sh',
|
||||
'-c',
|
||||
'for n in 1 2 3; do echo \$n; sleep 1; done'
|
||||
]).start((output) {
|
||||
print('Realtime Output: ${output.trim()}');
|
||||
});
|
||||
|
||||
final result = await process.wait();
|
||||
print('Final Exit Code: ${result.exitCode}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
// Process killing
|
||||
print('\n=== Process Killing ===');
|
||||
try {
|
||||
final process = await factory.command(['sleep', '10']).start();
|
||||
|
||||
print('Process started with PID: ${process.pid}');
|
||||
print('Is running: ${process.running()}');
|
||||
|
||||
// Kill after 1 second
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
final killed = process.kill();
|
||||
print('Kill signal sent: $killed');
|
||||
|
||||
final result = await process.wait();
|
||||
print('Process completed with exit code: ${result.exitCode}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
// Quiet mode (no output)
|
||||
print('\n=== Quiet Mode ===');
|
||||
try {
|
||||
final result = await factory
|
||||
.command(['echo', 'This output is suppressed'])
|
||||
.withoutOutput()
|
||||
.run();
|
||||
print('Output length: ${result.output().length}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
// Color output (alternative to TTY mode)
|
||||
print('\n=== Color Output ===');
|
||||
try {
|
||||
final result = await factory.command(['ls', '--color=always']).run();
|
||||
print('Color Output: ${result.output().trim()}');
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
runZonedGuarded(() async {
|
||||
await runExamples();
|
||||
}, (error, stack) {
|
||||
if (error is ProcessTimedOutException) {
|
||||
print('Expected timeout error: ${error.message}');
|
||||
} else {
|
||||
print('Unexpected error: $error');
|
||||
print('Stack trace: $stack');
|
||||
}
|
||||
});
|
||||
}
|
9
packages/process/lib/platform_process.dart
Normal file
9
packages/process/lib/platform_process.dart
Normal file
|
@ -0,0 +1,9 @@
|
|||
/// A Laravel-compatible process management implementation in pure Dart.
|
||||
library platform_process;
|
||||
|
||||
export 'src/pending_process.dart';
|
||||
export 'src/process_result.dart';
|
||||
export 'src/invoked_process.dart';
|
||||
export 'src/factory.dart';
|
||||
export 'src/exceptions/process_failed_exception.dart';
|
||||
export 'src/exceptions/process_timed_out_exception.dart';
|
|
@ -0,0 +1,42 @@
|
|||
import '../process_result.dart';
|
||||
|
||||
/// Exception thrown when a process fails.
|
||||
class ProcessFailedException implements Exception {
|
||||
/// The process result.
|
||||
final ProcessResult _result;
|
||||
|
||||
/// Create a new process failed exception instance.
|
||||
ProcessFailedException(this._result);
|
||||
|
||||
/// Get the process result.
|
||||
ProcessResult get result => _result;
|
||||
|
||||
/// Get the process exit code.
|
||||
int get exitCode => _result.exitCode;
|
||||
|
||||
/// Get the process output.
|
||||
String get output => _result.output();
|
||||
|
||||
/// Get the process error output.
|
||||
String get errorOutput => _result.errorOutput();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer =
|
||||
StringBuffer('Process failed with exit code: ${_result.exitCode}');
|
||||
|
||||
if (_result.output().isNotEmpty) {
|
||||
buffer.writeln();
|
||||
buffer.writeln('Output:');
|
||||
buffer.writeln(_result.output().trim());
|
||||
}
|
||||
|
||||
if (_result.errorOutput().isNotEmpty) {
|
||||
buffer.writeln();
|
||||
buffer.writeln('Error Output:');
|
||||
buffer.writeln(_result.errorOutput().trim());
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import '../process_result.dart';
|
||||
|
||||
/// Exception thrown when a process times out.
|
||||
class ProcessTimedOutException implements Exception {
|
||||
/// The error message.
|
||||
final String message;
|
||||
|
||||
/// The process result, if available.
|
||||
final ProcessResult? result;
|
||||
|
||||
/// Create a new process timed out exception instance.
|
||||
ProcessTimedOutException(this.message, [this.result]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer(message);
|
||||
|
||||
if (result != null) {
|
||||
if (result!.output().isNotEmpty) {
|
||||
buffer.writeln();
|
||||
buffer.writeln();
|
||||
buffer.writeln('Output:');
|
||||
buffer.writeln('================');
|
||||
buffer.writeln(result!.output());
|
||||
}
|
||||
|
||||
if (result!.errorOutput().isNotEmpty) {
|
||||
buffer.writeln();
|
||||
buffer.writeln('Error Output:');
|
||||
buffer.writeln('================');
|
||||
buffer.writeln(result!.errorOutput());
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
34
packages/process/lib/src/factory.dart
Normal file
34
packages/process/lib/src/factory.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
import 'pending_process.dart';
|
||||
|
||||
/// A factory for creating process instances.
|
||||
class Factory {
|
||||
/// Create a new factory instance.
|
||||
Factory();
|
||||
|
||||
/// Create a new pending process instance with the given command.
|
||||
PendingProcess command(dynamic command) {
|
||||
if (command == null) {
|
||||
throw ArgumentError('Command cannot be null');
|
||||
}
|
||||
|
||||
if (command is String && command.trim().isEmpty) {
|
||||
throw ArgumentError('Command string cannot be empty');
|
||||
}
|
||||
|
||||
if (command is List) {
|
||||
if (command.isEmpty) {
|
||||
throw ArgumentError('Command list cannot be empty');
|
||||
}
|
||||
|
||||
if (command.any((element) => element is! String)) {
|
||||
throw ArgumentError('Command list must contain only strings');
|
||||
}
|
||||
}
|
||||
|
||||
if (command is! String && command is! List) {
|
||||
throw ArgumentError('Command must be a string or list of strings');
|
||||
}
|
||||
|
||||
return PendingProcess(this)..withCommand(command);
|
||||
}
|
||||
}
|
128
packages/process/lib/src/invoked_process.dart
Normal file
128
packages/process/lib/src/invoked_process.dart
Normal file
|
@ -0,0 +1,128 @@
|
|||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'process_result.dart';
|
||||
import 'exceptions/process_failed_exception.dart';
|
||||
|
||||
/// Represents a process that has been started.
|
||||
class InvokedProcess {
|
||||
/// The underlying process instance.
|
||||
final Process _process;
|
||||
|
||||
/// The output handler callback.
|
||||
final void Function(String)? _onOutput;
|
||||
|
||||
/// The collected stdout data.
|
||||
final List<int> _stdout = [];
|
||||
|
||||
/// The collected stderr data.
|
||||
final List<int> _stderr = [];
|
||||
|
||||
/// Whether the process has completed
|
||||
bool _completed = false;
|
||||
|
||||
/// Whether the process was killed
|
||||
bool _killed = false;
|
||||
|
||||
/// Completer for stdout stream
|
||||
final _stdoutCompleter = Completer<void>();
|
||||
|
||||
/// Completer for stderr stream
|
||||
final _stderrCompleter = Completer<void>();
|
||||
|
||||
/// Create a new invoked process instance.
|
||||
InvokedProcess(this._process, this._onOutput) {
|
||||
_process.stdout.listen(
|
||||
(data) {
|
||||
_stdout.addAll(data);
|
||||
if (_onOutput != null) {
|
||||
_onOutput!(String.fromCharCodes(data));
|
||||
}
|
||||
},
|
||||
onDone: () => _stdoutCompleter.complete(),
|
||||
);
|
||||
|
||||
_process.stderr.listen(
|
||||
(data) {
|
||||
_stderr.addAll(data);
|
||||
if (_onOutput != null) {
|
||||
_onOutput!(String.fromCharCodes(data));
|
||||
}
|
||||
},
|
||||
onDone: () => _stderrCompleter.complete(),
|
||||
);
|
||||
|
||||
// Track when the process completes
|
||||
_process.exitCode.then((_) => _completed = true);
|
||||
}
|
||||
|
||||
/// Get the process ID.
|
||||
int get pid => _process.pid;
|
||||
|
||||
/// Write data to the process stdin.
|
||||
void write(dynamic input) {
|
||||
if (input is String) {
|
||||
_process.stdin.write(input);
|
||||
} else if (input is List<int>) {
|
||||
_process.stdin.add(input);
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the process stdin.
|
||||
Future<void> closeStdin() async {
|
||||
await _process.stdin.close();
|
||||
}
|
||||
|
||||
/// Signal the process.
|
||||
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
|
||||
_killed = true;
|
||||
return _process.kill(signal);
|
||||
}
|
||||
|
||||
/// Check if the process is still running.
|
||||
bool running() {
|
||||
return !_completed;
|
||||
}
|
||||
|
||||
/// Wait for the process to complete and get its result.
|
||||
Future<ProcessResult> wait() async {
|
||||
// Wait for all streams to complete
|
||||
await Future.wait([
|
||||
_stdoutCompleter.future,
|
||||
_stderrCompleter.future,
|
||||
]);
|
||||
|
||||
final exitCode = await _process.exitCode;
|
||||
final result = ProcessResult(
|
||||
exitCode,
|
||||
String.fromCharCodes(_stdout),
|
||||
String.fromCharCodes(_stderr),
|
||||
);
|
||||
|
||||
// Don't throw if the process was killed
|
||||
if (!_killed && exitCode != 0) {
|
||||
throw ProcessFailedException(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Get the latest output from the process.
|
||||
String latestOutput() {
|
||||
return String.fromCharCodes(_stdout);
|
||||
}
|
||||
|
||||
/// Get the latest error output from the process.
|
||||
String latestErrorOutput() {
|
||||
return String.fromCharCodes(_stderr);
|
||||
}
|
||||
|
||||
/// Get all output from the process.
|
||||
String output() {
|
||||
return String.fromCharCodes(_stdout);
|
||||
}
|
||||
|
||||
/// Get all error output from the process.
|
||||
String errorOutput() {
|
||||
return String.fromCharCodes(_stderr);
|
||||
}
|
||||
}
|
285
packages/process/lib/src/pending_process.dart
Normal file
285
packages/process/lib/src/pending_process.dart
Normal file
|
@ -0,0 +1,285 @@
|
|||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
import 'factory.dart';
|
||||
import 'process_result.dart';
|
||||
import 'invoked_process.dart';
|
||||
import 'exceptions/process_timed_out_exception.dart';
|
||||
import 'exceptions/process_failed_exception.dart';
|
||||
|
||||
/// A class that represents a process that is ready to be started.
|
||||
class PendingProcess {
|
||||
/// The process factory instance.
|
||||
final Factory _factory;
|
||||
|
||||
/// The command to invoke the process.
|
||||
dynamic command;
|
||||
|
||||
/// The working directory of the process.
|
||||
String? workingDirectory;
|
||||
|
||||
/// The maximum number of seconds the process may run.
|
||||
int? timeout = 60;
|
||||
|
||||
/// The maximum number of seconds the process may go without returning output.
|
||||
int? idleTimeout;
|
||||
|
||||
/// The additional environment variables for the process.
|
||||
Map<String, String> environment = {};
|
||||
|
||||
/// The standard input data that should be piped into the command.
|
||||
dynamic input;
|
||||
|
||||
/// Indicates whether output should be disabled for the process.
|
||||
bool quietly = false;
|
||||
|
||||
/// Indicates if TTY mode should be enabled.
|
||||
bool tty = false;
|
||||
|
||||
/// Create a new pending process instance.
|
||||
PendingProcess(this._factory);
|
||||
|
||||
/// Format the command for display.
|
||||
String _formatCommand() {
|
||||
if (command is List) {
|
||||
return (command as List).join(' ');
|
||||
}
|
||||
return command.toString();
|
||||
}
|
||||
|
||||
/// Specify the command that will invoke the process.
|
||||
PendingProcess withCommand(dynamic command) {
|
||||
this.command = command;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Specify the working directory of the process.
|
||||
PendingProcess withWorkingDirectory(String directory) {
|
||||
workingDirectory = directory;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Specify the maximum number of seconds the process may run.
|
||||
PendingProcess withTimeout(int seconds) {
|
||||
timeout = seconds;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Specify the maximum number of seconds a process may go without returning output.
|
||||
PendingProcess withIdleTimeout(int seconds) {
|
||||
idleTimeout = seconds;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Indicate that the process may run forever without timing out.
|
||||
PendingProcess forever() {
|
||||
timeout = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Set the additional environment variables for the process.
|
||||
PendingProcess withEnvironment(Map<String, String> env) {
|
||||
environment = env;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Set the standard input that should be provided when invoking the process.
|
||||
PendingProcess withInput(dynamic input) {
|
||||
this.input = input;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Disable output for the process.
|
||||
PendingProcess withoutOutput() {
|
||||
quietly = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Enable TTY mode for the process.
|
||||
PendingProcess withTty([bool enabled = true]) {
|
||||
tty = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Run the process synchronously.
|
||||
Future<ProcessResult> run(
|
||||
[dynamic command, void Function(String)? onOutput]) async {
|
||||
this.command = command ?? this.command;
|
||||
|
||||
if (this.command == null) {
|
||||
throw ArgumentError('No command specified');
|
||||
}
|
||||
|
||||
// Handle immediate timeout
|
||||
if (timeout == 0) {
|
||||
throw ProcessTimedOutException(
|
||||
'The process "${_formatCommand()}" exceeded the timeout of $timeout seconds.',
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final process = await _createProcess();
|
||||
Timer? timeoutTimer;
|
||||
Timer? idleTimer;
|
||||
DateTime lastOutputTime = DateTime.now();
|
||||
bool timedOut = false;
|
||||
String? timeoutMessage;
|
||||
|
||||
if (timeout != null) {
|
||||
timeoutTimer = Timer(Duration(seconds: timeout!), () {
|
||||
timedOut = true;
|
||||
timeoutMessage =
|
||||
'The process "${_formatCommand()}" exceeded the timeout of $timeout seconds.';
|
||||
process.kill();
|
||||
});
|
||||
}
|
||||
|
||||
if (idleTimeout != null) {
|
||||
idleTimer = Timer.periodic(Duration(seconds: 1), (_) {
|
||||
final idleSeconds =
|
||||
DateTime.now().difference(lastOutputTime).inSeconds;
|
||||
if (idleSeconds >= idleTimeout!) {
|
||||
timedOut = true;
|
||||
timeoutMessage =
|
||||
'The process "${_formatCommand()}" exceeded the idle timeout of $idleTimeout seconds.';
|
||||
process.kill();
|
||||
idleTimer?.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await _runProcess(process, (output) {
|
||||
lastOutputTime = DateTime.now();
|
||||
onOutput?.call(output);
|
||||
});
|
||||
|
||||
if (timedOut) {
|
||||
throw ProcessTimedOutException(timeoutMessage!);
|
||||
}
|
||||
|
||||
if (result.exitCode != 0) {
|
||||
throw ProcessFailedException(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
timeoutTimer?.cancel();
|
||||
idleTimer?.cancel();
|
||||
}
|
||||
} on ProcessException catch (e) {
|
||||
final result = ProcessResult(1, '', e.message);
|
||||
throw ProcessFailedException(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the process asynchronously.
|
||||
Future<InvokedProcess> start([void Function(String)? onOutput]) async {
|
||||
if (command == null) {
|
||||
throw ArgumentError('No command specified');
|
||||
}
|
||||
|
||||
try {
|
||||
final process = await _createProcess();
|
||||
|
||||
if (input != null) {
|
||||
if (input is String) {
|
||||
process.stdin.write(input);
|
||||
} else if (input is List<int>) {
|
||||
process.stdin.add(input);
|
||||
}
|
||||
await process.stdin.close();
|
||||
}
|
||||
|
||||
return InvokedProcess(process, onOutput);
|
||||
} on ProcessException catch (e) {
|
||||
final result = ProcessResult(1, '', e.message);
|
||||
throw ProcessFailedException(result);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Process> _createProcess() async {
|
||||
if (command is List) {
|
||||
final List<String> args =
|
||||
(command as List).map((e) => e.toString()).toList();
|
||||
return Process.start(
|
||||
args[0],
|
||||
args.skip(1).toList(),
|
||||
workingDirectory: workingDirectory ?? Directory.current.path,
|
||||
environment: environment,
|
||||
includeParentEnvironment: true,
|
||||
runInShell: false,
|
||||
mode: tty ? ProcessStartMode.inheritStdio : ProcessStartMode.normal,
|
||||
);
|
||||
} else {
|
||||
// For string commands, use shell to handle pipes, redirects, etc.
|
||||
final shell = Platform.isWindows ? 'cmd' : '/bin/sh';
|
||||
final shellArg = Platform.isWindows ? '/c' : '-c';
|
||||
return Process.start(
|
||||
shell,
|
||||
[shellArg, command.toString()],
|
||||
workingDirectory: workingDirectory ?? Directory.current.path,
|
||||
environment: environment,
|
||||
includeParentEnvironment: true,
|
||||
runInShell: true,
|
||||
mode: tty ? ProcessStartMode.inheritStdio : ProcessStartMode.normal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ProcessResult> _runProcess(
|
||||
Process process, void Function(String)? onOutput) async {
|
||||
final stdout = <int>[];
|
||||
final stderr = <int>[];
|
||||
final stdoutCompleter = Completer<void>();
|
||||
final stderrCompleter = Completer<void>();
|
||||
|
||||
if (!quietly) {
|
||||
process.stdout.listen(
|
||||
(data) {
|
||||
stdout.addAll(data);
|
||||
if (onOutput != null) {
|
||||
onOutput(String.fromCharCodes(data));
|
||||
}
|
||||
},
|
||||
onDone: () => stdoutCompleter.complete(),
|
||||
);
|
||||
|
||||
process.stderr.listen(
|
||||
(data) {
|
||||
stderr.addAll(data);
|
||||
if (onOutput != null) {
|
||||
onOutput(String.fromCharCodes(data));
|
||||
}
|
||||
},
|
||||
onDone: () => stderrCompleter.complete(),
|
||||
);
|
||||
} else {
|
||||
stdoutCompleter.complete();
|
||||
stderrCompleter.complete();
|
||||
}
|
||||
|
||||
if (input != null) {
|
||||
if (input is String) {
|
||||
process.stdin.write(input);
|
||||
} else if (input is List<int>) {
|
||||
process.stdin.add(input);
|
||||
}
|
||||
await process.stdin.close();
|
||||
}
|
||||
|
||||
// Wait for all streams to complete
|
||||
await Future.wait([
|
||||
stdoutCompleter.future,
|
||||
stderrCompleter.future,
|
||||
]);
|
||||
|
||||
final exitCode = await process.exitCode;
|
||||
|
||||
return ProcessResult(
|
||||
exitCode,
|
||||
String.fromCharCodes(stdout),
|
||||
String.fromCharCodes(stderr),
|
||||
);
|
||||
}
|
||||
}
|
32
packages/process/lib/src/process_result.dart
Normal file
32
packages/process/lib/src/process_result.dart
Normal file
|
@ -0,0 +1,32 @@
|
|||
/// Represents the result of a process execution.
|
||||
class ProcessResult {
|
||||
/// The process exit code.
|
||||
final int _exitCode;
|
||||
|
||||
/// The process standard output.
|
||||
final String _output;
|
||||
|
||||
/// The process error output.
|
||||
final String _errorOutput;
|
||||
|
||||
/// Create a new process result instance.
|
||||
ProcessResult(this._exitCode, this._output, this._errorOutput);
|
||||
|
||||
/// Get the process exit code.
|
||||
int get exitCode => _exitCode;
|
||||
|
||||
/// Get the process output.
|
||||
String output() => _output;
|
||||
|
||||
/// Get the process error output.
|
||||
String errorOutput() => _errorOutput;
|
||||
|
||||
/// Check if the process was successful.
|
||||
bool successful() => _exitCode == 0;
|
||||
|
||||
/// Check if the process failed.
|
||||
bool failed() => !successful();
|
||||
|
||||
@override
|
||||
String toString() => _output;
|
||||
}
|
17
packages/process/pubspec.yaml
Normal file
17
packages/process/pubspec.yaml
Normal file
|
@ -0,0 +1,17 @@
|
|||
name: platform_process
|
||||
description: A Laravel-compatible process management implementation in pure Dart
|
||||
version: 1.0.0
|
||||
homepage: https://github.com/platform/platform_process
|
||||
|
||||
environment:
|
||||
sdk: '>=2.17.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
meta: ^1.9.1
|
||||
path: ^1.8.0
|
||||
async: ^2.11.0
|
||||
collection: ^1.17.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.0.0
|
||||
test: ^1.24.0
|
72
packages/process/test/exceptions_test.dart
Normal file
72
packages/process/test/exceptions_test.dart
Normal file
|
@ -0,0 +1,72 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:platform_process/platform_process.dart';
|
||||
|
||||
void main() {
|
||||
group('ProcessFailedException', () {
|
||||
test('contains process result', () {
|
||||
final result = ProcessResult(1, 'output', 'error');
|
||||
final exception = ProcessFailedException(result);
|
||||
expect(exception.result, equals(result));
|
||||
});
|
||||
|
||||
test('provides access to error details', () {
|
||||
final result = ProcessResult(2, 'output', 'error message');
|
||||
final exception = ProcessFailedException(result);
|
||||
expect(exception.exitCode, equals(2));
|
||||
expect(exception.errorOutput, equals('error message'));
|
||||
expect(exception.output, equals('output'));
|
||||
});
|
||||
|
||||
test('toString includes error details', () {
|
||||
final result = ProcessResult(1, 'output', 'error message');
|
||||
final exception = ProcessFailedException(result);
|
||||
expect(exception.toString(), contains('error message'));
|
||||
expect(exception.toString(), contains('1'));
|
||||
});
|
||||
|
||||
test('handles empty error output', () {
|
||||
final result = ProcessResult(1, 'output', '');
|
||||
final exception = ProcessFailedException(result);
|
||||
expect(
|
||||
exception.toString(), contains('Process failed with exit code: 1'));
|
||||
});
|
||||
|
||||
test('handles empty output', () {
|
||||
final result = ProcessResult(1, '', 'error');
|
||||
final exception = ProcessFailedException(result);
|
||||
expect(exception.output, isEmpty);
|
||||
expect(exception.errorOutput, equals('error'));
|
||||
});
|
||||
});
|
||||
|
||||
group('ProcessTimedOutException', () {
|
||||
test('contains timeout message', () {
|
||||
final exception = ProcessTimedOutException('Process timed out after 60s');
|
||||
expect(exception.message, equals('Process timed out after 60s'));
|
||||
});
|
||||
|
||||
test('optionally includes process result', () {
|
||||
final result = ProcessResult(143, 'partial output', '');
|
||||
final exception = ProcessTimedOutException('Timed out', result);
|
||||
expect(exception.result, equals(result));
|
||||
});
|
||||
|
||||
test('toString includes message', () {
|
||||
final exception = ProcessTimedOutException('Custom timeout message');
|
||||
expect(exception.toString(), contains('Custom timeout message'));
|
||||
});
|
||||
|
||||
test('toString includes result details when available', () {
|
||||
final result = ProcessResult(143, 'output', 'error');
|
||||
final exception = ProcessTimedOutException('Timed out', result);
|
||||
expect(exception.toString(), contains('Timed out'));
|
||||
expect(exception.result, equals(result));
|
||||
});
|
||||
|
||||
test('handles null result', () {
|
||||
final exception = ProcessTimedOutException('Timed out');
|
||||
expect(exception.result, isNull);
|
||||
expect(exception.toString(), contains('Timed out'));
|
||||
});
|
||||
});
|
||||
}
|
42
packages/process/test/factory_test.dart
Normal file
42
packages/process/test/factory_test.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:platform_process/platform_process.dart';
|
||||
|
||||
void main() {
|
||||
group('Factory Tests', () {
|
||||
late Factory factory;
|
||||
|
||||
setUp(() {
|
||||
factory = Factory();
|
||||
});
|
||||
|
||||
test('command() creates PendingProcess with string command', () {
|
||||
final process = factory.command('echo Hello');
|
||||
expect(process, isA<PendingProcess>());
|
||||
});
|
||||
|
||||
test('command() creates PendingProcess with list command', () {
|
||||
final process = factory.command(['echo', 'Hello']);
|
||||
expect(process, isA<PendingProcess>());
|
||||
});
|
||||
|
||||
test('command() with null throws ArgumentError', () {
|
||||
expect(() => factory.command(null), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('command() with empty string throws ArgumentError', () {
|
||||
expect(() => factory.command(''), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('command() with empty list throws ArgumentError', () {
|
||||
expect(() => factory.command([]), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('command() with invalid type throws ArgumentError', () {
|
||||
expect(() => factory.command(123), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('command() with list containing non-string throws ArgumentError', () {
|
||||
expect(() => factory.command(['echo', 123]), throwsArgumentError);
|
||||
});
|
||||
});
|
||||
}
|
104
packages/process/test/invoked_process_test.dart
Normal file
104
packages/process/test/invoked_process_test.dart
Normal file
|
@ -0,0 +1,104 @@
|
|||
import 'dart:io';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:platform_process/platform_process.dart';
|
||||
|
||||
void main() {
|
||||
group('InvokedProcess Tests', () {
|
||||
test('latestOutput() returns latest stdout', () async {
|
||||
final factory = Factory();
|
||||
final process = await factory.command(
|
||||
['sh', '-c', 'echo "line1"; sleep 0.1; echo "line2"']).start();
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(process.latestOutput().trim(), equals('line1'));
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
expect(process.latestOutput().trim(), contains('line2'));
|
||||
|
||||
await process.wait();
|
||||
}, timeout: Timeout(Duration(seconds: 5)));
|
||||
|
||||
test('latestErrorOutput() returns latest stderr', () async {
|
||||
final factory = Factory();
|
||||
final process = await factory.command([
|
||||
'sh',
|
||||
'-c',
|
||||
'echo "error1" >&2; sleep 0.1; echo "error2" >&2'
|
||||
]).start();
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(process.latestErrorOutput().trim(), equals('error1'));
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
expect(process.latestErrorOutput().trim(), contains('error2'));
|
||||
|
||||
await process.wait();
|
||||
}, timeout: Timeout(Duration(seconds: 5)));
|
||||
|
||||
test('running() returns correct state', () async {
|
||||
final factory = Factory();
|
||||
final process = await factory.command(['sleep', '0.5']).start();
|
||||
|
||||
expect(process.running(), isTrue);
|
||||
await Future.delayed(Duration(milliseconds: 600));
|
||||
expect(process.running(), isFalse);
|
||||
}, timeout: Timeout(Duration(seconds: 5)));
|
||||
|
||||
test('write() sends input to process', () async {
|
||||
final factory = Factory();
|
||||
final process = await factory.command(['cat']).start();
|
||||
|
||||
process.write('Hello');
|
||||
process.write(' World');
|
||||
await process.closeStdin();
|
||||
final result = await process.wait();
|
||||
expect(result.output().trim(), equals('Hello World'));
|
||||
}, timeout: Timeout(Duration(seconds: 5)));
|
||||
|
||||
test('write() handles byte input', () async {
|
||||
final factory = Factory();
|
||||
final process = await factory.command(['cat']).start();
|
||||
|
||||
process.write([72, 101, 108, 108, 111]); // "Hello" in bytes
|
||||
await process.closeStdin();
|
||||
final result = await process.wait();
|
||||
expect(result.output().trim(), equals('Hello'));
|
||||
}, timeout: Timeout(Duration(seconds: 5)));
|
||||
|
||||
test('kill() terminates process', () async {
|
||||
final factory = Factory();
|
||||
final process = await factory.command(['sleep', '10']).start();
|
||||
|
||||
expect(process.running(), isTrue);
|
||||
final killed = process.kill();
|
||||
expect(killed, isTrue);
|
||||
|
||||
final result = await process.wait();
|
||||
expect(result.exitCode, equals(-15)); // SIGTERM
|
||||
expect(process.running(), isFalse);
|
||||
}, timeout: Timeout(Duration(seconds: 5)));
|
||||
|
||||
test('kill() with custom signal', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final factory = Factory();
|
||||
final process = await factory.command(['sleep', '10']).start();
|
||||
|
||||
expect(process.running(), isTrue);
|
||||
final killed = process.kill(ProcessSignal.sigint);
|
||||
expect(killed, isTrue);
|
||||
|
||||
final result = await process.wait();
|
||||
expect(result.exitCode, equals(-2)); // SIGINT
|
||||
expect(process.running(), isFalse);
|
||||
}
|
||||
}, timeout: Timeout(Duration(seconds: 5)));
|
||||
|
||||
test('pid returns process ID', () async {
|
||||
final factory = Factory();
|
||||
final process = await factory.command(['echo', 'test']).start();
|
||||
|
||||
expect(process.pid, isPositive);
|
||||
await process.wait();
|
||||
}, timeout: Timeout(Duration(seconds: 5)));
|
||||
});
|
||||
}
|
147
packages/process/test/laravel_process_test.dart
Normal file
147
packages/process/test/laravel_process_test.dart
Normal file
|
@ -0,0 +1,147 @@
|
|||
import 'dart:io';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:platform_process/platform_process.dart';
|
||||
|
||||
void main() {
|
||||
late Factory factory;
|
||||
|
||||
setUp(() {
|
||||
factory = Factory();
|
||||
});
|
||||
|
||||
group('Laravel Process Tests', () {
|
||||
test('successful process', () async {
|
||||
final result = await factory
|
||||
.command(['ls'])
|
||||
.withWorkingDirectory(Directory.current.path)
|
||||
.run();
|
||||
|
||||
expect(result.successful(), isTrue);
|
||||
expect(result.failed(), isFalse);
|
||||
expect(result.exitCode, equals(0));
|
||||
expect(result.output(), contains('test'));
|
||||
expect(result.errorOutput(), isEmpty);
|
||||
});
|
||||
|
||||
test('process with error output', () async {
|
||||
if (!Platform.isWindows) {
|
||||
try {
|
||||
await factory
|
||||
.command(['sh', '-c', 'echo "Hello World" >&2; exit 1']).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessFailedException catch (e) {
|
||||
expect(e.exitCode, equals(1));
|
||||
expect(e.output, isEmpty);
|
||||
expect(e.errorOutput.trim(), equals('Hello World'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('process can throw without output', () async {
|
||||
if (!Platform.isWindows) {
|
||||
try {
|
||||
await factory.command(['sh', '-c', 'exit 1']).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessFailedException catch (e) {
|
||||
expect(e.exitCode, equals(1));
|
||||
expect(e.output, isEmpty);
|
||||
expect(e.errorOutput, isEmpty);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('process can throw with error output', () async {
|
||||
if (!Platform.isWindows) {
|
||||
try {
|
||||
await factory
|
||||
.command(['sh', '-c', 'echo "Hello World" >&2; exit 1']).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessFailedException catch (e) {
|
||||
expect(e.exitCode, equals(1));
|
||||
expect(e.output, isEmpty);
|
||||
expect(e.errorOutput.trim(), equals('Hello World'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('process can throw with output', () async {
|
||||
if (!Platform.isWindows) {
|
||||
try {
|
||||
await factory
|
||||
.command(['sh', '-c', 'echo "Hello World"; exit 1']).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessFailedException catch (e) {
|
||||
expect(e.exitCode, equals(1));
|
||||
expect(e.output.trim(), equals('Hello World'));
|
||||
expect(e.errorOutput, isEmpty);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('process can timeout', () async {
|
||||
if (!Platform.isWindows) {
|
||||
try {
|
||||
await factory.command(['sleep', '0.5']).withTimeout(0).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessTimedOutException catch (e) {
|
||||
expect(e.message, contains('exceeded the timeout'));
|
||||
expect(e.message, contains('sleep 0.5'));
|
||||
expect(e.message, contains('0 seconds'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('process can use standard input', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final result = await factory.command(['cat']).withInput('foobar').run();
|
||||
|
||||
expect(result.output(), equals('foobar'));
|
||||
}
|
||||
});
|
||||
|
||||
test('process pipe operations', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final result = await factory.command([
|
||||
'sh',
|
||||
'-c',
|
||||
'echo "Hello, world\nfoo\nbar" | grep -i "foo"'
|
||||
]).run();
|
||||
|
||||
expect(result.output().trim(), equals('foo'));
|
||||
}
|
||||
});
|
||||
|
||||
test('process with working directory', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final result =
|
||||
await factory.command(['pwd']).withWorkingDirectory('/tmp').run();
|
||||
|
||||
expect(result.output().trim(), equals('/tmp'));
|
||||
}
|
||||
});
|
||||
|
||||
test('process with environment variables', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final result = await factory
|
||||
.command(['sh', '-c', 'echo \$TEST_VAR']).withEnvironment(
|
||||
{'TEST_VAR': 'test_value'}).run();
|
||||
|
||||
expect(result.output().trim(), equals('test_value'));
|
||||
}
|
||||
});
|
||||
|
||||
test('process output can be captured via callback', () async {
|
||||
final output = <String>[];
|
||||
|
||||
final process = await factory.command(['ls']).start((data) {
|
||||
output.add(data);
|
||||
});
|
||||
|
||||
await Future<void>.delayed(Duration(milliseconds: 100));
|
||||
expect(output, isNotEmpty);
|
||||
expect(output.join(), contains('test'));
|
||||
|
||||
await process.wait();
|
||||
});
|
||||
});
|
||||
}
|
136
packages/process/test/pending_process_test.dart
Normal file
136
packages/process/test/pending_process_test.dart
Normal file
|
@ -0,0 +1,136 @@
|
|||
import 'dart:io';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:platform_process/platform_process.dart';
|
||||
|
||||
void main() {
|
||||
group('PendingProcess Tests', () {
|
||||
late Factory factory;
|
||||
|
||||
setUp(() {
|
||||
factory = Factory();
|
||||
});
|
||||
|
||||
test('forever() disables timeout', () async {
|
||||
final process = factory.command(['sleep', '0.5']).forever();
|
||||
expect(process.timeout, isNull);
|
||||
});
|
||||
|
||||
test('withIdleTimeout() sets idle timeout', () async {
|
||||
final process = factory.command(['echo', 'test']).withIdleTimeout(5);
|
||||
expect(process.idleTimeout, equals(5));
|
||||
});
|
||||
|
||||
test('withTty() enables TTY mode', () async {
|
||||
final process = factory.command(['echo', 'test']).withTty();
|
||||
expect(process.tty, isTrue);
|
||||
});
|
||||
|
||||
test('withTty(false) disables TTY mode', () async {
|
||||
final process = factory.command(['echo', 'test']).withTty(false);
|
||||
expect(process.tty, isFalse);
|
||||
});
|
||||
|
||||
test('run() without command throws ArgumentError', () async {
|
||||
final process = PendingProcess(factory);
|
||||
expect(() => process.run(), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('start() without command throws ArgumentError', () async {
|
||||
final process = PendingProcess(factory);
|
||||
expect(() => process.start(), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('run() with command parameter overrides previous command', () async {
|
||||
final process = factory.command(['echo', 'old']);
|
||||
final result = await process.run(['echo', 'new']);
|
||||
expect(result.output().trim(), equals('new'));
|
||||
});
|
||||
|
||||
test('run() handles process exceptions', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final process = factory.command(['nonexistent']);
|
||||
expect(() => process.run(), throwsA(isA<ProcessFailedException>()));
|
||||
}
|
||||
});
|
||||
|
||||
test('start() handles process exceptions', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final process = factory.command(['nonexistent']);
|
||||
expect(() => process.start(), throwsA(isA<ProcessFailedException>()));
|
||||
}
|
||||
});
|
||||
|
||||
test('withoutOutput() disables output', () async {
|
||||
final result =
|
||||
await factory.command(['echo', 'test']).withoutOutput().run();
|
||||
expect(result.output(), isEmpty);
|
||||
});
|
||||
|
||||
test('idle timeout triggers', () async {
|
||||
if (!Platform.isWindows) {
|
||||
// Use tail -f to wait indefinitely without producing output
|
||||
final process =
|
||||
factory.command(['tail', '-f', '/dev/null']).withIdleTimeout(1);
|
||||
|
||||
await expectLater(
|
||||
process.run(),
|
||||
throwsA(
|
||||
allOf(
|
||||
isA<ProcessTimedOutException>(),
|
||||
predicate((ProcessTimedOutException e) =>
|
||||
e.message.contains('exceeded the idle timeout of 1 seconds')),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}, timeout: Timeout(Duration(seconds: 5)));
|
||||
|
||||
test('timeout triggers', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final process = factory.command(['sleep', '5']).withTimeout(1);
|
||||
|
||||
await expectLater(
|
||||
process.run(),
|
||||
throwsA(
|
||||
allOf(
|
||||
isA<ProcessTimedOutException>(),
|
||||
predicate((ProcessTimedOutException e) =>
|
||||
e.message.contains('exceeded the timeout of 1 seconds')),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}, timeout: Timeout(Duration(seconds: 5)));
|
||||
|
||||
test('immediate timeout triggers', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final process = factory.command(['sleep', '1']).withTimeout(0);
|
||||
|
||||
await expectLater(
|
||||
process.run(),
|
||||
throwsA(
|
||||
allOf(
|
||||
isA<ProcessTimedOutException>(),
|
||||
predicate((ProcessTimedOutException e) =>
|
||||
e.message.contains('exceeded the timeout of 0 seconds')),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('string command is executed through shell', () async {
|
||||
if (!Platform.isWindows) {
|
||||
final result = await factory.command('echo "Hello from shell"').run();
|
||||
expect(result.output().trim(), equals('Hello from shell'));
|
||||
}
|
||||
});
|
||||
|
||||
test('input as bytes is handled', () async {
|
||||
final process =
|
||||
factory.command(['cat']).withInput([72, 101, 108, 108, 111]);
|
||||
final result = await process.run();
|
||||
expect(result.output().trim(), equals('Hello'));
|
||||
});
|
||||
});
|
||||
}
|
70
packages/process/test/process_result_test.dart
Normal file
70
packages/process/test/process_result_test.dart
Normal file
|
@ -0,0 +1,70 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:platform_process/platform_process.dart';
|
||||
|
||||
void main() {
|
||||
group('ProcessResult', () {
|
||||
test('successful process is detected correctly', () {
|
||||
final result = ProcessResult(0, 'output', '');
|
||||
expect(result.successful(), isTrue);
|
||||
expect(result.failed(), isFalse);
|
||||
});
|
||||
|
||||
test('failed process is detected correctly', () {
|
||||
final result = ProcessResult(1, '', 'error');
|
||||
expect(result.successful(), isFalse);
|
||||
expect(result.failed(), isTrue);
|
||||
});
|
||||
|
||||
test('output methods return correct streams', () {
|
||||
final result = ProcessResult(0, 'stdout', 'stderr');
|
||||
expect(result.output(), equals('stdout'));
|
||||
expect(result.errorOutput(), equals('stderr'));
|
||||
});
|
||||
|
||||
test('toString returns stdout', () {
|
||||
final result = ProcessResult(0, 'test output', 'error output');
|
||||
expect(result.toString(), equals('test output'));
|
||||
});
|
||||
|
||||
test('empty output is handled correctly', () {
|
||||
final result = ProcessResult(0, '', '');
|
||||
expect(result.output(), isEmpty);
|
||||
expect(result.errorOutput(), isEmpty);
|
||||
});
|
||||
|
||||
test('exit code is accessible', () {
|
||||
final result = ProcessResult(123, '', '');
|
||||
expect(result.exitCode, equals(123));
|
||||
});
|
||||
|
||||
test('multiline output is preserved', () {
|
||||
final stdout = 'line1\nline2\nline3';
|
||||
final stderr = 'error1\nerror2';
|
||||
final result = ProcessResult(0, stdout, stderr);
|
||||
expect(result.output(), equals(stdout));
|
||||
expect(result.errorOutput(), equals(stderr));
|
||||
});
|
||||
|
||||
test('whitespace in output is preserved', () {
|
||||
final stdout = ' leading and trailing spaces ';
|
||||
final result = ProcessResult(0, stdout, '');
|
||||
expect(result.output(), equals(stdout));
|
||||
});
|
||||
|
||||
test('non-zero exit code indicates failure', () {
|
||||
for (var code in [1, 2, 127, 255]) {
|
||||
final result = ProcessResult(code, '', '');
|
||||
expect(result.failed(), isTrue,
|
||||
reason: 'Exit code $code should indicate failure');
|
||||
expect(result.successful(), isFalse,
|
||||
reason: 'Exit code $code should not indicate success');
|
||||
}
|
||||
});
|
||||
|
||||
test('zero exit code indicates success', () {
|
||||
final result = ProcessResult(0, '', '');
|
||||
expect(result.successful(), isTrue);
|
||||
expect(result.failed(), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
125
packages/process/test/process_test.dart
Normal file
125
packages/process/test/process_test.dart
Normal file
|
@ -0,0 +1,125 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:platform_process/platform_process.dart';
|
||||
|
||||
void main() {
|
||||
late Factory factory;
|
||||
|
||||
setUp(() {
|
||||
factory = Factory();
|
||||
});
|
||||
|
||||
group('Basic Process Operations', () {
|
||||
test('echo command returns expected output', () async {
|
||||
final result = await factory.command(['echo', 'test']).run();
|
||||
expect(result.output().trim(), equals('test'));
|
||||
expect(result.exitCode, equals(0));
|
||||
});
|
||||
|
||||
test('nonexistent command throws ProcessFailedException', () async {
|
||||
expect(
|
||||
() => factory.command(['nonexistent-command']).run(),
|
||||
throwsA(isA<ProcessFailedException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('command with arguments works correctly', () async {
|
||||
final result = await factory.command(['echo', '-n', 'test']).run();
|
||||
expect(result.output(), equals('test'));
|
||||
});
|
||||
});
|
||||
|
||||
group('Process Configuration', () {
|
||||
test('working directory is respected', () async {
|
||||
final result =
|
||||
await factory.command(['pwd']).withWorkingDirectory('/tmp').run();
|
||||
expect(result.output().trim(), equals('/tmp'));
|
||||
});
|
||||
|
||||
test('environment variables are passed correctly', () async {
|
||||
final result = await factory
|
||||
.command(['sh', '-c', 'echo \$TEST_VAR']).withEnvironment(
|
||||
{'TEST_VAR': 'test_value'}).run();
|
||||
expect(result.output().trim(), equals('test_value'));
|
||||
});
|
||||
|
||||
test('quiet mode suppresses output', () async {
|
||||
final result =
|
||||
await factory.command(['echo', 'test']).withoutOutput().run();
|
||||
expect(result.output(), isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('Async Process Operations', () {
|
||||
test('async process completes successfully', () async {
|
||||
final process = await factory.command(['sleep', '0.1']).start();
|
||||
|
||||
expect(process.pid, greaterThan(0));
|
||||
|
||||
final result = await process.wait();
|
||||
expect(result.exitCode, equals(0));
|
||||
});
|
||||
|
||||
test('process input is handled correctly', () async {
|
||||
final result =
|
||||
await factory.command(['cat']).withInput('test input').run();
|
||||
expect(result.output(), equals('test input'));
|
||||
});
|
||||
|
||||
test('process can be killed', () async {
|
||||
final process = await factory.command(['sleep', '10']).start();
|
||||
|
||||
expect(process.kill(), isTrue);
|
||||
|
||||
final result = await process.wait();
|
||||
expect(result.exitCode, isNot(0));
|
||||
});
|
||||
});
|
||||
|
||||
group('Shell Commands', () {
|
||||
test('pipe operations work correctly', () async {
|
||||
final result =
|
||||
await factory.command(['sh', '-c', 'echo hello | tr a-z A-Z']).run();
|
||||
expect(result.output().trim(), equals('HELLO'));
|
||||
});
|
||||
|
||||
test('multiple commands execute in sequence', () async {
|
||||
final result = await factory
|
||||
.command(['sh', '-c', 'echo start && sleep 0.1 && echo end']).run();
|
||||
expect(
|
||||
result.output().trim().split('\n'),
|
||||
equals(['start', 'end']),
|
||||
);
|
||||
});
|
||||
|
||||
test('complex shell operations work', () async {
|
||||
final result = await factory
|
||||
.command(['sh', '-c', 'echo "Count: 1" && echo "Count: 2"']).run();
|
||||
expect(
|
||||
result.output().trim().split('\n'),
|
||||
equals(['Count: 1', 'Count: 2']),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Error Handling', () {
|
||||
test('failed process throws with correct exit code', () async {
|
||||
try {
|
||||
await factory.command(['sh', '-c', 'exit 1']).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessFailedException catch (e) {
|
||||
expect(e.exitCode, equals(1));
|
||||
}
|
||||
});
|
||||
|
||||
test('process failure includes error output', () async {
|
||||
try {
|
||||
await factory
|
||||
.command(['sh', '-c', 'echo error message >&2; exit 1']).run();
|
||||
fail('Should have thrown');
|
||||
} on ProcessFailedException catch (e) {
|
||||
expect(e.errorOutput.trim(), equals('error message'));
|
||||
expect(e.exitCode, equals(1));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue