scheduler 작업중

This commit is contained in:
Toki 2024-12-25 13:20:41 +09:00
parent 1f2e6c5d0a
commit e0b377fc02
16 changed files with 458 additions and 45 deletions

21
.vscode/launch.json vendored
View file

@ -80,6 +80,27 @@
"type": "dart",
"program": "./bin/main.dart",
"args": ["exe", "-f", "C:/Work/automation/iot/script/windows/startup.yaml"]
},
{
"name": "scheduler regist",
"request": "launch",
"type": "dart",
"program": "./bin/main.dart",
"args": ["scheduler", "-r", "./assets/scheduler.yaml"]
},
{
"name": "scheduler unregist",
"request": "launch",
"type": "dart",
"program": "./bin/main.dart",
"args": ["scheduler", "-u", "test"]
},
{
"name": "scheduler background",
"request": "launch",
"type": "dart",
"program": "./bin/main.dart",
"args": ["scheduler", "-s"]
}
]
}

20
assets/scheduler.yaml Normal file
View file

@ -0,0 +1,20 @@
---
scheduler:
alias: test
cron: '*/5 * * * *'
enableLog: false # (Optional) default: false
property:
workspace: .
pipeline:
id: main
workflow:
- exe: print
commands:
### Print
- command: Print
id: print
param:
message: test

View file

