platform/packages/app/lib/src/application.dart
2024-09-03 13:14:31 -07:00

237 lines
7.5 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:protevus_application/application.dart';
import 'package:protevus_openapi/v3.dart';
import 'package:protevus_runtime/runtime.dart';
import 'package:logging/logging.dart';
export 'application_server.dart';
export 'options.dart';
export 'starter.dart';
/// This object starts and stops instances of your [ApplicationChannel].
///
/// An application object opens HTTP listeners that forward requests to instances of your [ApplicationChannel].
/// It is unlikely that you need to use this class directly - the `conduit serve` command creates an application object
/// on your behalf.
class Application<T extends ApplicationChannel> {
/// A list of isolates that this application supervises.
List<ApplicationIsolateSupervisor> supervisors = [];
/// The [ApplicationServer] listening for HTTP requests while under test.
///
/// This property is only valid when an application is started via [startOnCurrentIsolate].
late ApplicationServer server;
/// The [ApplicationChannel] handling requests while under test.
///
/// This property is only valid when an application is started via [startOnCurrentIsolate]. You use
/// this value to access elements of your application channel during testing.
T get channel => server.channel as T;
/// The logger that this application will write messages to.
///
/// This logger's name will appear as 'conduit'.
Logger logger = Logger("conduit");
/// The options used to configure this application.
///
/// Changing these values once the application has started will have no effect.
ApplicationOptions options = ApplicationOptions();
/// The duration to wait for each isolate during startup before failing.
///
/// A [TimeoutException] is thrown if an isolate fails to startup in this time period.
///
/// Defaults to 30 seconds.
Duration isolateStartupTimeout = const Duration(seconds: 30);
/// Whether or not this application is running.
///
/// This will return true if [start]/[startOnCurrentIsolate] have been invoked and completed; i.e. this is the synchronous version of the [Future] returned by [start]/[startOnCurrentIsolate].
///
/// This value will return to false after [stop] has completed.
bool get isRunning => _hasFinishedLaunching;
bool _hasFinishedLaunching = false;
ChannelRuntime get _runtime => RuntimeContext.current[T] as ChannelRuntime;
/// Starts this application, allowing it to handle HTTP requests.
///
/// This method spawns [numberOfInstances] isolates, instantiates your application channel
/// for each of these isolates, and opens an HTTP listener that sends requests to these instances.
///
/// The [Future] returned from this method will complete once all isolates have successfully started
/// and are available to handle requests.
///
/// If your application channel implements [ApplicationChannel.initializeApplication],
/// it will be invoked prior to any isolate being spawned.
///
/// See also [startOnCurrentIsolate] for starting an application when running automated tests.
Future start({int numberOfInstances = 1, bool consoleLogging = false}) async {
if (supervisors.isNotEmpty) {
throw StateError(
"Application error. Cannot invoke 'start' on already running Conduit application.",
);
}
if (options.address == null) {
if (options.isIpv6Only) {
options.address = InternetAddress.anyIPv6;
} else {
options.address = InternetAddress.anyIPv4;
}
}
try {
await _runtime.runGlobalInitialization(options);
for (var i = 0; i < numberOfInstances; i++) {
final supervisor = await _spawn(
this,
options,
i + 1,
logger,
isolateStartupTimeout,
logToConsole: consoleLogging,
);
supervisors.add(supervisor);
await supervisor.resume();
}
} catch (e, st) {
logger.severe("$e", this, st);
await stop().timeout(const Duration(seconds: 5));
rethrow;
}
for (final sup in supervisors) {
sup.sendPendingMessages();
}
_hasFinishedLaunching = true;
}
/// Starts the application on the current isolate, and does not spawn additional isolates.
///
/// An application started in this way will run on the same isolate this method is invoked on.
/// Performance is limited when running the application with this method; prefer to use [start].
Future startOnCurrentIsolate() async {
if (supervisors.isNotEmpty) {
throw StateError(
"Application error. Cannot invoke 'test' on already running Conduit application.",
);
}
options.address ??= InternetAddress.loopbackIPv4;
try {
await _runtime.runGlobalInitialization(options);
server = ApplicationServer(_runtime.channelType, options, 1);
await server.start();
_hasFinishedLaunching = true;
} catch (e, st) {
logger.severe("$e", this, st);
await stop().timeout(const Duration(seconds: 5));
rethrow;
}
}
/// Stops the application from running.
///
/// Closes every isolate and their channel and stops listening for HTTP requests.
/// The [ServiceRegistry] will close any of its resources.
Future stop() async {
_hasFinishedLaunching = false;
await Future.wait(supervisors.map((s) => s.stop()))
.onError((error, stackTrace) {
if (error.runtimeType.toString() == 'LateError') {
throw StateError(
'Channel type $T was not loaded in the current isolate. Check that the class was declared and public.',
);
}
throw error! as Error;
});
try {
await server.server.close(force: true);
} catch (e) {
logger.severe(e);
}
_hasFinishedLaunching = false;
supervisors = [];
logger.clearListeners();
}
/// Creates an [APIDocument] from an [ApplicationChannel].
///
/// This method is called by the `conduit document` CLI.
static Future<APIDocument> document(
Type type,
ApplicationOptions config,
Map<String, dynamic> projectSpec,
) async {
final runtime = RuntimeContext.current[type] as ChannelRuntime;
await runtime.runGlobalInitialization(config);
final server = ApplicationServer(runtime.channelType, config, 1);
await server.channel.prepare();
final doc = await server.channel.documentAPI(projectSpec);
await server.channel.close();
return doc;
}
Future<ApplicationIsolateSupervisor> _spawn(
Application application,
ApplicationOptions config,
int identifier,
Logger logger,
Duration startupTimeout, {
bool logToConsole = false,
}) async {
final receivePort = ReceivePort();
final libraryUri = _runtime.libraryUri;
final typeName = _runtime.name;
final entryPoint = _runtime.isolateEntryPoint;
final initialMessage = ApplicationInitialServerMessage(
typeName,
libraryUri,
config,
identifier,
receivePort.sendPort,
logToConsole: logToConsole,
);
final isolate =
await Isolate.spawn(entryPoint, initialMessage, paused: true);
return ApplicationIsolateSupervisor(
application,
isolate,
receivePort,
identifier,
logger,
startupTimeout: startupTimeout,
);
}
}
/// Thrown when an application encounters an exception during startup.
///
/// Contains the original exception that halted startup.
class ApplicationStartupException implements Exception {
ApplicationStartupException(this.originalException);
dynamic originalException;
@override
String toString() => originalException.toString();
}