Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## [2.22.1] - 2026-07-13
- Fixed a keepalive issue where overlapping pings could occur and caught errors during ping execution. Thanks [@vicajilau].

## [2.22.0] - 2026-07-03
- Added optional `handshakeTimeout` and `authTimeout` to `SSHClient` to limit connection negotiation and user authentication times [#182]. Thanks [@GT-610].

Expand Down
12 changes: 11 additions & 1 deletion lib/src/ssh_keepalive.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,24 @@ class SSHKeepAlive {

final Future Function() ping;

bool _isPinging = false;

SSHKeepAlive({
required this.ping,
this.interval = const Duration(seconds: 10),
});

void start() {
_timer ??= Timer.periodic(interval, (timer) async {
await ping();
if (_isPinging) return;
_isPinging = true;
try {
await ping();
} catch (_) {
// Ignore errors, the client transport will handle disconnection.
} finally {
_isPinging = false;
}
});
}

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: dartssh2
version: 2.22.0
version: 2.22.1
description: SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as easy to use.
homepage: https://github.com/TerminalStudio/dartssh2

Expand Down
84 changes: 84 additions & 0 deletions test/src/ssh_keepalive_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import 'dart:async';

import 'package:dartssh2/src/ssh_keepalive.dart';
import 'package:test/test.dart';

void main() {
group('SSHKeepAlive', () {
test('calls ping at specified interval', () async {
var pingCount = 0;
final completer = Completer<void>();

final keepAlive = SSHKeepAlive(
interval: const Duration(milliseconds: 10),
ping: () async {
pingCount++;
if (pingCount >= 3) {
completer.complete();
}
},
);

keepAlive.start();
await completer.future;
keepAlive.stop();

expect(pingCount, greaterThanOrEqualTo(3));
});

test('prevents overlapping pings', () async {
var pingCount = 0;
var activePings = 0;
var maxActivePings = 0;
final completer = Completer<void>();

final keepAlive = SSHKeepAlive(
interval: const Duration(milliseconds: 10),
ping: () async {
pingCount++;
activePings++;
if (activePings > maxActivePings) {
maxActivePings = activePings;
}
// Sleep longer than the interval to cause an overlapping tick
await Future.delayed(const Duration(milliseconds: 50));
activePings--;
if (pingCount >= 2 && !completer.isCompleted) {
completer.complete();
}
},
);

keepAlive.start();
// Wait for a few intervals. If overlapping wasn't prevented, activePings would exceed 1.
await Future.delayed(const Duration(milliseconds: 100));
keepAlive.stop();

expect(maxActivePings, equals(1));
});

test('handles ping errors and resets isPinging status', () async {
var pingCount = 0;
final completer = Completer<void>();

final keepAlive = SSHKeepAlive(
interval: const Duration(milliseconds: 10),
ping: () async {
pingCount++;
if (pingCount == 1) {
throw Exception('ping failed');
}
if (pingCount == 2) {
completer.complete();
}
},
);

keepAlive.start();
await completer.future;
keepAlive.stop();

expect(pingCount, equals(2));
});
});
}