This commit is contained in:
snltty
2025-03-31 15:10:10 +08:00
parent ae5a0f59bf
commit cdd2fca097
12 changed files with 153 additions and 124 deletions

View File

@@ -58,33 +58,33 @@
/// <summary>
/// 计划任务方法
/// </summary>
[Flags]
public enum PlanMethod : byte
{
/// <summary>
/// 手动
/// </summary>
Hand = 0,
None = 0,
/// <summary>
/// 启动后
/// </summary>
Setup = 1,
/// <summary>
/// 到点
/// </summary>
At = 100,
At = 2,
/// <summary>
/// 定时
/// </summary>
Timer = 101,
Timer = 4,
/// <summary>
/// 表达式
/// </summary>
Cron = 102,
Cron = 8,
/// <summary>
/// 触发
/// </summary>
Trigger = 103,
Trigger = 16,
}
}

View File

@@ -8,9 +8,6 @@ namespace linker.messenger.plan
{
public sealed class PlanTransfer
{
private string regex = @"([0-9]+|\*)-([0-9]+|\*)-([0-9]+|\*)\s+([0-9]+|\*):([0-9]+|\*):([0-9]+|\*)";
private string regexNumver = @"([0-9]+)-([0-9]+)-([0-9]+)\s+([0-9]+):([0-9]+):([0-9]+)";
private readonly ConcurrentDictionary<string, IPlanHandle> handles = new ConcurrentDictionary<string, IPlanHandle>();
private readonly ConcurrentDictionary<int, PlanExecCacheInfo> caches = new ConcurrentDictionary<int, PlanExecCacheInfo>();
@@ -30,16 +27,20 @@ namespace linker.messenger.plan
public IEnumerable<PlanInfo> Get(string category)
{
return planStore.Get(category);
try
{
return planStore.Get(category);
}
catch (Exception)
{
}
return [];
}
public bool Add(PlanInfo info)
{
bool result = planStore.Add(info);
caches.TryRemove(info.Id, out _);
PlanExecCacheInfo cache = new PlanExecCacheInfo { Plan = info };
cache.Active = UpdateNextTime(cache) && string.IsNullOrWhiteSpace(info.TriggerHandle);
caches.TryAdd(info.Id, cache);
caches.TryAdd(info.Id, new PlanExecCacheInfo(info));
return result;
}
public bool Remove(int id)
@@ -49,13 +50,10 @@ namespace linker.messenger.plan
return result;
}
public void Trigger(string category,string key,string handle)
public void Trigger(string category, string key, string handle)
{
PlanExecCacheInfo trigger = caches.Values.FirstOrDefault(c => c.Plan.Category == category && c.Plan.Key == key && c.Plan.TriggerHandle == handle && c.Plan.TriggerHandle != c.Plan.Handle && c.Plan.Method == PlanMethod.Trigger);
if (trigger != null)
{
trigger.Active = UpdateNextTime(trigger);
}
trigger?.UpdateNextTime(1);
}
private void PlanTask()
@@ -66,112 +64,154 @@ namespace linker.messenger.plan
}
private void Load()
{
foreach (PlanInfo info in planStore.Get())
try
{
try
{
PlanExecCacheInfo cache = new PlanExecCacheInfo { Plan = info };
cache.Active = (cache.Plan.Method < PlanMethod.At) || (UpdateNextTime(cache) && cache.Plan.Method != PlanMethod.Trigger);
caches.TryAdd(info.Id, cache);
}
catch (Exception)
foreach (PlanInfo info in planStore.Get())
{
caches.TryAdd(info.Id, new PlanExecCacheInfo(info));
}
}
catch (Exception)
{
}
}
private void RunSetup()
{
foreach (PlanExecCacheInfo item in caches.Values.Where(c => c.Plan.Method == PlanMethod.Setup && c.Plan.Disabled == false && c.Running == false))
try
{
foreach (PlanExecCacheInfo item in caches.Values.Where(c => c.Plan.Method == PlanMethod.Setup))
{
item.UpdateNextTime(1);
}
}
catch (Exception)
{
Run(item);
}
}
private void RunLoop()
{
foreach (PlanExecCacheInfo item in caches.Values.Where(c => c.NextTime <= DateTime.Now && c.Plan.Method >= PlanMethod.At))
foreach (PlanExecCacheInfo item in caches.Values.Where(c => c.Plan.Disabled == false && c.Running == false && c.Times > 0 && c.NextTime != null && c.NextTime <= DateTime.Now))
{
Run(item);
}
}
private void Run(PlanExecCacheInfo item)
{
if (item.Plan.Disabled || item.Active == false || item.Running || handles.TryGetValue(item.Plan.Category, out IPlanHandle handle) == false)
if (handles.TryGetValue(item.Plan.Category, out IPlanHandle handle))
{
return;
}
item.Running = true;
item.Active = (item.Plan.Method < PlanMethod.At) || (UpdateNextTime(item) && item.Plan.Method != PlanMethod.Trigger);
item.LastTime = DateTime.Now;
handle.HandleAsync(item.Plan.Handle, item.Plan.Key, item.Plan.Value).ContinueWith((result) =>
{
item.Running = false;
PlanExecCacheInfo trigger = caches.Values.FirstOrDefault(c => c.Plan.Category == item.Plan.Category && c.Plan.Key == item.Plan.Key && c.Plan.TriggerHandle == item.Plan.Handle && c.Plan.TriggerHandle != c.Plan.Handle && c.Plan.Method == PlanMethod.Trigger);
if (trigger != null)
item.StartRun();
item.UpdateNextTime(0);
handle.HandleAsync(item.Plan.Handle, item.Plan.Key, item.Plan.Value).ContinueWith((result) =>
{
trigger.Active = UpdateNextTime(trigger);
}
});
item.EndRun();
Trigger(item.Plan.Category, item.Plan.Key, item.Plan.Handle);
});
}
}
private bool UpdateNextTime(PlanExecCacheInfo cache)
}
public sealed class PlanExecCacheInfo
{
public static string regex = @"([0-9]+|\*)-([0-9]+|\*)-([0-9]+|\*)\s+([0-9]+|\*):([0-9]+|\*):([0-9]+|\*)";
public static string regexNumver = @"([0-9]+)-([0-9]+)-([0-9]+)\s+([0-9]+):([0-9]+):([0-9]+)";
public PlanInfo Plan { get; set; }
public DateTime LastTime { get; set; }
public DateTime? NextTime { get; set; } = DateTime.Now;
public bool Running { get; set; }
public ulong Times { get; set; }
public string Error { get; set; }
public PlanExecCacheInfo(PlanInfo plan)
{
Plan = plan;
TimesMethod();
NextTimeMethod();
}
public void StartRun()
{
Running = true;
LastTime = DateTime.Now;
Times--;
}
public void EndRun()
{
Running = false;
}
public void UpdateNextTime(ulong addTimes = 0)
{
Times += addTimes;
NextTimeMethod();
}
private void TimesMethod()
{
Times = Plan.Method switch
{
PlanMethod.None => 0,
PlanMethod.Setup => 0,
PlanMethod.At => ulong.MaxValue,
PlanMethod.Timer => ulong.MaxValue,
PlanMethod.Cron => ulong.MaxValue,
PlanMethod.Trigger => 0,
_ => 0,
};
}
private bool NextTimeMethod()
{
try
{
if (cache.Plan.Method == PlanMethod.At)
return Plan.Method switch
{
return NextTimeAt(cache);
}
else if (cache.Plan.Method == PlanMethod.Timer)
{
return NextTimeTimer(cache);
}
else if (cache.Plan.Method == PlanMethod.Cron)
{
return NextTimeCorn(cache);
}
else if (cache.Plan.Method == PlanMethod.Trigger)
{
return NextTimeTrigger(cache);
}
PlanMethod.None => false,
PlanMethod.Setup => true,
PlanMethod.At => NextTimeAt(),
PlanMethod.Timer => NextTimeTimer(),
PlanMethod.Cron => NextTimeCorn(),
PlanMethod.Trigger => NextTimeTrigger(),
_ => false,
};
}
catch (Exception ex)
{
cache.Error = ex.Message;
Error = ex.Message;
}
return false;
}
private bool NextTimeCorn(PlanExecCacheInfo cache)
private bool NextTimeCorn()
{
try
{
CronExpression cron = CronExpression.Parse(cache.Plan.Rule, CronFormat.IncludeSeconds);
CronExpression cron = CronExpression.Parse(Plan.Rule, CronFormat.IncludeSeconds);
DateTimeOffset? nextOccurrence = cron.GetNextOccurrence(DateTimeOffset.Now, TimeZoneInfo.Local);
if (nextOccurrence.HasValue)
{
cache.NextTime = nextOccurrence.Value.LocalDateTime;
NextTime = nextOccurrence.Value.LocalDateTime;
return true;
}
return true;
NextTime = null;
}
catch (Exception ex)
{
cache.Error = ex.Message;
Error = ex.Message;
}
return false;
}
private bool NextTimeAt(PlanExecCacheInfo cache)
private bool NextTimeAt()
{
if (Regex.IsMatch(cache.Plan.Rule, regex) == false)
if (Regex.IsMatch(Plan.Rule, regex) == false)
{
cache.Error = $"{cache.Plan.Rule} format error";
Error = $"{Plan.Rule} format error";
NextTime = null;
return false;
}
DateTime from = DateTime.Now;
GroupCollection groups = Regex.Match(cache.Plan.Rule, regex).Groups;
GroupCollection groups = Regex.Match(Plan.Rule, regex).Groups;
int year = groups[1].Value == "*" ? from.Year : int.Parse(groups[1].Value);
int month = groups[2].Value == "*" ? from.Month : int.Parse(groups[2].Value);
int day = groups[3].Value == "*" ? from.Day : int.Parse(groups[3].Value);
@@ -189,18 +229,19 @@ namespace linker.messenger.plan
else if (groups[2].Value == "*") next = next.AddMonths(1);
else if (groups[1].Value == "*") next = next.AddYears(1);
}
cache.NextTime = next;
NextTime = next;
return true;
}
private bool NextTimeTimer(PlanExecCacheInfo cache)
private bool NextTimeTimer()
{
if (Regex.IsMatch(cache.Plan.Rule, regexNumver) == false)
if (Regex.IsMatch(Plan.Rule, regexNumver) == false)
{
cache.Error = $"{cache.Plan.Rule} format error";
Error = $"{Plan.Rule} format error";
NextTime = null;
return false;
}
GroupCollection groups = Regex.Match(cache.Plan.Rule, regexNumver).Groups;
GroupCollection groups = Regex.Match(Plan.Rule, regexNumver).Groups;
int year = int.Parse(groups[1].Value);
int month = int.Parse(groups[2].Value);
int day = int.Parse(groups[3].Value);
@@ -208,18 +249,19 @@ namespace linker.messenger.plan
int minute = int.Parse(groups[5].Value);
int second = int.Parse(groups[6].Value);
cache.NextTime = DateTime.Now.AddYears(year).AddMonths(month).AddDays(day).AddHours(hour).AddMinutes(minute).AddSeconds(second);
NextTime = DateTime.Now.AddYears(year).AddMonths(month).AddDays(day).AddHours(hour).AddMinutes(minute).AddSeconds(second);
return true;
}
private bool NextTimeTrigger(PlanExecCacheInfo cache)
private bool NextTimeTrigger()
{
if (Regex.IsMatch(cache.Plan.Rule, regexNumver) == false)
if (Regex.IsMatch(Plan.Rule, regexNumver) == false)
{
cache.Error = $"{cache.Plan.Rule} format error";
Error = $"{Plan.Rule} format error";
NextTime = null;
return false;
}
GroupCollection groups = Regex.Match(cache.Plan.Rule, regexNumver).Groups;
GroupCollection groups = Regex.Match(Plan.Rule, regexNumver).Groups;
int year = int.Parse(groups[1].Value);
int month = int.Parse(groups[2].Value);
int day = int.Parse(groups[3].Value);
@@ -227,22 +269,9 @@ namespace linker.messenger.plan
int minute = int.Parse(groups[5].Value);
int second = int.Parse(groups[6].Value);
cache.NextTime = DateTime.Now.AddYears(year).AddMonths(month).AddDays(day).AddHours(hour).AddMinutes(minute).AddSeconds(second);
NextTime = DateTime.Now.AddYears(year).AddMonths(month).AddDays(day).AddHours(hour).AddMinutes(minute).AddSeconds(second);
return true;
}
}
public sealed class PlanExecCacheInfo
{
public PlanInfo Plan { get; set; }
public DateTime LastTime { get; set; }
public DateTime NextTime { get; set; }
public bool Running { get; set; }
public bool Active { get; set; }
public string Error { get; set; }
}
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<project ver="10" name="linker.tray.win" libEmbed="true" icon="..\linker\favicon.ico" ui="win" output="linker.tray.win.exe" CompanyName="snltty" FileDescription="linker.tray.win" LegalCopyright="Copyright (C) snltty 2024" ProductName="linker.tray.win" InternalName="linker.install.win" FileVersion="0.0.0.233" ProductVersion="0.0.0.233" publishDir="/dist/" dstrip="false" local="false" ignored="false">
<project ver="10" name="linker.tray.win" libEmbed="true" icon="..\linker\favicon.ico" ui="win" output="linker.tray.win.exe" CompanyName="snltty" FileDescription="linker.tray.win" LegalCopyright="Copyright (C) snltty 2024" ProductName="linker.tray.win" InternalName="linker.install.win" FileVersion="0.0.0.234" ProductVersion="0.0.0.234" publishDir="/dist/" dstrip="false" local="false" ignored="false">
<file name="main.aardio" path="main.aardio" comment="main.aardio"/>
<folder name="资源文件" path="res" embed="true" local="false" ignored="false">
<file name="favicon.ico" path="res\favicon.ico" comment="res\favicon.ico"/>

