Skip to content

Commit

Permalink
Implement xterm v3 Terminal for LXD
Browse files Browse the repository at this point in the history
  • Loading branch information
jpnurmi committed Sep 27, 2022
1 parent 65fb8bc commit 8364284
Show file tree
Hide file tree
Showing 4 changed files with 1,706 additions and 0 deletions.
64 changes: 64 additions & 0 deletions packages/lxd_terminal/lib/lxd_terminal.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
library lxd_terminal;

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:lxd/lxd.dart';
import 'package:xterm/xterm.dart';

class LxdTerminal extends Terminal {
LxdTerminal(this.client, {required super.maxLines});

final LxdClient client;

WebSocket? _ws0;
WebSocket? _wsc;
StreamSubscription? _sub;

Future<void> close() async {
await _sub?.cancel();
await _ws0?.close();
await _wsc?.close();
}

Future<void> execute(LxdInstance instance) async {
final user = instance.config['user.name'] ?? 'root';
final op = await client.execInstance(
instance.name,
command: ['login', '-f', user],
environment: {'TERM': 'xterm-256color'},
interactive: true,
waitForWebSocket: true,
);

Future<WebSocket> getWebSocket(String id) {
final fd = op.metadata!['fds'][id] as String;
return client.getOperationWebSocket(op.id, fd);
}

_wsc = await getWebSocket('control');
onResize = (width, height, _, __) => _wsc?.sendTermSize(width, height);
_wsc!.sendTermSize(viewWidth, viewHeight);

_ws0 = await getWebSocket('0');
onOutput = (data) => _ws0?.add(utf8.encode(data));

_sub = _ws0!.listen((data) async {
if (data is List<int>) {
write(utf8.decode(data));
} else if (data is String) {
if (data.isEmpty) {
// TODO: proper way to detect exit
await close();
} else {
write('$data\r\n');
}
} else {
throw UnsupportedError('$data');
}
});

await client.waitOperation(op.id);
}
}
28 changes: 28 additions & 0 deletions packages/lxd_terminal/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: lxd_terminal
publish_to: 'none'

environment:
sdk: ">=2.17.0 <3.0.0"
flutter: ">=3.0.0"

dependencies:
flutter:
sdk: flutter
lxd:
git:
url: https://github.com/jpnurmi/lxd.dart
ref: 9799464c4488f52203407169baca963f88f0e349
xterm: # ^3.2.7
git:
ref: actions
url: https://github.com/jpnurmi/xterm.dart.git

dev_dependencies:
build_runner: ^2.1.11
flutter_lints: ^2.0.0
flutter_test:
sdk: flutter
mockito: 5.3.0

flutter:
uses-material-design: true
96 changes: 96 additions & 0 deletions packages/lxd_terminal/test/lxd_terminal_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:lxd/lxd.dart';
import 'package:lxd_terminal/lxd_terminal.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import 'lxd_terminal_test.mocks.dart';

@GenerateMocks([LxdClient, LxdInstance, LxdOperation, WebSocket])
void main() {
test('execute', () async {
final instance = MockLxdInstance();
when(instance.name).thenReturn('mine');
when(instance.config).thenReturn({'user.name': 'me'});

final exec = MockLxdOperation();
when(exec.id).thenReturn('x');
when(exec.metadata).thenReturn({
'fds': {
'0': 'fd0',
'control': 'fdc',
}
});

final wait = MockLxdOperation();
when(wait.id).thenReturn('w');

final client = MockLxdClient();
when(client.execInstance(
'mine',
command: ['login', '-f', 'me'],
environment: {'TERM': 'xterm-256color'},
interactive: true,
waitForWebSocket: true,
)).thenAnswer((_) async => exec);

final completer = Completer<LxdOperation>();
when(client.waitOperation('x')).thenAnswer((_) => completer.future);

final controller = StreamController<dynamic>(sync: true);

final ws0 = MockWebSocket();
when(client.getOperationWebSocket('x', 'fd0')).thenAnswer((_) async => ws0);
when(ws0.listen(any)).thenAnswer((i) => controller.stream
.listen(i.positionalArguments.first as void Function(dynamic)));
when(ws0.close()).thenAnswer((_) => controller.close());

final wsc = MockWebSocket();
when(client.getOperationWebSocket('x', 'fdc')).thenAnswer((_) async => wsc);
when(wsc.close()).thenAnswer((_) async {});

final terminal = TestLxdTerminal(client, maxLines: 123);
final result = terminal.execute(instance);

await untilCalled(client.waitOperation('x'));

verify(client.execInstance(
'mine',
command: ['login', '-f', 'me'],
environment: {'TERM': 'xterm-256color'},
interactive: true,
waitForWebSocket: true,
)).called(1);

verify(client.getOperationWebSocket('x', 'fd0')).called(1);
verify(client.getOperationWebSocket('x', 'fdc')).called(1);

controller.add('bytes'.codeUnits);
expect(terminal.written, ['bytes']);

controller.add('string');
expect(terminal.written, ['bytes', 'string\r\n']);

controller.add('');
expect(terminal.written, ['bytes', 'string\r\n']);

await untilCalled(ws0.close());
await untilCalled(wsc.close());

completer.complete(wait);

await expectLater(result, completes);
});
}

class TestLxdTerminal extends LxdTerminal {
TestLxdTerminal(super.client, {required super.maxLines});

final written = <String>[];

@override
void write(String data) => written.add(data);
}
Loading

0 comments on commit 8364284

Please sign in to comment.