// ignore_for_file: depend_on_referenced_packages import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'package:dart_framework/platform/isolate_manager.dart'; import 'package:dart_framework/platform/process.dart'; import 'package:dart_framework/utils/os_startup.dart'; import 'package:dart_framework/utils/string_util.dart'; import 'package:dart_framework/utils/system_util.dart'; import 'package:oto/cli/commands/scheduler/data/scheduler_data.dart'; import 'package:oto/cli/commands/scheduler/scheduler_isolate.dart'; import 'package:oto/cli/commands/scheduler/scheduler_linux.dart'; import 'package:oto/cli/commands/scheduler/scheduler_osx.dart'; import 'package:oto/cli/commands/scheduler/scheduler_windows.dart'; import 'package:oto/oto/application.dart'; import 'package:oto/oto/core/system_runtime.dart'; import 'package:oto/oto/data/command_data.dart'; import 'package:path/path.dart' as path; class SchedulerManager { static String get _schedulerPath { return '$dataPath/scheduler'; } static String get _logsPath { return '$_schedulerPath/logs'; } ProcessInfo? currentScheduler; final Map _map = {}; final logFile = File( '$_logsPath/[$pid]_scheduler_${getDate()}_${getTime().replaceAll(':', '')}.txt'); String get exePath { return throw UnimplementedError('Unimplemented exePath'); } String get _settingsPath { return '$_schedulerPath/settings.json'; } SchedulerListData get _localData { var file = File(_settingsPath); if (!file.existsSync()) { _localData = SchedulerListData(); } return SchedulerListData.fromJson(jsonDecode(file.readAsStringSync())); } set _localData(SchedulerListData data) { var file = File(_settingsPath); file.writeAsStringSync(JsonEncoder.withIndent(' ').convert(data.toJson()), mode: FileMode.write, flush: true); } String get _startupID { return isDebug ? '$appIdendifier.debug' : appIdendifier; } HashSet get _schedulers { HashSet hash = HashSet(); var dir = Directory(_schedulerPath); if (dir.existsSync()) { var fileList = dir.listSync(); for (var item in fileList) { if (item is File) { if (path.extension(item.path).toLowerCase().contains('.yaml')) { hash.add(path.basenameWithoutExtension(item.path)); } } } } return hash; } final SystemRuntime runtime; final Future> Function(String) processFinder; final void Function(int) exitHandler; SchedulerManager({ SystemRuntime? runtime, Future> Function(String)? processFinder, void Function(int)? exitHandler, }) : runtime = runtime ?? const DefaultSystemRuntime(), processFinder = processFinder ?? findProcess, exitHandler = exitHandler ?? exit; factory SchedulerManager.create({ SystemRuntime? runtime, Future> Function(String)? processFinder, void Function(int)? exitHandler, }) { if (Platform.isMacOS) { return SchedulerOsx( runtime: runtime, processFinder: processFinder, exitHandler: exitHandler, ); } else if (Platform.isWindows) { return SchedulerWindows( runtime: runtime, processFinder: processFinder, exitHandler: exitHandler, ); } else if (Platform.isLinux) { return SchedulerLinux( runtime: runtime, processFinder: processFinder, exitHandler: exitHandler, ); } else { throw UnsupportedError('This platform not supported.'); } } Future regist(String filePath) async { var file = File(filePath); if (!file.existsSync()) { return throw Exception("The file '$filePath' does not exist."); } Map? map; DataBuild? build; try { map = Application.getMapFromYamlA(await file.readAsString()); build = DataBuild.fromJson(map!); } on Exception catch (e, stacktace) { print(stacktace); return throw Exception( "The file '$filePath' is not in yaml format or is an unsupported build format."); } if (build.scheduler == null) { return throw Exception('Scheduler entry does not exist within yaml.'); } var dirLogs = Directory(_logsPath); var alias = build.scheduler!.alias; var hash = _schedulers; if (hash.contains(alias)) { return throw Exception( 'A scheduler with the same alias is registered. Please do it with a different alias.'); } await _checkStartup(); await _checkProcess(); await _startProcess(); if (!dirLogs.existsSync()) { dirLogs.createSync(recursive: true); } var newPath = '$_schedulerPath/$alias.yaml'; await file.copy(newPath); print(''); print(':::: Scheduler registered.'); print(''); print('[alias]'.padRight(8) + alias); print('[path]'.padRight(8) + newPath); print(''); //update settings var local = _localData; var schedulerData = SchedulerData(); schedulerData.alias = alias; local.map[alias] = schedulerData; _localData = local; return simpleFuture; } Future unregist(String alias) async { if (_schedulers.contains(alias)) { var file = File('$_schedulerPath/$alias.yaml'); file.deleteSync(); } else { return throw Exception("There is no scheduler called '$alias'."); } var local = _localData; if (local.map.containsKey(alias)) { local.map.remove(alias); _localData = local; } await _checkTerminate(); print(''); print(":::: Scheduler '$alias's registration has been removed."); } Future showList() async { print(''); var scheduleList = _schedulers; if (scheduleList.isEmpty) { print(':::: There are no registered schedulers.'); print(''); } else { print(':::: List of currently registered schedulers.'); print(''); print('${' [Enable]'.padRight(20)}${'[Alias]'.padRight(20)}[Path]'); var local = _localData; for (var item in _schedulers) { var enable = true; if (local.map.containsKey(item)) { enable = local.map[item]!.enable; } print( '${' - $enable'.padRight(20)}${item.padRight(20)}$_schedulerPath/$item.yaml'); } print(''); } await _checkProcess(); return simpleFuture; } Future enableScheduler(String alias, bool enable) async { var local = _localData; if (_schedulers.contains(alias)) { var data = SchedulerData(); data.alias = alias; data.enable = enable; local.map[alias] = data; _localData = local; var activate = enable ? 'activated' : 'deactivate'; print(''); print("Scheduler '$alias' has been $activate."); print(''); await _checkStartup(); await _checkProcess(); await _startProcess(); exitHandler(0); } else { return throw Exception("There is no scheduler called '$alias'."); } } Future terminate() async { await _checkProcess(); if (currentScheduler != null) { runtime.killPid(currentScheduler!.pid); print('Scheduler operation stopped.'); } return simpleFuture; } Future _checkTerminate() async { if (_schedulers.isEmpty) { await _unregistStartup(); await terminate(); } return simpleFuture; } Future _startProcess() async { // Start directly if Launchd did not auto-launch the process if (currentScheduler == null) { if (runtime.isWindows) { final program = Platform.resolvedExecutable; var arguments = []; if (isDebug) arguments.add(scriptPath); arguments.addAll(['scheduler', '-s']); await runtime.startExecutable(program, arguments, mode: ProcessStartMode.detached); } else { var script = isDebug ? ' $scriptPath' : ''; var shell = StringBuffer(); shell.writeln('nohup $executablePath$script scheduler -s &'); // shell.writeln('disown'); print(shell.toString()); runtime.runShell(shell, workspace: Directory.systemTemp.path, printStdout: false); await _checkProcess(); } } return simpleFuture; } Future _checkProcess() async { var oto = runtime.isWindows ? '$appName.exe' : appName; //process check var processes = await processFinder(runtime.isWindows ? (isDebug ? 'dart.exe' : '$appName.exe') : 'scheduler -s'); for (var process in processes) { var processName = process.name; if (isDebug) { if (processName.contains('main.dart scheduler -s')) { currentScheduler = process; } } else { if (processName.contains('$oto scheduler -s')) { currentScheduler = process; } } if (currentScheduler != null) { print('Scheduler is running on pid [${currentScheduler!.pid}].'); break; } } return simpleFuture; } Future _checkStartup() async { if (!await OSStartup.isRegistedStartup(_startupID)) { List param = []; var exe = Platform.resolvedExecutable; if (isDebug) { param.add(scriptPath); } else { exe = exePath; } param.add('scheduler'); param.add('-s'); await OSStartup.registStartup(exe, _startupID, arguments: param, logErrorFilePath: '$_schedulerPath/launch_err.log'); } return simpleFuture; } Future _unregistStartup() async { if (await OSStartup.isRegistedStartup(_startupID)) { await OSStartup.unregistStartup(_startupID); } return simpleFuture; } //======================== other process ========================// Future start() async { _log('Scheduler is running on pid [$pid].'); while (true) { // Check every second for registered scheduler files await Future.delayed(Duration(seconds: 1)); var settings = _localData.map; for (var alias in _schedulers) { var enable = settings.containsKey(alias) ? settings[alias]!.enable : true; if (!_map.containsKey(alias) && enable) { _log('Schduler found: $alias'); var handler = IsolateManager.create(IsolateScheduler( {'alias': alias, 'root': _schedulerPath, 'logs': _logsPath})); handler.addListener('terminated', _onTerminated); handler.addListener('log', _onLog); _map[alias] = handler; } if (_map.containsKey(alias) && !enable) { _removeHandler(alias); } } } } void _removeHandler(String alias) { var handler = _map.remove(alias); handler!.exit(); } void _onTerminated(Object? data) async { var map = data as Map; var alias = map['alias'] as String; var type = map['type'] as String; var message = map['message'] as String; _removeHandler(alias); switch (type) { case 'error': _log('[Error] $message'); break; } var local = _localData; var list = _schedulers; if (list.isEmpty) { _log('Scheduler operation stopped.'); if (local.map.containsKey(alias)) { local.map.remove(alias); } } else { list.contains(alias); SchedulerData data = SchedulerData(); if (local.map.containsKey(alias)) { data = local.map[alias]!; } data.alias = alias; data.enable = false; local.map[alias] = data; } _localData = local; await _checkTerminate(); } void _onLog(Object? data) { _log(data as String); } void _log(String message) { logFile.writeAsStringSync('[${getDates()}] $message\n', mode: FileMode.append, flush: true); } // visible for testing Future testStartProcess() => _startProcess(); Future testCheckProcess() => _checkProcess(); }