독립 control plane 구성을 위해 기존 Dart CLI/runtime을 runner 앱 경계로 옮기고, 후속 client/core/root 작업을 같은 마일스톤 task group에 연결한다.
406 lines
12 KiB
Dart
406 lines
12 KiB
Dart
// 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<String, IsolateHandler> _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<String> get _schedulers {
|
|
HashSet<String> 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<List<ProcessInfo>> Function(String) processFinder;
|
|
final void Function(int) exitHandler;
|
|
|
|
SchedulerManager({
|
|
SystemRuntime? runtime,
|
|
Future<List<ProcessInfo>> Function(String)? processFinder,
|
|
void Function(int)? exitHandler,
|
|
}) : runtime = runtime ?? const DefaultSystemRuntime(),
|
|
processFinder = processFinder ?? findProcess,
|
|
exitHandler = exitHandler ?? exit;
|
|
|
|
factory SchedulerManager.create({
|
|
SystemRuntime? runtime,
|
|
Future<List<ProcessInfo>> 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<String, dynamic>? 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 = <String>[];
|
|
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<String> 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();
|
|
}
|