Binary file not shown.

View File

@@ -1 +1 @@
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><title>linker.web</title><link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/><script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script><script defer="defer" src="js/chunk-vendors.a0b07350.js"></script><script defer="defer" src="js/app.15b4bc70.js"></script><link href="css/chunk-vendors.d8267b33.css" rel="stylesheet"><link href="css/app.e96fbec3.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but linker.web doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><title>linker.web</title><link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/><script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script><script defer="defer" src="js/chunk-vendors.a0b07350.js"></script><script defer="defer" src="js/app.abf99840.js"></script><link href="css/chunk-vendors.d8267b33.css" rel="stylesheet"><link href="css/app.e96fbec3.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but linker.web doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

View File

@@ -11,11 +11,11 @@
<el-select v-model="state.ruleForm.Method" style="width:10rem" @change="handleChange">
<el-option v-for="(item,index) in plan.methods" :value="item.value" :label="item.label"></el-option>
</el-select>
<strong class="mgl-2" v-if="state.ruleForm.Method >= 100">
<strong class="mgl-2" v-if="state.ruleForm.Method >= 2">
{{ state.ruleForm.Rule }}
</strong>
</el-form-item>
<el-form-item label="在" prop="Rule" v-if="state.ruleForm.Method == 100">
<el-form-item label="在" prop="Rule" v-if="state.ruleForm.Method == 2">
<div class="w-100">
<el-select v-model="state.ruleAt.type" style="width:10rem" @change="handleChange">
<el-option :value="2" label="每月"></el-option>
@@ -32,7 +32,7 @@
<el-input @change="handleChange" :class="{'mgl-1':state.ruleAt.type < 5}" v-model="state.ruleAt.sec" style="width: 8rem"><template #append></template></el-input>
</div>
</el-form-item>
<el-form-item label="每" prop="Rule" v-if="state.ruleForm.Method == 101">
<el-form-item label="每" prop="Rule" v-if="state.ruleForm.Method == 4">
<div class="w-100">
<el-input @change="handleChange" v-model="state.ruleTimer.year" style="width: 8rem"><template #append></template></el-input>
<el-input @change="handleChange" class="mgl-1" v-model="state.ruleTimer.month" style="width: 8rem"><template #append></template></el-input>
@@ -44,7 +44,7 @@
<el-input @change="handleChange" class="mgl-1" v-model="state.ruleTimer.sec" style="width: 8rem"><template #append></template></el-input>
</div>
</el-form-item>
<el-form-item label="Cron" prop="Rule" v-if="state.ruleForm.Method == 102">
<el-form-item label="Cron" prop="Rule" v-if="state.ruleForm.Method == 8">
<div class="w-100">
<el-input @change="handleChange" v-model="state.ruleCron.sec" style="width: 8rem"><template #append></template></el-input>
<el-input @change="handleChange" class="mgl-1" v-model="state.ruleCron.min" style="width: 8rem"><template #append></template></el-input>
@@ -56,7 +56,7 @@
<el-input @change="handleChange" class="mgl-1" v-model="state.ruleCron.week" style="width: 8rem"><template #append></template></el-input>
</div>
</el-form-item>
<el-form-item label="在" prop="Rule" v-if="state.ruleForm.Method == 103">
<el-form-item label="在" prop="Rule" v-if="state.ruleForm.Method == 16">
<div class="w-100">
<el-select v-model="state.ruleForm.TriggerHandle" style="width:10rem" @change="handleChange">
<el-option v-for="(item,index) in plan.triggers" :value="item.value" :label="item.label"></el-option>
@@ -166,7 +166,7 @@ export default {
});
const decodeRuleJson = {
100:(rule)=>{
2:(rule)=>{
rule = rule || `*-*-* 0:0:0`;
if(regex.test(rule) == false){
return;
@@ -183,7 +183,7 @@ export default {
state.ruleAt.min = minute;
state.ruleAt.sec = second;
},
101:(rule)=>{
4:(rule)=>{
rule = rule || `0-0-0 0:0:30`;
if(regexNumber.test(rule) == false){
return;
@@ -196,7 +196,7 @@ export default {
state.ruleTimer.min = minute;
state.ruleTimer.sec = second;
},
102:(rule)=>{
8:(rule)=>{
rule = rule || `30 * * * * ?`;
if(regexCorn.test(rule) == false){
return;
@@ -209,7 +209,7 @@ export default {
state.ruleCron.month = month;
state.ruleCron.week = week;
},
103:(rule)=>{
16:(rule)=>{
rule = rule || `0-0-0 0:0:30`;
if(regexNumber.test(rule) == false){
return;
@@ -230,7 +230,7 @@ export default {
}
const buildRuleJson = {
100:()=>{
2:()=>{
switch (state.ruleAt.type) {
case 2:
return `*-*-${state.ruleAt.day} ${state.ruleAt.hour}:${state.ruleAt.min}:${state.ruleAt.sec}`;
@@ -247,9 +247,9 @@ export default {
}
return '';
},
101:()=>`${state.ruleTimer.year}-${state.ruleTimer.month}-${state.ruleTimer.day} ${state.ruleTimer.hour}:${state.ruleTimer.min}:${state.ruleTimer.sec}`,
102:()=>`${state.ruleCron.sec} ${state.ruleCron.min} ${state.ruleCron.hour} ${state.ruleCron.day} ${state.ruleCron.month} ${state.ruleCron.week}`,
103:()=>`${state.ruleTrigger.year}-${state.ruleTrigger.month}-${state.ruleTrigger.day} ${state.ruleTrigger.hour}:${state.ruleTrigger.min}:${state.ruleTrigger.sec}`,
4:()=>`${state.ruleTimer.year}-${state.ruleTimer.month}-${state.ruleTimer.day} ${state.ruleTimer.hour}:${state.ruleTimer.min}:${state.ruleTimer.sec}`,
8:()=>`${state.ruleCron.sec} ${state.ruleCron.min} ${state.ruleCron.hour} ${state.ruleCron.day} ${state.ruleCron.month} ${state.ruleCron.week}`,
16:()=>`${state.ruleTrigger.year}-${state.ruleTrigger.month}-${state.ruleTrigger.day} ${state.ruleTrigger.hour}:${state.ruleTrigger.min}:${state.ruleTrigger.sec}`,
}
const buildRule= ()=>{
if(state.ruleForm.Method in buildRuleJson){

View File

@@ -27,10 +27,10 @@ export default {
methods:[
{label:'手动',value:0},
{label:'启动后',value:1},
{label:'到点',value:100},
{label:'定时',value:101},
{label:'Cron',value:102},
{label:'触发',value:103},
{label:'到点',value:2},
{label:'定时',value:4},
{label:'Cron',value:8},
{label:'触发',value:16},
]
});
provide('plan',plan);

View File

@@ -17,7 +17,7 @@ export default {
const ruleTrans = {
0:()=>`手动`,
1:()=>`网络启动后`,
100:(item,rule)=>{
2:(item,rule)=>{
if(regex.test(rule) == false){
return rule;
}
@@ -28,7 +28,7 @@ export default {
if(month == '*') return `每月的${day}${hour}${minute}${second}`;
if(year == '*') return `每年的${month}${day}${hour}${minute}${second}`;
},
101:(item,rule)=>{
4:(item,rule)=>{
if(regexNumber.test(rule) == false){
return rule;
}
@@ -42,10 +42,10 @@ export default {
if(second != '0') arr.push(`${second}`);
return `${arr.join('')}`
},
102:(item,rule)=>{
8:(item,rule)=>{
return `Cron : ${rule}`;
},
103:(item,rule)=>{
16:(item,rule)=>{
if(regexNumber.test(rule) == false){
return rule;
}
@@ -80,7 +80,7 @@ export default {
Value:'',
Disabled:false,
TriggerHandle:'',
Method:100,
Method:2,
Rule:''
};
plan.value.triggers = JSON.parse(JSON.stringify(plan.value.handles.filter(c=>c.value != props.handle)));

View File

@@ -1,4 +1,4 @@
v1.7.2
2025-03-30 19:43:58
2025-03-31 15:10:10
1. 内网穿透的计划任务
2. 一些修复和优化