@ -7,8 +7,9 @@ import 'package:oto_cli/cli/commands/command_exe.dart';
// import 'package:oto_cli/cli/commands/command_start.dart';
void main(List<String> arguments) async {
initialize();
CLI.initialize('OTO', arguments, [
await initialize('oto', 'com.toki-labs.oto');
CLI.initialize('oto', arguments, [
CommandTemplate(),
CommandExe(),
CommandScheduler()

View file

@ -39,6 +39,10 @@ class CLIConfig {
CLIConfig(this.serviceName, this.arguments, this.commands) {
var fileName = path.basename(Platform.resolvedExecutable);
fileName = fileName.replaceAll(path.extension(fileName), '');
if(fileName == 'dart') {
//debug
fileName = 'dart bin/main.dart';
}
executableFileName = fileName;
}
}

View file

@ -1,10 +1,8 @@
// ignore_for_file: avoid_init_to_null
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dart_framework/charset/euc_kr.dart';
import 'package:dart_framework/data/isolate_data.dart';
import 'package:dart_framework/platform/isolate_manager.dart';
import 'package:oto_cli/cli/cli.dart';
@ -33,12 +31,11 @@ class CommandExe extends CommandBase {
_handler = IsolateManager.create(IsolateExe(BuildType.jenkins));
await isComplete();
break;
}
if (item.startsWith('-t')) {
} else if (item.startsWith('-t')) {
_handler = IsolateManager.create(IsolateExe(BuildType.test));
await isComplete();
}
if (item.startsWith('-f')) {
break;
} else if (item.startsWith('-f')) {
var file = '';
if (parameters.length > i + 1) {
file = parameters[i + 1];
@ -47,6 +44,8 @@ class CommandExe extends CommandBase {
}
await startYaml(file);
break;
} else {
printHelp();
}
}
return simpleFuture;

View file

@ -35,9 +35,20 @@ class CommandManager {
}
if (!printHelp) {
await CLI.println('Execute command: $command$argStr',
if(parameters.isEmpty) {
commandItem.printHelp();
} else {
await CLI.println('Execute command: $command$argStr',
color: Color.green);
await commandItem.execute(parameters);
try {
await commandItem.execute(parameters);
} on Exception catch(e, stacktace) {
print('');
var message = e.toString().replaceAll('Exception:', '');
await CLI.println('${CLI.style('Error:', background: Color.red)} $message');
print('');
}
}
}
} else {
printHelp(append: [

View file

@ -1,33 +1,55 @@
import 'dart:async';
import 'dart:io';
import 'package:oto_cli/cli/commands/command_base.dart';
import 'package:dart_framework/utils/system_util.dart';
import 'package:oto_cli/cli/commands/scheduler/scheduler.dart';
import 'package:oto_cli/cli/commands/scheduler/scheduler_manager.dart';
class CommandScheduler extends CommandBase {
@override
Map<String, List<String>> get arguments => {
'-l': ["Shows the currently operational schedulers."],
'-k {alias}': ["Shut down the active scheduler."]
'-r {file}': ["Register the defined file (yaml) with the scheduler."],
'-u {alias}': ["Unregister schedules registered with the scheduler based on alias."]
};
@override
String get name => 'scheduler';
final Scheduler _scheduler = Scheduler();
final SchedulerManager _scheduler = SchedulerManager();
@override
Future execute(List<String> parameters) async {
for (var i = 0; i < parameters.length; ++i) {
var item = parameters[i];
if (item.startsWith('-l')) {
} else if (item.startsWith('-k')) {
_scheduler.showList();
break;
} else if (item.startsWith('-r')) {
var file = '';
if (parameters.length > i + 1) {
file = parameters[i + 1];
} else {
file = item.replaceFirst('-r', '').trimLeft();
}
await _scheduler.regist(file);
exit(0);
} else if (item.startsWith('-u')) {
var alias = '';
if (parameters.length > i + 1) {
alias = parameters[i + 1];
} else {
alias = item.replaceFirst('-f', '').trimLeft();
alias = item.replaceFirst('-u', '').trimLeft();
}
print('======================> $alias');
await _scheduler.unregist(alias);
break;
} else if (item.startsWith('-s')) { //hidden: start process
await _scheduler.start();
break;
} else if (item.startsWith('-t')) { //hidden: terminate process
await _scheduler.terminate();
break;
} else {
printHelp();
}
}
return simpleFuture;

View file

@ -1,20 +0,0 @@
import 'dart:io';
import 'package:oto_cli/cli/commands/scheduler/scheduler_linux.dart';
import 'package:oto_cli/cli/commands/scheduler/scheduler_osx.dart';
import 'package:oto_cli/cli/commands/scheduler/scheduler_windows.dart';
abstract class Scheduler {
factory Scheduler() {
if(Platform.isMacOS) {
return SchedulerOsx();
} else if(Platform.isWindows) {
return SchedulerWindows();
} else if(Platform.isLinux) {
return SchedulerLinux();
} else {
throw UnsupportedError('This platform not supported.');
}
}
}

View file

@ -1,6 +1,5 @@
import 'package:oto_cli/cli/commands/scheduler/scheduler.dart';
import 'package:oto_cli/cli/commands/scheduler/scheduler_manager.dart';
class SchedulerLinux implements Scheduler {
class SchedulerLinux extends SchedulerManager {
}

View file

@ -0,0 +1,325 @@
import 'dart:collection';
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/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:file_hasher/file_hasher.dart';
import 'package:oto_cli/cli/commands/scheduler/scheduler_linux.dart';
import 'package:oto_cli/cli/commands/scheduler/scheduler_osx.dart';
import 'package:oto_cli/cli/commands/scheduler/scheduler_windows.dart';
import 'package:oto_cli/oto/application.dart';
import 'package:oto_cli/oto/data/command_data.dart';
import 'package:path/path.dart' as path;
class SchedulerManager {
static String get schedulerPath {
var debugPath = isDebug ? '/debug' : '';
return '$dataPath$debugPath/scheduler';
}
static String get logsPath {
return '$schedulerPath/logs';
}
ProcessInfo? currentScheduler;
final Map<String, IsolateHandler> _map = {};
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;
}
SchedulerManager();
factory SchedulerManager.create() {
if(Platform.isMacOS) {
return SchedulerOsx();
} else if(Platform.isWindows) {
return SchedulerWindows();
} else if(Platform.isLinux) {
return SchedulerLinux();
} 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);
}
await file.copy('$schedulerPath/$alias.yaml');
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'.");
}
if(schedulers.isEmpty) {
await terminate();
await unregistStartup();
}
}
void showList() {
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]');
for(var item in schedulers) {
print('${' - true'.padRight(20)}${item.padRight(20)}$schedulerPath/$item.yaml');
}
print('');
}
}
Future terminate() async {
await checkProcess();
if(currentScheduler != null) {
Process.killPid(currentScheduler!.pid);
print('Scheduler operation stopped.');
}
return simpleFuture;
}
Future startProcess() async {
//Launchd로
if(currentScheduler == null) {
var script = isDebug ? ' $scriptPath' : '';
var shell = StringBuffer();
shell.writeln('nohup $executablePath$script scheduler -s &');
shell.writeln('disown');
ProcessExecutor.run(shell, workspace: Directory.systemTemp.path, printStdout: false);
await checkProcess();
}
return simpleFuture;
}
Future checkSchedules() async {
return simpleFuture;
}
Future checkProcess() async {
//process check
var processes = await findProcess('scheduler -s');
for(var process in processes) {
print(process.name);
var processName = process.name;
if(processName.contains('main.dart scheduler -s') || processName.contains('$appName scheduler -s')) {
currentScheduler = process;
print('Scheduler is running on pid ${currentScheduler!.pid}.');
break;
}
}
return simpleFuture;
}
Future checkStartup() async {
if(!await OSStartup.isRegistedStartup(appIdendifier)) {
List<String> param = [];
if(isDebug) {
param.add(scriptPath);
}
param.add('scheduler');
param.add('-s');
await OSStartup.registStartup(executablePath, appIdendifier, arguments: param);
}
return simpleFuture;
}
Future unregistStartup() async {
if(await OSStartup.isRegistedStartup(appIdendifier)) {
await OSStartup.unregistStartup(appIdendifier);
}
return simpleFuture;
}
//======================== other process ========================//
final logFile = File('$logsPath/scheduler.txt');
Future start() async {
while(true) {
//
await Future.delayed(Duration(seconds: 1));
var scheduleList = schedulers;
for(var alias in scheduleList) {
if(!_map.containsKey(alias)) {
await logFile.writeAsString('Schduler started: $alias\n', mode: FileMode.append);
var handler = IsolateManager.create(IsolateScheduler({'alias': alias, 'root': schedulerPath, 'logs': logsPath}));
//To-do: (by user, by error) json에
handler.addListener('terminated', onTerminated );
_map[alias] = handler;
}
}
}
}
void onTerminated(Object? data) {
var map = data as Map;
var alias = data['alias'] as String;
var type = data['type'] as String;
var message = data['message'] as String;
_map.remove(alias);
}
}
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;
late DataBuild build;
late String cronExpress;
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 hash = await FileHasher.hash(file);
if(hash != fileHash) {
fileHash = hash;
//
var map = Application.getMapFromYamlA(await file.readAsString());
//Validate yaml format
try {
build = DataBuild.fromJson(map!);
} on Exception catch(e, stacktace) {
onError('Failed to parse yaml file.');
logError(e, stacktace);
return;
}
if(build.scheduler == null) {
onError('Scheduler entry does not exist within yaml.');
return;
}
try {
Schedule.parse(build.scheduler!.cron);
cronExpress = build.scheduler!.cron;
} on Error catch(e, stacktace) {
onError("Cron express '${build.scheduler!.cron}' is not a suitable format.");
return;
}
}
if(cronClosed) {
cronClosed = false;
cron = Cron();
cron!.schedule(Schedule.parse(cronExpress), () async {
if(notProgress) {
await execute(build);
cronClosed = true;
await cron!.close();
}
});
}
} else {
//file이
onError("The file '${file.path}' does not exist.");
return;
}
}
}
Future execute(DataBuild data) async {
notProgress = false;
logFile = File('$logsPath/${alias}_${getDate()}_${getTime().replaceAll(':', '.')}.txt');
log('Execute task: ${DateTime.now().toString()}');
await Future.delayed(Duration(minutes: 1, seconds: 30));
notProgress = true;
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}));
}
}

