Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 6 additions & 2 deletions packages/fuchsia_ctl/bin/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ Future<void> main(List<String> args) async {
..addOption('identity-file',
defaultsTo: '.ssh/pkey', help: 'The key to use when SSHing.')
..addOption('timeout-seconds',
defaultsTo: '120', help: 'Ssh command timeout in seconds.');
defaultsTo: '120', help: 'Ssh command timeout in seconds.')
..addOption('log-file',
defaultsTo: '', help: 'The file to write stdout and stderr.');
parser.addCommand('pave')
..addOption('public-key',
abbr: 'p', help: 'The public key to add to authorized_keys.')
Expand Down Expand Up @@ -176,6 +178,7 @@ Future<OperationResult> ssh(
const SshClient sshClient = SshClient();
final String targetIp = await devFinder.getTargetAddress(deviceName);
final String identityFile = args['identity-file'];
final String outputFile = args['log-file'];
if (args['interactive']) {
return await sshClient.interactive(
targetIp,
Expand All @@ -186,7 +189,8 @@ Future<OperationResult> ssh(
identityFilePath: identityFile,
command: args['command'].split(' '),
timeoutMs:
Duration(milliseconds: int.parse(args['timeout-seconds']) * 1000));
Duration(milliseconds: int.parse(args['timeout-seconds']) * 1000),
logFilePath: outputFile);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: trailing comma will make this format a bit more nicely

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

stdout.writeln(
'==================================== STDOUT ====================================');
stdout.writeln(result.info);
Expand Down
84 changes: 84 additions & 0 deletions packages/fuchsia_ctl/lib/src/logger.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import 'dart:io';

/// Defines the available log levels.
class LogLevel {
const LogLevel._(this._level, this.name);

final int _level;

/// String name for the log level.
final String name;

/// LogLevel for messages instended for debugging.
static const LogLevel debug = LogLevel._(0, 'DEBUG');

/// LogLevel for messages instended to provide information about the exection.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: typo in execution

static const LogLevel info = LogLevel._(1, 'INFO');

/// LogLevel for messages instended to flag potential problems.
static const LogLevel warning = LogLevel._(2, 'WARN');

/// LogLevel for errors in the execution.
static const LogLevel error = LogLevel._(3, 'ERROR');
}

/// Abstract class for loggers.
abstract class Logger {
/// Processes a debug message.
void debug(Object message);

/// Processes an info message.
void info(Object message);

/// Processes a warning message.
void warning(Object message);

/// Processes an error message.
void error(Object message);
}

/// Logger to print message to standard output.
class PrintLogger implements Logger {
/// Creates a logger instance to print messages to standard output.
PrintLogger({
IOSink out,
this.level = LogLevel.info,
}) : out = out ?? stdout;

/// The [IOSink] to print to.
final IOSink out;

/// Available log levels.
final LogLevel level;

@override
void debug(Object message) => _log(LogLevel.debug, message);

@override
void info(Object message) => _log(LogLevel.info, message);

@override
void warning(Object message) => _log(LogLevel.warning, message);

@override
void error(Object message) => _log(LogLevel.error, message);

void _log(LogLevel level, Object message) {
if (level._level >= this.level._level)
out.writeln(toLogString('$message', level: level));
}
}

/// Transforms a [message] with [level] to a string that contains the DateTime,
/// level and message.
String toLogString(String message, {LogLevel level}) {
final StringBuffer buffer = StringBuffer();
buffer.write(DateTime.now().toIso8601String());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
buffer.write(DateTime.now().toIso8601String());
buffer.write(DateTime.now().toUtc().toIso8601String());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

buffer.write(': ');
if (level != null) {
buffer.write(level.name);
buffer.write(' ');
}
buffer.write(message);
return buffer.toString();
}
90 changes: 79 additions & 11 deletions packages/fuchsia_ctl/lib/src/ssh_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:file/memory.dart';
import 'package:fuchsia_ctl/src/logger.dart';
import 'package:meta/meta.dart';
import 'package:pedantic/pedantic.dart';
import 'package:process/process.dart';

import 'operation_result.dart';
Expand Down Expand Up @@ -92,21 +97,84 @@ class SshClient {
Future<OperationResult> runCommand(String targetIp,
{@required String identityFilePath,
@required List<String> command,
Duration timeoutMs = defaultSshTimeoutMs}) async {
Duration timeoutMs = defaultSshTimeoutMs,
String logFilePath,
FileSystem fs}) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: trailing comma for arg list

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

assert(targetIp != null);
assert(identityFilePath != null);
assert(command != null);

return OperationResult.fromProcessResult(
await processManager
.run(
getSshArguments(
identityFilePath: identityFilePath,
targetIp: targetIp,
command: command,
),
)
.timeout(timeoutMs),
final bool logToFile = !(logFilePath == null || logFilePath.isEmpty);
FileSystem fileSystem = fs ?? const LocalFileSystem();
IOSink logFile;
Logger logger;

// If no file is passed to this method we create a memoryfile to keep to
// return the stdout in OperationResult.
if (logToFile) {
fileSystem.file(logFilePath).existsSync() ??
fileSystem.file(logFilePath).deleteSync();
fileSystem.file(logFilePath).createSync();
final IOSink data = fileSystem.file(logFilePath).openWrite();
logger = PrintLogger(out: data);
} else {
fileSystem = MemoryFileSystem();
fileSystem.file('logs')..createSync();
logFile = fileSystem.file('logs').openWrite();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it's not used below and the output will just be an empty string if no log file is specified.

I think we might be better off having some kind of BufferedLogger class that would allow us to retrieve the buffer as well as writing it to a file if needed.

Alternatively, if we're logging this to a file/stdout, do we really need to return stdout/stderr back to the caller?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed from here and moved the functionality to the logger class.

logger = PrintLogger();
}

final Process process = await processManager.start(
getSshArguments(
identityFilePath: identityFilePath,
targetIp: targetIp,
command: command,
),
);
final StreamSubscription<String> stdoutSubscription = process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((String log) {
if (!logToFile) {
logFile.writeln(log);
} else {
logger.info(log);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we just do logger.info(log) here? Won't it write to the file if it's attached to the IOSink, and otherwise jut write to stdout?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. It was implemented like that because logfile adds datetime and log level and all the ssh commands in fuchsia_ctl expects output with no additional prepended.

});
final StreamSubscription<String> stderrSubscription = process.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((String log) {
if (!logToFile) {
logFile.writeln(log);
} else {
logger.warning(log);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to error.

}
});

// Wait for stdout and stderr to be fully processed because proc.exitCode
// may complete first.
await Future.wait<void>(<Future<void>>[
stdoutSubscription.asFuture<void>(),
stderrSubscription.asFuture<void>(),
]);
// The streams as futures have already completed, so waiting for the
// potentially async stream cancellation to complete likely has no benefit.
unawaited(stdoutSubscription.cancel());
unawaited(stderrSubscription.cancel());

final int exitCode = await process.exitCode.timeout(timeoutMs);

String output = '';
if (!logToFile) {
logFile
..flush()
..close();
output = await fileSystem.file('logs').readAsString();
}

return exitCode != 0
? OperationResult.error('Failed', info: output)
: OperationResult.success(info: output);
}
}
3 changes: 2 additions & 1 deletion packages/fuchsia_ctl/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ dependencies:
file: ^5.0.10
meta: ^1.1.7
path: ^1.6.4
pedantic: ^1.9.2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid having pedantic as a direct dependency - use it as a dev dependency instead. Although this package is not consumed by anyone, this can cause problems if we later need to use some other package that's also using pedantic with an incompatible constraint. Pedantic's semver constraints are really more about the analysis option rules anyway, rather than the unawaited function.

We should just write our own unawaited function, which is literally one line of code, where we need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

process: ^3.0.12
retry: ^3.0.0+1
uuid: ^2.0.2

dev_dependencies:
test: ^1.6.9
mockito: ^4.1.1
test: ^1.6.9
9 changes: 9 additions & 0 deletions packages/fuchsia_ctl/test/logger_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright 2020 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:test/test.dart';

void main() {
test('', () async {});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe forgot to save before committing? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep uploaded the version with the tests.

38 changes: 32 additions & 6 deletions packages/fuchsia_ctl/test/ssh_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
// found in the LICENSE file.

import 'dart:async';
import 'dart:io' show ProcessResult;

import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:fuchsia_ctl/fuchsia_ctl.dart';
import 'package:fuchsia_ctl/src/ssh_client.dart';
import 'package:fuchsia_ctl/src/operation_result.dart';
Expand Down Expand Up @@ -51,8 +52,8 @@ void main() {
const List<String> command = <String>['ls', '-al'];
final MockProcessManager processManager = MockProcessManager();

when(processManager.run(any)).thenAnswer((_) async {
return ProcessResult(0, 0, 'Good job', '');
when(processManager.start(any)).thenAnswer((_) async {
return FakeProcess(0, <String>['abc'], <String>['cdf']);
});

final SshClient ssh = SshClient(processManager: processManager);
Expand All @@ -64,7 +65,7 @@ void main() {
);

final List<String> capturedStartArgs =
verify(processManager.run(captureAny))
verify(processManager.start(captureAny))
.captured
.cast<List<String>>()
.single;
Expand All @@ -76,6 +77,31 @@ void main() {
targetIp: targetIp,
command: command,
));
expect(result.info, 'abc\ncdf\n');
expect(result.success, true);
});

test('Command output is written to a log file', () async {
const List<String> command = <String>['ls', '-al'];
final MockProcessManager processManager = MockProcessManager();

when(processManager.start(any)).thenAnswer((_) async {
return FakeProcess(0, <String>['ef'], <String>['abc']);
});

final SshClient ssh = SshClient(processManager: processManager);
final FileSystem fs = MemoryFileSystem();
final OperationResult result = await ssh.runCommand(
targetIp,
identityFilePath: identityFilePath,
command: command,
fs: fs,
logFilePath: 'myfile.txt',
);

final String content = await fs.file('myfile.txt').readAsString();
expect(content, contains('WARN abc'));
expect(content, contains('INFO ef'));
expect(result.success, true);
});

Expand All @@ -92,9 +118,9 @@ void main() {
test('sshCommand times out', () async {
final MockProcessManager processManager = MockProcessManager();

when(processManager.run(any)).thenAnswer((_) async {
when(processManager.start(any)).thenAnswer((_) async {
await Future<void>.delayed(const Duration(milliseconds: 3));
return ProcessResult(0, 0, 'Good job', '');
return FakeProcess(0, <String>[''], <String>['']);
});

final SshClient ssh = SshClient(processManager: processManager);
Expand Down