oto/apps/runner/lib/cli/commands/scheduler/scheduler_isolate.dart
toki 86afabb3eb refactor(runner): 런타임 패키지를 앱 하위로 이동한다
독립 control plane 구성을 위해 기존 Dart CLI/runtime을 runner 앱 경계로 옮기고, 후속 client/core/root 작업을 같은 마일스톤 task group에 연결한다.
2026-06-05 06:34:45 +09:00

159 lines
4.7 KiB
Dart

import 'dart:io';
import 'package:cron/cron.dart';
import 'package:dart_framework/data/isolate_data.dart';
import 'package:dart_framework/platform/isolate_manager.dart';
import 'package:dart_framework/utils/string_util.dart';
import 'package:dart_framework/utils/system_util.dart';
import 'package:file_hasher/file_hasher.dart';
import 'package:oto/cli/commands/command_exe.dart';
import 'package:oto/cli/commands/scheduler/scheduler_interval.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/data/command_data.dart';
class IsolateScheduler extends IsolateBase {
late String schedulerPath;
late String logsPath;
late String alias;
late File file;
late File logFile;
int fileHash = 0;
bool logEnable = false;
bool notProgress = true;
bool cronClosed = true;
int? interval;
String? cronExpress;
late DataBuild build;
SchedulerInterval? schedulerInterval;
CommandExe commandExe = CommandExe();
Cron? cron;
int nextStartTime = 0;
IsolateScheduler(Map data) {
alias = data['alias'];
schedulerPath = data['root'];
logsPath = data['logs'];
file = File('$schedulerPath/$alias.yaml');
}
@override
void onReady(Object? initData) async {
while (true) {
await Future.delayed(Duration(seconds: 1));
if (file.existsSync()) {
var updated = false;
var hash = await FileHasher.hash(file);
if (hash != fileHash) {
updated = true;
fileHash = hash;
// Reload file contents on every change
var map = Application.getMapFromYamlA(await file.readAsString());
//============ Validate yaml format ============//
try {
build = DataBuild.fromJson(map!);
} on Exception {
onError('Failed to parse yaml file.');
return;
}
if (build.scheduler == null) {
onError('Scheduler entry does not exist within yaml.');
return;
}
var scheduler = build.scheduler!;
if (scheduler.cron == null && scheduler.interval == null) {
onError(
'A scheduler script must have either a cron entry or an interval entry.');
return;
}
if (scheduler.cron != null) {
try {
Schedule.parse(scheduler.cron!);
cronExpress = scheduler.cron!;
} on Error {
onError(
"Cron express '${build.scheduler!.cron}' is not a suitable format.");
return;
}
}
if (scheduler.interval != null) {
interval = scheduler.interval!;
}
logEnable =
scheduler.enableLog == null ? false : scheduler.enableLog!;
schedulerInterval?.activate = false;
}
//============ execute cron/interval ============//
if (cronExpress != null) {
//Use cron
if (cronClosed) {
cronClosed = false;
cron = Cron();
cron!.schedule(Schedule.parse(cronExpress!), () async {
if (notProgress) {
await execute(build);
cronClosed = true;
await cron!.close();
}
});
}
} else {
//Use interval
if (updated) {
schedulerInterval = SchedulerInterval(interval!, build, execute);
}
}
} else {
// Exit if the file no longer exists
onError("The file '${file.path}' does not exist.");
if (cron != null) await cron?.close();
return;
}
}
}
Future execute(DataBuild data) async {
send(IsoMessege('log', data: "Scheduler '$alias' has started."));
notProgress = false;
logFile = File(
'$logsPath/[$pid]_${alias}_${getDate()}_${getTime().replaceAll(':', '')}.txt');
if (data.scheduler!.enableLog ?? false) {
log('Start process on pid [$pid]');
}
await commandExe.executeScheduler(data, logEnable, logFile.path);
notProgress = true;
send(IsoMessege('log', data: "Scheduler '$alias' is done."));
return simpleFuture;
}
void log(String message) {
logFile.writeAsStringSync('[${getDates()}] $message\n',
mode: FileMode.append, flush: true);
}
void logError(Exception e, StackTrace stacktace) {
log('[${e.toString()}]\n${stacktace.toString()}');
}
void onErrorStacktrace(Exception e, StackTrace stacktrace) {
onError('$e\n$stacktrace');
}
void onError(String message) {
sendTerminated('error', message);
}
void sendTerminated(String type, String message) {
send(IsoMessege('terminated',
data: {'type': type, 'message': message, 'alias': alias}));
}
}