diff --git a/.vscode/launch.json b/.vscode/launch.json index 6a8f517..5f4f92d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -81,6 +81,13 @@ "program": "./bin/main.dart", "args": ["exe", "-f", "C:/Work/automation/iot/script/windows/startup.yaml"] }, + { + "name": "scheduler list", + "request": "launch", + "type": "dart", + "program": "./bin/main.dart", + "args": ["scheduler", "-l"] + }, { "name": "scheduler regist", "request": "launch", diff --git a/assets/scheduler.yaml b/assets/scheduler.yaml index 95b0097..2707bf0 100644 --- a/assets/scheduler.yaml +++ b/assets/scheduler.yaml @@ -2,7 +2,7 @@ scheduler: alias: test cron: '* * * * *' - enableLog: false # (Optional) default: false + enableLog: true # (Optional) default: false property: workspace: . diff --git a/assets/scheduler2.yaml b/assets/scheduler2.yaml new file mode 100644 index 0000000..cf3a2d4 --- /dev/null +++ b/assets/scheduler2.yaml @@ -0,0 +1,20 @@ +--- +scheduler: + alias: test2 + cron: '*/5 * * * *' + enableLog: true # (Optional) default: false + +property: + workspace: . + +pipeline: + id: main + workflow: + - exe: print + +commands: +### Print +- command: Print + id: print + param: + message: test \ No newline at end of file diff --git a/lib/cli/cli.dart b/lib/cli/cli.dart index 56e0ed2..739a3c1 100644 --- a/lib/cli/cli.dart +++ b/lib/cli/cli.dart @@ -115,7 +115,7 @@ class CLI { color: color, background: background, style: style); } - static final commandNameSpace = 18; + static final commandNameSpace = 21; static get serviceName => CLI.current.config.serviceName; static get executableFileName => CLI.current.config.executableFileName; diff --git a/lib/cli/commands/command_scheduler.dart b/lib/cli/commands/command_scheduler.dart index 85c7056..ea9aeb2 100644 --- a/lib/cli/commands/command_scheduler.dart +++ b/lib/cli/commands/command_scheduler.dart @@ -11,7 +11,8 @@ class CommandScheduler extends CommandBase { Map> get arguments => { '-l': ["Shows the currently operational schedulers."], '-r {file}': ["Register the defined file (yaml) with the scheduler."], - '-u {alias}': ["Unregister schedules registered with the scheduler based on alias."] + '-u {alias}': ["Unregister schedules registered with the scheduler based on alias."], + '-e {alias} {bool}': ["Enables/disables the registered scheduler."] }; @override String get name => 'scheduler'; @@ -25,24 +26,39 @@ class CommandScheduler extends CommandBase { await _scheduler.showList(); break; } else if (item.startsWith('-r')) { - var file = ''; if (parameters.length > i + 1) { - file = parameters[i + 1]; + var file = parameters[i + 1]; + await _scheduler.regist(file); } else { - file = item.replaceFirst('-r', '').trimLeft(); + printHelp(); } - await _scheduler.regist(file); exit(0); } else if (item.startsWith('-u')) { - var alias = ''; if (parameters.length > i + 1) { - alias = parameters[i + 1]; + var alias = parameters[i + 1]; + await _scheduler.unregist(alias); } else { - alias = item.replaceFirst('-u', '').trimLeft(); + printHelp(); } - await _scheduler.unregist(alias); break; - } else if (item.startsWith('-s')) { //hidden: start process + } else if (item.startsWith('-e')) { + if (parameters.length > i + 2) { + var alias = parameters[i + 1]; + // ignore: sdk_version_since + var enable = bool.tryParse(parameters[i + 2]); + if(enable == null) { + printHelp(); + } else { + await _scheduler.enableScheduler(alias, enable); + } + } else { + printHelp(); + } + break; + } + + //================== for Debug ==================// + else if (item.startsWith('-s')) { //hidden: start process await _scheduler.start(); break; } else if (item.startsWith('-t')) { //hidden: terminate process diff --git a/lib/cli/commands/scheduler/data/scheduler_data.dart b/lib/cli/commands/scheduler/data/scheduler_data.dart new file mode 100644 index 0000000..e2ed6f1 --- /dev/null +++ b/lib/cli/commands/scheduler/data/scheduler_data.dart @@ -0,0 +1,29 @@ + +import 'package:json_annotation/json_annotation.dart'; + +part 'scheduler_data.g.dart'; + +@JsonSerializable() +class SchedulerListData { + + late Map map = {}; + + SchedulerListData(); + + factory SchedulerListData.fromJson(Map json) => + _$SchedulerListDataFromJson(json); + Map toJson() => _$SchedulerListDataToJson(this); +} + +@JsonSerializable() +class SchedulerData { + + late String alias; + late bool enable = true; + + SchedulerData(); + + factory SchedulerData.fromJson(Map json) => + _$SchedulerDataFromJson(json); + Map toJson() => _$SchedulerDataToJson(this); +} diff --git a/lib/cli/commands/scheduler/data/scheduler_data.g.dart b/lib/cli/commands/scheduler/data/scheduler_data.g.dart new file mode 100644 index 0000000..c1d0e83 --- /dev/null +++ b/lib/cli/commands/scheduler/data/scheduler_data.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'scheduler_data.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SchedulerListData _$SchedulerListDataFromJson(Map json) => + SchedulerListData() + ..map = (json['map'] as Map).map( + (k, e) => + MapEntry(k, SchedulerData.fromJson(e as Map)), + ); + +Map _$SchedulerListDataToJson(SchedulerListData instance) => + { + 'map': instance.map, + }; + +SchedulerData _$SchedulerDataFromJson(Map json) => + SchedulerData() + ..alias = json['alias'] as String + ..enable = json['enable'] as bool; + +Map _$SchedulerDataToJson(SchedulerData instance) => + { + 'alias': instance.alias, + 'enable': instance.enable, + }; diff --git a/lib/cli/commands/scheduler/scheduler_isolate.dart b/lib/cli/commands/scheduler/scheduler_isolate.dart new file mode 100644 index 0000000..f0d5ced --- /dev/null +++ b/lib/cli/commands/scheduler/scheduler_isolate.dart @@ -0,0 +1,120 @@ + +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/oto/application.dart'; +import 'package:oto_cli/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; + 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.'); + 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."); + if(cron != null) await cron?.close(); + return; + } + } + } + + Future execute(DataBuild data) async { + notProgress = false; + if(data.scheduler!.enableLog ?? false) { + logFile = File('$logsPath/${alias}_${getDate()}_${getTime().replaceAll(':', '')}_[$pid].txt'); + log('Start process on pid [$pid]'); + } + await Future.delayed(Duration(seconds: 30)); //To-do: delete test, exe task + 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})); + } +} diff --git a/lib/cli/commands/scheduler/scheduler_manager.dart b/lib/cli/commands/scheduler/scheduler_manager.dart index 93b32e6..fc3a815 100644 --- a/lib/cli/commands/scheduler/scheduler_manager.dart +++ b/lib/cli/commands/scheduler/scheduler_manager.dart @@ -1,15 +1,15 @@ import 'dart:collection'; +import 'dart:convert'; 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/data/scheduler_data.dart'; +import 'package:oto_cli/cli/commands/scheduler/scheduler_isolate.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'; @@ -19,21 +19,43 @@ import 'package:path/path.dart' as path; class SchedulerManager { - static String get schedulerPath { + static String get _schedulerPath { var debugPath = isDebug ? '.debug' : ''; return '$dataPath$debugPath/scheduler'; } - static String get logsPath { - return '$schedulerPath/logs'; + static String get _logsPath { + return '$_schedulerPath/logs'; } - ProcessInfo? currentScheduler; + ProcessInfo? _currentScheduler; final Map _map = {}; + final logFile = File('$_logsPath/scheduler_${getDate()}_${getTime().replaceAll(':', '')}_[$pid].txt'); - HashSet get schedulers { + 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); + var dir = Directory(_schedulerPath); if(dir.existsSync()) { var fileList = dir.listSync(); for(var item in fileList) { @@ -47,10 +69,6 @@ class SchedulerManager { return hash; } - String get startupID { - return isDebug ? '$appIdendifier.debug' : appIdendifier; - } - SchedulerManager(); factory SchedulerManager.create() { @@ -84,45 +102,50 @@ class SchedulerManager { return throw Exception('Scheduler entry does not exist within yaml.'); } - var dirLogs = Directory(logsPath); + var dirLogs = Directory(_logsPath); var alias = build.scheduler!.alias; - var hash = schedulers; + 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(); + await _checkStartup(); + await _checkProcess(); + await _startProcess(); if(!dirLogs.existsSync()) { dirLogs.createSync(recursive: true); } - await file.copy('$schedulerPath/$alias.yaml'); + await file.copy('$_schedulerPath/$alias.yaml'); + + //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'); + if(_schedulers.contains(alias)) { + var file = File('$_schedulerPath/$alias.yaml'); file.deleteSync(); } else { return throw Exception("There is no scheduler called '$alias'."); } - await checkTerminate(); - } - - Future checkTerminate() async { - if(schedulers.isEmpty) { - await unregistStartup(); - await terminate(); + var local = _localData; + if(local.map.containsKey(alias)) { + local.map.remove(alias); + _localData = local; } - return simpleFuture; + + await _checkTerminate(); } Future showList() async { print(''); - var scheduleList = schedulers; + var scheduleList = _schedulers; if(scheduleList.isEmpty) { print(':::: There are no registered schedulers.'); print(''); @@ -130,53 +153,89 @@ class SchedulerManager { 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'); + 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(''); } - checkProcess(); + _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(); + + exit(0); + } else { + return throw Exception("There is no scheduler called '$alias'."); + } + } + Future terminate() async { - await checkProcess(); - if(currentScheduler != null) { - Process.killPid(currentScheduler!.pid); + await _checkProcess(); + if(_currentScheduler != null) { + Process.killPid(_currentScheduler!.pid); print('Scheduler operation stopped.'); } return simpleFuture; } - Future startProcess() async { + Future _checkTerminate() async { + if(_schedulers.isEmpty) { + await _unregistStartup(); + await terminate(); + } + return simpleFuture; + } + + Future _startProcess() async { //Launchd로 자동 실행이 되지 않은경우 직접실행 - if(currentScheduler == null) { + 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(); + await _checkProcess(); } return simpleFuture; } - Future checkProcess() async { + Future _checkProcess() async { //process check var processes = await findProcess('scheduler -s'); for(var process in processes) { 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}].'); + _currentScheduler = process; + print('Scheduler is running on pid [${_currentScheduler!.pid}].'); break; } } return simpleFuture; } - Future checkStartup() async { - if(!await OSStartup.isRegistedStartup(startupID)) { + Future _checkStartup() async { + if(!await OSStartup.isRegistedStartup(_startupID)) { List param = []; var exe = Platform.resolvedExecutable; if(isDebug) { @@ -193,168 +252,83 @@ class SchedulerManager { } param.add('scheduler'); param.add('-s'); - await OSStartup.registStartup(exe, startupID, arguments: param); + await OSStartup.registStartup(exe, _startupID, arguments: param); } return simpleFuture; } - Future unregistStartup() async { - if(await OSStartup.isRegistedStartup(startupID)) { - await OSStartup.unregistStartup(startupID); + Future _unregistStartup() async { + if(await OSStartup.isRegistedStartup(_startupID)) { + await OSStartup.unregistStartup(_startupID); } return simpleFuture; } //======================== other process ========================// - final logFile = File('$logsPath/scheduler_${getDate()}_${getTime().replaceAll(':', '')}_[$pid].txt'); Future start() async { - log('Scheduler is running on pid [$pid].'); + _log('Scheduler is running on pid [$pid].'); while(true) { //매초 마다 등록된 파일이 있나 체크 await Future.delayed(Duration(seconds: 1)); - var scheduleList = schedulers; - for(var alias in scheduleList) { - if(!_map.containsKey(alias)) { - log('Schduler started: $alias'); - var handler = IsolateManager.create(IsolateScheduler({'alias': alias, 'root': schedulerPath, 'logs': logsPath})); - //To-do: 종료시 사유(by user, by error)에 따라 json에 내용을 기입하여 재실행 안하도록 - handler.addListener('terminated', onTerminated ); + 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 started: $alias'); + var handler = IsolateManager.create(IsolateScheduler({'alias': alias, 'root': _schedulerPath, 'logs': _logsPath})); + handler.addListener('terminated', _onTerminated ); _map[alias] = handler; } + if(_map.containsKey(alias) && !enable) { + _removeHandler(alias); + } } } } - void onTerminated(Object? data) async { + 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; - var handler = _map.remove(alias); - handler!.exit(); + _removeHandler(alias); switch(type) { case 'error': - log('[Error] $message'); + _log('[Error] $message'); break; } - if(schedulers.isEmpty) log('Scheduler operation stopped.'); - await checkTerminate(); - } - - 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()}'); - } -} - -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."); - if(cron != null) await cron?.close(); - return; + 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(); } - Future execute(DataBuild data) async { - notProgress = false; - logFile = File('$logsPath/${alias}_${getDate()}_${getTime().replaceAll(':', '')}_[$pid].txt'); - log('Start process on pid [$pid]'); - await Future.delayed(Duration(minutes: 1, seconds: 30)); //To-do: delete test, exe task - notProgress = true; - return simpleFuture; - } - - void log(String message) { + 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 _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})); - } -} +} \ No newline at end of file diff --git a/lib/oto/data/command_data.dart b/lib/oto/data/command_data.dart index e1bfc42..dc57c20 100644 --- a/lib/oto/data/command_data.dart +++ b/lib/oto/data/command_data.dart @@ -1,7 +1,5 @@ // ignore_for_file: non_constant_identifier_names, avoid_init_to_null -import 'dart:ffi'; - import 'package:dart_framework/utils/slack/slack_sender.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:dart_framework/core/app_data_manager.dart';