View file

@ -1,6 +1,5 @@
import 'package:oto_cli/cli/commands/scheduler/scheduler_manager.dart';
import 'package:oto_cli/cli/commands/scheduler/scheduler.dart';
class SchedulerOsx implements Scheduler {
class SchedulerOsx extends SchedulerManager {
}

View file

@ -1,6 +1,6 @@
import 'package:oto_cli/cli/commands/scheduler/scheduler.dart';
import 'package:oto_cli/cli/commands/scheduler/scheduler_manager.dart';
class SchedulerWindows implements Scheduler {
class SchedulerWindows extends SchedulerManager {
}

View file

@ -119,7 +119,7 @@ class Application {
return simpleFuture;
}
Map<String, dynamic>? getMapFromYamlA(String yamlStr) {
static Map<String, dynamic>? getMapFromYamlA(String yamlStr) {
Map<String, dynamic>? map;
if (yamlStr.length > 5) {
YamlMap data = loadYaml(yamlStr);

View file

@ -14,6 +14,7 @@ part 'command_data.g.dart';
/// ***************************** Base Data ****************************///
@JsonSerializable()
class DataBuild {
late DataScheduler? scheduler;
late Map<String, dynamic>? property;
late DataPipeline? pipeline;
late List<DataCommand> commands;
@ -25,6 +26,19 @@ class DataBuild {
Map<String, dynamic> toJson() => _$DataBuildToJson(this);
}
@JsonSerializable()
class DataScheduler {
late String alias;
late String cron;
late bool? enableLog;
DataScheduler();
factory DataScheduler.fromJson(Map<String, dynamic> json) =>
_$DataSchedulerFromJson(json);
Map<String, dynamic> toJson() => _$DataSchedulerToJson(this);
}
@JsonSerializable()
class DataCommand {
DataCommand();

View file

@ -7,6 +7,9 @@ part of 'command_data.dart';
// **************************************************************************
DataBuild _$DataBuildFromJson(Map<String, dynamic> json) => DataBuild()
..scheduler = json['scheduler'] == null
? null
: DataScheduler.fromJson(json['scheduler'] as Map<String, dynamic>)
..property = json['property'] as Map<String, dynamic>?
..pipeline = json['pipeline'] == null
? null
@ -16,11 +19,25 @@ DataBuild _$DataBuildFromJson(Map<String, dynamic> json) => DataBuild()
.toList();
Map<String, dynamic> _$DataBuildToJson(DataBuild instance) => <String, dynamic>{
'scheduler': instance.scheduler,
'property': instance.property,
'pipeline': instance.pipeline,
'commands': instance.commands,
};
DataScheduler _$DataSchedulerFromJson(Map<String, dynamic> json) =>
DataScheduler()
..alias = json['alias'] as String
..cron = json['cron'] as String
..enableLog = json['enableLog'] as bool?;
Map<String, dynamic> _$DataSchedulerToJson(DataScheduler instance) =>
<String, dynamic>{
'alias': instance.alias,
'cron': instance.cron,
'enableLog': instance.enableLog,
};
DataCommand _$DataCommandFromJson(Map<String, dynamic> json) => DataCommand()
..command = $enumDecode(_$CommandTypeEnumMap, json['command'])
..id = json['id'] as String

View file

@ -19,6 +19,7 @@ dependencies:
ref: master
color: ^3.0.0
file_hasher: ^0.1.0+0
cron: ^0.6.1
dev_dependencies:
lints: ^5.0.0