diff --git a/lib/src/ssh_session.dart b/lib/src/ssh_session.dart index f22d2bc..fe1514e 100644 --- a/lib/src/ssh_session.dart +++ b/lib/src/ssh_session.dart @@ -35,6 +35,12 @@ class SSHSession { SSHSession(this._channel) { _channel.setRequestHandler(_handleRequest); + done.then((_) { + if (!_exitCompleter.isCompleted) { + _exitCompleter.complete(_exitCode); + } + }); + _channelDataSubscription = _channel.stream.listen( _handleChannelData, onDone: _handleChannelDataDone, @@ -51,6 +57,8 @@ class SSHSession { SSHSessionExitSignal? _exitSignal; + final _exitCompleter = Completer(); + late final StreamSubscription _channelDataSubscription; late final _stdinController = StreamController(); @@ -103,6 +111,18 @@ class SSHSession { _channel.close(); } + /// Waits for the remote process to report an exit status. + /// + /// Returns the exit status, or `null` if the process exited without reporting + /// one, was terminated by a signal, or [timeout] elapsed before it exited. + Future waitForExit({Duration? timeout}) { + Future wait = _exitCompleter.future; + if (timeout != null) { + wait = wait.timeout(timeout, onTimeout: () => null); + } + return wait; + } + /// Deliver [signal] to the remote process. Some implementations may not /// support this. void kill(SSHSignal signal) { @@ -113,6 +133,9 @@ class SSHSession { switch (request.requestType) { case SSHChannelRequestType.exitStatus: _exitCode = request.exitStatus!; + if (!_exitCompleter.isCompleted) { + _exitCompleter.complete(_exitCode); + } return true; case SSHChannelRequestType.exitSignal: _exitSignal = SSHSessionExitSignal( @@ -121,6 +144,9 @@ class SSHSession { errorMessage: request.errorMessage!, languageTag: request.languageTag!, ); + if (!_exitCompleter.isCompleted) { + _exitCompleter.complete(null); + } return true; } return false; diff --git a/test/src/ssh_client_run_with_result_test.dart b/test/src/ssh_client_run_with_result_test.dart index 057c50a..006d092 100644 --- a/test/src/ssh_client_run_with_result_test.dart +++ b/test/src/ssh_client_run_with_result_test.dart @@ -8,6 +8,65 @@ import 'package:dartssh2/src/ssh_channel.dart'; import 'package:test/test.dart'; void main() { + group('SSHSession.waitForExit', () { + test('returns exit status after channel closes', () async { + final harness = _SessionHarness(); + + final exit = harness.session.waitForExit(); + harness.sendExitStatus(7); + harness.close(); + + expect(await exit, 7); + + harness.dispose(); + }); + + test('returns exit status before channel closes', () async { + final harness = _SessionHarness(); + + final exit = harness.session.waitForExit(); + harness.sendExitStatus(7); + + expect(await exit, 7); + + harness.dispose(); + }); + + test('returns null when timeout elapses before exit', () async { + final harness = _SessionHarness(); + + final exit = await harness.session.waitForExit( + timeout: const Duration(milliseconds: 10), + ); + + expect(exit, isNull); + expect(harness.session.exitCode, isNull); + + harness.dispose(); + }); + + test('returns null when channel closes without exit status', () async { + final harness = _SessionHarness(); + + final exit = harness.session.waitForExit(); + harness.close(); + + expect(await exit, isNull); + + harness.dispose(); + }); + + test('returns existing exit status immediately', () async { + final harness = _SessionHarness(); + + harness.sendExitStatus(3); + + expect(await harness.session.waitForExit(), 3); + + harness.dispose(); + }); + }); + group('SSHClient.runWithResult', () { test('captures stdout/stderr and exit status', () async { final harness = _SessionHarness();