Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add startup binary management #40

Merged
merged 10 commits into from
Nov 14, 2023
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release

# Binaries for the nodes
assets/bin
10 changes: 0 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,6 @@

![SideSail logo](logo.png)

<details>

<summary>ChatGPT logo prompt</summary>

<hidden>
Clean logo emphasizing a sail with an embedded chain pattern. The design is minimalistic, utilizing cool blues, greys, and whites, with a slightly different shape for the sail.
</hidden>

</details>

A work-in-progress sidechain UI.

```shell
Expand Down
Empty file added assets/bin/.gitkeep
Empty file.
202 changes: 190 additions & 12 deletions lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:sail_ui/sail_ui.dart';
import 'package:sail_ui/theme/theme.dart';
import 'package:sail_ui/theme/theme_data.dart';
import 'package:sail_ui/widgets/core/sail_text.dart';
import 'package:sail_ui/widgets/core/scaffold.dart';
import 'package:sail_ui/widgets/loading_indicator.dart';
import 'package:sidesail/config/runtime_args.dart';
import 'package:sidesail/config/sidechains.dart';
import 'package:sidesail/logger.dart';
import 'package:sidesail/providers/proc_provider.dart';
import 'package:sidesail/routing/router.dart';
import 'package:sidesail/rpc/rpc.dart';
import 'package:sidesail/rpc/rpc_config.dart';
import 'package:sidesail/rpc/rpc_mainchain.dart';
import 'package:sidesail/rpc/rpc_sidechain.dart';
import 'package:sidesail/storage/client_settings.dart';
import 'package:sidesail/storage/sail_settings/theme_settings.dart';
Expand Down Expand Up @@ -34,16 +46,44 @@ class SailApp extends StatefulWidget {
}

class SailAppState extends State<SailApp> with WidgetsBindingObserver {
SailThemeData? theme;
AppRouter get router => GetIt.I.get<AppRouter>();
SidechainRPC get _sidechain => GetIt.I.get<SidechainRPC>();
SidechainContainer get _sidechain => GetIt.I.get<SidechainContainer>();
MainchainRPC get mainchain => GetIt.I.get<MainchainRPC>();
ProcessProvider get processes => GetIt.I.get<ProcessProvider>();
final settings = GetIt.I.get<ClientSettings>();

/// Unrecoverable error on startup we can't get past.
dynamic _initBinariesError;
String mainchainStartupMessage = 'Checking if mainchain is started';
String sidechainStartupMessage = 'Checking if sidechain is started';
bool binariesStarted = false;
SailThemeData? theme;

@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
loadTheme();
unawaited(loadTheme());
unawaited(
initRPCs().then(
(_) => Future.wait([
initMainchainBinary(),
initSidechainBinary(),
// let the spinner work for at least one second, to avoid flickering
Future.delayed(const Duration(seconds: 1)),
])
.then(
(_) => setState(() {
binariesStarted = true;
}),
)
.onError(
(error, stackTrace) => setState(() {
_initBinariesError = error.toString();
}),
),
),
);
}

void rebuildUI() {
Expand All @@ -56,19 +96,125 @@ class SailAppState extends State<SailApp> with WidgetsBindingObserver {
SailThemeData _themeDataFromTheme(SailThemeValues theme) {
switch (theme) {
case SailThemeValues.light:
return SailThemeData.lightTheme(_sidechain.chain.color);
return SailThemeData.lightTheme(_sidechain.rpc.chain.color);
case SailThemeValues.dark:
return SailThemeData.darkTheme(_sidechain.chain.color);
return SailThemeData.darkTheme(_sidechain.rpc.chain.color);
default:
throw Exception('Could not get theme');
}
}

void loadTheme([SailThemeValues? themeToLoad]) async {
Future<void> initRPCs() async {
// Not ideal, but fuck it
if (RuntimeArgs.isInTest) {
return;
}

final mainchainFut = readRpcConfig(mainchainDatadir(), 'drivechain.conf', null).then(
(conf) async {
log.d('read mainchain RPC configuration');
mainchain.conf = conf;
final (connected, connectionError) = await mainchain.testConnection();

if (!connected) {
log.w('mainchain NOT connected: $connectionError');
} else {
log.d('mainchain connected');
}
},
);

final sidechainFut = readRpcConfig(
_sidechain.rpc.chain.type.datadir(),
_sidechain.rpc.chain.type.confFile(),
_sidechain.rpc.chain,
).then((conf) async {
log.d('read sidechain RPC configuration');
_sidechain.rpc.conf = conf;

final (connected, connectionError) = await _sidechain.rpc.testConnection();
if (!connected) {
log.w('sidechain NOT connected: $connectionError');
} else {
log.d('sidechain connected');
}
});

await Future.wait([mainchainFut, sidechainFut]);
}

Future<void> initMainchainBinary() async {
return _initBinary('drivechaind', mainchain, (msg) {
setState(
() {
mainchainStartupMessage = msg;
},
);
});
}

Future<void> initSidechainBinary() async {
return _initBinary(_sidechain.rpc.chain.binary, _sidechain.rpc, (msg) {
setState(
() {
sidechainStartupMessage = msg;
},
);
});
}

Future<void> _initBinary(String binary, RPCConnection conn, void Function(String) updateMsg) async {
log.d('init binaries: checking $binary connection');

// First, let the RPC connection check finish.
await conn.initDone;

// If we managed to connect, we're finished here!
if (conn.connected) {
updateMsg("'$binary' is already running");
log.d('init binaries: $binary is already running, not doing anything');
return;
}

// We have to start the mainchain
final tempDir = await Directory.systemTemp.createTemp(binary);
final tempLogFile = await File('${tempDir.path}/$binary.debug.log').create(recursive: true);

if (!context.mounted) {
return;
}

// TODO: update this when adding other nodes with different args
final args = [
'-debuglogfile=${tempLogFile.path}',
'-regtest',
];

log.d('init binaries: starting $binary $args');

try {
final pid = await processes.start(context, binary, args);
log.d('init binaries: started $binary with PID $pid');
updateMsg("Started '$binary', waiting for successful RPC connection");
} catch (err) {
throw 'unable to start $binary: $err';
}

// Add timeout?
log.i('init binaries: waiting for $binary connection');
await waitForBoolToBeTrue(() async {
final (connected, _) = await conn.testConnection();
return connected;
});

log.i('init binaries: $binary connected');
}

Future<void> loadTheme([SailThemeValues? themeToLoad]) async {
themeToLoad ??= (await settings.getValue(ThemeSetting())).value;
if (themeToLoad == SailThemeValues.platform) {
// ignore: deprecated_member_use
themeToLoad = WidgetsBinding.instance.window.platformBrightness == Brightness.light //
themeToLoad = WidgetsBinding.instance.window.platformBrightness == Brightness.light
? SailThemeValues.light
: SailThemeValues.dark;
}
Expand All @@ -79,17 +225,38 @@ class SailAppState extends State<SailApp> with WidgetsBindingObserver {
await settings.setValue(ThemeSetting(newValue: themeToLoad));
}

bool get inInitialSetup => theme == null || !binariesStarted;

Widget loadingIndicator() {
if (!binariesStarted) {
return SailColumn(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
spacing: SailStyleValues.padding25,
children: [
SailText.primary20('Mainchain starting up: $mainchainStartupMessage'),
SailText.primary20('Sidechain starting up: $sidechainStartupMessage'),
const SailCircularProgressIndicator(),
],
);
}
return const SailCircularProgressIndicator();
}

@override
Widget build(BuildContext context) {
if (theme == null) {
if (inInitialSetup) {
return MaterialApp(
home: Material(
color: _sidechain.chain.color,
color: _sidechain.rpc.chain.color,
child: SailTheme(
data: SailThemeData.lightTheme(_sidechain.chain.color),
child: const SailScaffold(
data: SailThemeData.lightTheme(_sidechain.rpc.chain.color),
child: SailScaffold(
body: Center(
child: SailCircularProgressIndicator(),
// TODO: better error message with troubleshooting tips here
child: _initBinariesError != null
? SailText.primary24('Ran into unrecoverable error on startup: $_initBinariesError')
: loadingIndicator(),
Comment on lines +256 to +259
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@octobocto input appreciated. When this branch is triggered we're at a unrecoverable state, and most likely it's the programmer's fault. Shouldn't happen with the end user, but if it happens they should get a good error message

),
),
),
Expand All @@ -109,3 +276,14 @@ class SailAppState extends State<SailApp> with WidgetsBindingObserver {
super.dispose();
}
}

Future<void> waitForBoolToBeTrue(
Future<bool> Function() boolGetter, {
Duration pollInterval = const Duration(milliseconds: 100),
}) async {
bool result = await boolGetter();
if (!result) {
await Future.delayed(pollInterval);
await waitForBoolToBeTrue(boolGetter, pollInterval: pollInterval);
}
}
62 changes: 37 additions & 25 deletions lib/config/dependencies.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:get_it/get_it.dart';
import 'package:sidesail/config/sidechains.dart';
import 'package:sidesail/pages/tabs/settings/node_settings_tab.dart';
import 'package:sidesail/providers/balance_provider.dart';
import 'package:sidesail/providers/proc_provider.dart';
import 'package:sidesail/providers/transactions_provider.dart';
import 'package:sidesail/routing/router.dart';
import 'package:sidesail/rpc/rpc_ethereum.dart';
Expand All @@ -10,15 +12,19 @@ import 'package:sidesail/rpc/rpc_testchain.dart';
import 'package:sidesail/storage/client_settings.dart';
import 'package:sidesail/storage/secure_store.dart';

// This gets overwritten at a later point, just here to make the
// type system happy.
final _emptyNodeConf = SingleNodeConnectionSettings('', '', 0, '', '');

// register all global dependencies, for use in views, or in view models
// each dependency can only be registered once
Future<void> initGetitDependencies(Sidechain initialChain) async {
final mainFuture = MainchainRPCLive.create();
await _initSidechainRPC(initialChain);
final mainRPC = await mainFuture;
Future<void> initDependencies(Sidechain chain) async {
GetIt.I.allowReassignment = true;

await _initSidechainRPC(chain);

GetIt.I.registerLazySingleton<MainchainRPC>(
() => mainRPC,
() => MainchainRPCLive(conf: _emptyNodeConf),
);

GetIt.I.registerLazySingleton<AppRouter>(
Expand All @@ -34,6 +40,9 @@ Future<void> initGetitDependencies(Sidechain initialChain) async {
// we start polling for balance updates
() => BalanceProvider(),
);

GetIt.I.registerLazySingleton(() => ProcessProvider());

GetIt.I.registerLazySingleton<TransactionsProvider>(
() => TransactionsProvider(),
);
Expand All @@ -43,31 +52,34 @@ Future<void> initGetitDependencies(Sidechain initialChain) async {
// rpcs in parallell, so they're ready instantly when swapping
// we can also query the balance
Future<void> _initSidechainRPC(Sidechain chain) async {
final ethFuture = EthereumRPCLive.create();
final testFuture = TestchainRPCLive.create();

final ethRPC = await ethFuture;
final testRPC = await testFuture;

GetIt.I.registerLazySingleton<TestchainRPC>(
() => testRPC,
);
GetIt.I.registerLazySingleton<EthereumRPC>(
() => ethRPC,
);

SidechainSubRPC sidechainSubRPC;
SidechainRPC Function() getSidechain;
switch (chain.type) {
case SidechainType.testChain:
sidechainSubRPC = testRPC;
break;
getSidechain = () => GetIt.I.get<TestchainRPC>();

case SidechainType.ethereum:
sidechainSubRPC = ethRPC;
break;
getSidechain = () => GetIt.I.get<EthereumRPC>();
}
GetIt.I.registerLazySingleton<TestchainRPC>(
() => TestchainRPCLive(conf: _emptyNodeConf),
);
GetIt.I.registerLazySingleton<EthereumRPC>(
() {
final sc = EthereumSidechain();
// TODO: make this properly configurable
return EthereumRPCLive(
conf: SingleNodeConnectionSettings(
'todo.conf',
'localhost',
sc.rpcPort,
'',
'',
),
);
},
);

GetIt.I.registerLazySingleton<SidechainRPC>(
() => SidechainRPC(subRPC: sidechainSubRPC),
GetIt.I.registerLazySingleton<SidechainContainer>(
() => SidechainContainer(getSidechain()),
);
}
Loading