code refactoring

This commit is contained in:
Toki 2024-12-25 22:07:53 +09:00
parent 95c08139d7
commit 992689893c
10 changed files with 381 additions and 187 deletions

7
.vscode/launch.json vendored
View file

@ -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",

View file

@ -2,7 +2,7 @@
scheduler:
alias: test
cron: '* * * * *'
enableLog: false # (Optional) default: false
enableLog: true # (Optional) default: false
property:
workspace: .

20
assets/scheduler2.yaml Normal file
View file

@ -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

View file

@ -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;

View file

@ -11,7 +11,8 @@ class CommandScheduler extends CommandBase {
Map<String, List<String>> 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

View file

@ -0,0 +1,29 @@
import 'package:json_annotation/json_annotation.dart';
part 'scheduler_data.g.dart';
@JsonSerializable()
class SchedulerListData {
late Map<String, SchedulerData> map = <String, SchedulerData>{};
SchedulerListData();
factory SchedulerListData.fromJson(Map<String, dynamic> json) =>
_$SchedulerListDataFromJson(json);
Map<String, dynamic> toJson() => _$SchedulerListDataToJson(this);
}
@JsonSerializable()
class SchedulerData {
late String alias;
late bool enable = true;
SchedulerData();
factory SchedulerData.fromJson(Map<String, dynamic> json) =>
_$SchedulerDataFromJson(json);
Map<String, dynamic> toJson() => _$SchedulerDataToJson(this);
}

View file

@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'scheduler_data.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SchedulerListData _$SchedulerListDataFromJson(Map<String, dynamic> json) =>
SchedulerListData()
..map = (json['map'] as Map<String, dynamic>).map(
(k, e) =>
MapEntry(k, SchedulerData.fromJson(e as Map<String, dynamic>)),
);
Map<String, dynamic> _$SchedulerListDataToJson(SchedulerListData instance) =>
<String, dynamic>{
'map': instance.map,
};
SchedulerData _$SchedulerDataFromJson(Map<String, dynamic> json) =>
SchedulerData()
..alias = json['alias'] as String
..enable = json['enable'] as bool;
Map<String, dynamic> _$SchedulerDataToJson(SchedulerData instance) =>
<String, dynamic>{
'alias': instance.alias,
'enable': instance.enable,
};

View file

@ -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}));
}
}

View file

@ -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<String, IsolateHandler> _map = {};
final logFile = File('$_logsPath/scheduler_${getDate()}_${getTime().replaceAll(':', '')}_[$pid].txt');
HashSet<String> 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<String> get _schedulers {
HashSet<String> 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<String> 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}));
}
}
}

View file

@ -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';