[1.2.5] Better linux handling

This commit is contained in:
BlubbFish 2022-01-20 19:20:04 +01:00
parent 628db8db10
commit 1862aa7da2
8 changed files with 205 additions and 276 deletions

View File

@ -4,68 +4,39 @@ using System.Threading;
namespace BlubbFish.Utils.IoT.Bots { namespace BlubbFish.Utils.IoT.Bots {
public abstract class ABot { public abstract class ABot {
#if !NETCOREAPP
private Thread sig_thread;
#endif
private Boolean RunningProcess = true; private Boolean RunningProcess = true;
protected ProgramLogger logger = new ProgramLogger(); protected ProgramLogger logger = new ProgramLogger();
private void SetupShutdown(Object sender, ConsoleCancelEventArgs e) { private void ConsoleCancelEvent(Object sender, ConsoleCancelEventArgs e) {
e.Cancel = true; e.Cancel = true;
Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.SetupShutdown: Signalhandler Windows INT recieved."); Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.ConsoleCancelEvent()");
this.RunningProcess = false; this.RunningProcess = false;
} }
#if NETCOREAPP #if NETCOREAPP
private void Default_Unloading(AssemblyLoadContext obj) { private void Unloading(AssemblyLoadContext obj) => this.RunningProcess = false;
Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.SetupShutdown: Signalhandler Windows NETCORE recieved.");
this.RunningProcess = false; private void ProcessExit(Object sender, EventArgs e) => this.RunningProcess = false;
Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.WaitForShutdown: Shutdown.");
this.Dispose();
}
#endif #endif
protected void WaitForShutdown() { protected void WaitForShutdown() {
if(Type.GetType("Mono.Runtime") != null) { #if NETCOREAPP
#if !NETCOREAPP AssemblyLoadContext.Default.Unloading += this.Unloading;
this.sig_thread = new Thread(delegate () { AppDomain.CurrentDomain.ProcessExit += this.ProcessExit;
Mono.Unix.UnixSignal[] signals = new Mono.Unix.UnixSignal[] { Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.WaitForShutdown: Attach Unloading and ProcessExit.");
new Mono.Unix.UnixSignal(Mono.Unix.Native.Signum.SIGTERM), #endif
new Mono.Unix.UnixSignal(Mono.Unix.Native.Signum.SIGINT) Console.CancelKeyPress += new ConsoleCancelEventHandler(this.ConsoleCancelEvent);
}; Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.WaitForShutdown: Attach ConsoleCancelEvent.");
Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.WaitForShutdown: Signalhandler Mono attached."); while(this.RunningProcess) {
while(true) { Thread.Sleep(100);
Int32 i = Mono.Unix.UnixSignal.WaitAny(signals, -1); }
Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.WaitForShutdown: Signalhandler Mono INT recieved " + i + "."); Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.WaitForShutdown: Shutdown.");
this.RunningProcess = false;
break;
}
});
this.sig_thread.Start();
#endif
} else {
#if NETCOREAPP
AssemblyLoadContext.Default.Unloading += this.Default_Unloading;
Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.WaitForShutdown: Signalhandler Netcore attached.");
#endif
Console.CancelKeyPress += new ConsoleCancelEventHandler(this.SetupShutdown);
Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.WaitForShutdown: Signalhandler Windows attached.");
}
while(this.RunningProcess) {
Thread.Sleep(100);
}
Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.WaitForShutdown: Shutdown.");
} }
public virtual void Dispose() { public virtual void Dispose() {
#if !NETCOREAPP Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.Dispose: Shutdown.");
if(this.sig_thread != null && this.sig_thread.IsAlive) { this.RunningProcess = false;
this.sig_thread.Abort();
}
#endif
} }
} }
} }

View File

@ -5,17 +5,18 @@
<RootNamespace>BlubbFish.Utils.IoT.Bots</RootNamespace> <RootNamespace>BlubbFish.Utils.IoT.Bots</RootNamespace>
<AssemblyName>Bot-Utils</AssemblyName> <AssemblyName>Bot-Utils</AssemblyName>
<PackageId>Bots.IoT.Utils.BlubbFish</PackageId> <PackageId>Bots.IoT.Utils.BlubbFish</PackageId>
<Version>1.2.4</Version> <Version>1.2.5</Version>
<NeutralLanguage>de-DE</NeutralLanguage> <NeutralLanguage>de-DE</NeutralLanguage>
<Description>Bot-Utils are helpers for programming a bot</Description> <Description>Bot-Utils are helpers for programming a bot</Description>
<Authors>BlubbFish</Authors> <Authors>BlubbFish</Authors>
<Company>BlubbFish</Company> <Company>BlubbFish</Company>
<Copyright>Copyright © BlubbFish 2018 - 18.01.2022</Copyright> <Copyright>Copyright © BlubbFish 2018 - 20.01.2022</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>http://git.blubbfish.net/vs_utils/Bot-Utils</PackageProjectUrl> <PackageProjectUrl>http://git.blubbfish.net/vs_utils/Bot-Utils</PackageProjectUrl>
<RepositoryUrl>http://git.blubbfish.net/vs_utils/Bot-Utils.git</RepositoryUrl> <RepositoryUrl>http://git.blubbfish.net/vs_utils/Bot-Utils.git</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PackageReleaseNotes> <PackageReleaseNotes>
1.2.5 - 2022-01-20 - Better linux handling
1.2.4 - 2022-01-18 - Config enabled module loading 1.2.4 - 2022-01-18 - Config enabled module loading
1.2.3 - 2022-01-09 - Tiny Refactoring 1.2.3 - 2022-01-09 - Tiny Refactoring
1.2.2 - 2021-08-22 - Going to netcore 1.2.2 - 2021-08-22 - Going to netcore

View File

@ -11,6 +11,7 @@ namespace BlubbFish.Utils.IoT.Bots {
protected void ModulDispose() { protected void ModulDispose() {
foreach (KeyValuePair<String, AModul<T>> item in this.moduls) { foreach (KeyValuePair<String, AModul<T>> item in this.moduls) {
Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.ModulDispose: Entlade Modul: " + item.Key);
item.Value.Dispose(); item.Value.Dispose();
Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.ModulDispose: Modul entladen: " + item.Key); Console.WriteLine("BlubbFish.Utils.IoT.Bots.Bot.ModulDispose: Modul entladen: " + item.Key);
} }

View File

@ -1,170 +1,162 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using BlubbFish.Utils.IoT.Bots.Interfaces;
using BlubbFish.Utils.IoT.Bots.Interfaces;
namespace BlubbFish.Utils.IoT.Bots.Moduls {
public abstract class CronJob<T> : AModul<T>, IDisposable, IForceLoad { namespace BlubbFish.Utils.IoT.Bots.Moduls {
protected readonly List<Tuple<String, Action<Object>, Object>> internalCron = new List<Tuple<String, Action<Object>, Object>>(); public abstract class CronJob<T> : AModul<T>, IForceLoad {
protected Thread thread; protected readonly List<Tuple<String, Action<Object>, Object>> internalCron = new List<Tuple<String, Action<Object>, Object>>();
protected DateTime crontime; protected Thread thread;
protected Boolean threadRunning = false;
protected readonly Dictionary<String, String> cron_named = new Dictionary<String, String> { protected DateTime crontime;
{ "@yearly", "0 0 1 1 *" },
{ "@annually", "0 0 1 1 *" }, protected readonly Dictionary<String, String> cron_named = new Dictionary<String, String> {
{ "@monthly", "0 0 1 * *" }, { "@yearly", "0 0 1 1 *" },
{ "@weekly", "0 0 * * 0" }, { "@annually", "0 0 1 1 *" },
{ "@daily", "0 0 * * *" }, { "@monthly", "0 0 1 * *" },
{ "@hourly", "0 * * * *" } { "@weekly", "0 0 * * 0" },
}; { "@daily", "0 0 * * *" },
{ "@hourly", "0 * * * *" }
#region Constructor };
public CronJob(T lib, InIReader settings) : base(lib, settings) {
this.crontime = DateTime.Now; #region Constructor
this.thread = new Thread(this.Runner); public CronJob(T lib, InIReader settings) : base(lib, settings) {
this.thread.Start(); this.crontime = DateTime.Now;
} this.thread = new Thread(this.Runner);
#endregion this.threadRunning = true;
this.thread.Start();
#region Cronjobrunner }
protected void Runner() { #endregion
Thread.Sleep(DateTime.Now.AddMinutes(1).AddSeconds(DateTime.Now.Second * -1).AddMilliseconds(DateTime.Now.Millisecond * -1) - DateTime.Now);
while (true) { #region Cronjobrunner
if (this.crontime.Minute != DateTime.Now.Minute) { protected void Runner() {
this.crontime = DateTime.Now; DateTime nextminute = DateTime.Now.AddMinutes(1).AddSeconds(DateTime.Now.Second * -1).AddMilliseconds(DateTime.Now.Millisecond * -1);
if (this.config.Count != 0) { while(nextminute > DateTime.Now && this.threadRunning) {
foreach (KeyValuePair<String, Dictionary<String, String>> item in this.config) { Thread.Sleep(100);
if (item.Value.ContainsKey("cron") && item.Value.ContainsKey("set") && this.ParseCronString(item.Value["cron"])) { }
this.SetValues(item.Value["set"]); while (this.threadRunning) {
} if (this.crontime.Minute != DateTime.Now.Minute) {
} this.crontime = DateTime.Now;
} if (this.config.Count != 0) {
foreach (Tuple<String, Action<Object>, Object> item in this.internalCron) { foreach (KeyValuePair<String, Dictionary<String, String>> item in this.config) {
if (this.ParseCronString(item.Item1)) { if (item.Value.ContainsKey("cron") && item.Value.ContainsKey("set") && this.ParseCronString(item.Value["cron"])) {
item.Item2?.Invoke(item.Item3); this.SetValues(item.Value["set"]);
} }
} }
} }
Thread.Sleep(100); foreach (Tuple<String, Action<Object>, Object> item in this.internalCron) {
} if (this.ParseCronString(item.Item1)) {
} item.Item2?.Invoke(item.Item3);
}
protected abstract void SetValues(String value); }
#endregion }
Thread.Sleep(100);
#region CronFunctions }
protected Boolean ParseCronString(String cronstring) { }
cronstring = cronstring.Trim();
if (this.cron_named.ContainsKey(cronstring)) { protected abstract void SetValues(String value);
cronstring = this.cron_named[cronstring]; #endregion
}
String[] value = cronstring.Split(' '); #region CronFunctions
if (value.Length != 5) { protected Boolean ParseCronString(String cronstring) {
return false; cronstring = cronstring.Trim();
} if (this.cron_named.ContainsKey(cronstring)) {
if (!this.CheckDateStr(this.crontime.ToString("mm"), value[0], "0-59")) { cronstring = this.cron_named[cronstring];
return false; }
} String[] value = cronstring.Split(' ');
if (!this.CheckDateStr(this.crontime.ToString("HH"), value[1], "0-23")) { if (value.Length != 5) {
return false; return false;
} }
if (!this.CheckDateStr(this.crontime.ToString("MM"), value[3], "1-12")) { if (!this.CheckDateStr(this.crontime.ToString("mm"), value[0], "0-59")) {
return false; return false;
} }
if (value[2] != "*" && value[4] != "*") { if (!this.CheckDateStr(this.crontime.ToString("HH"), value[1], "0-23")) {
if (!this.CheckDateStr(this.crontime.ToString("dd"), value[2], "1-31") && !this.CheckDateStr(((Int32)this.crontime.DayOfWeek).ToString(), value[4], "0-7")) { return false;
return false; }
} if (!this.CheckDateStr(this.crontime.ToString("MM"), value[3], "1-12")) {
} else { return false;
if (!this.CheckDateStr(this.crontime.ToString("dd"), value[2], "1-31")) { }
return false; if (value[2] != "*" && value[4] != "*") {
} if (!this.CheckDateStr(this.crontime.ToString("dd"), value[2], "1-31") && !this.CheckDateStr(((Int32)this.crontime.DayOfWeek).ToString(), value[4], "0-7")) {
if (!this.CheckDateStr(((Int32)this.crontime.DayOfWeek).ToString(), value[4], "0-7")) { return false;
return false; }
} } else {
} if (!this.CheckDateStr(this.crontime.ToString("dd"), value[2], "1-31")) {
return true; return false;
} }
protected Boolean CheckDateStr(String date, String cron, String limit) { if (!this.CheckDateStr(((Int32)this.crontime.DayOfWeek).ToString(), value[4], "0-7")) {
cron = cron.ToLower(); return false;
for (Int32 i = 0; i <= 6; i++) { }
cron = cron.Replace(DateTime.Parse("2015-01-" + (4 + i) + "T00:00:00").ToString("ddd", CultureInfo.CreateSpecificCulture("en-US")), i.ToString()); }
cron = cron.Replace(DateTime.Parse("2015-01-" + (4 + i) + "T00:00:00").ToString("dddd", CultureInfo.CreateSpecificCulture("en-US")), i.ToString()); return true;
} }
for (Int32 i = 1; i <= 12; i++) { protected Boolean CheckDateStr(String date, String cron, String limit) {
cron = cron.Replace(DateTime.Parse("2015-" + i + "-01T00:00:00").ToString("MMM", CultureInfo.CreateSpecificCulture("en-US")), i.ToString()); cron = cron.ToLower();
cron = cron.Replace(DateTime.Parse("2015-" + i + "-01T00:00:00").ToString("MMMM", CultureInfo.CreateSpecificCulture("en-US")), i.ToString()); for (Int32 i = 0; i <= 6; i++) {
} cron = cron.Replace(DateTime.Parse("2015-01-" + (4 + i) + "T00:00:00").ToString("ddd", CultureInfo.CreateSpecificCulture("en-US")), i.ToString());
if (cron.Contains("*")) { cron = cron.Replace(DateTime.Parse("2015-01-" + (4 + i) + "T00:00:00").ToString("dddd", CultureInfo.CreateSpecificCulture("en-US")), i.ToString());
cron = cron.Replace("*", limit); }
} for (Int32 i = 1; i <= 12; i++) {
if (cron.Contains("-")) { cron = cron.Replace(DateTime.Parse("2015-" + i + "-01T00:00:00").ToString("MMM", CultureInfo.CreateSpecificCulture("en-US")), i.ToString());
MatchCollection m = new Regex("(\\d+)-(\\d+)").Matches(cron); cron = cron.Replace(DateTime.Parse("2015-" + i + "-01T00:00:00").ToString("MMMM", CultureInfo.CreateSpecificCulture("en-US")), i.ToString());
foreach (Match p in m) { }
List<String> s = new List<String>(); if (cron.Contains("*")) {
for (Int32 i = Math.Min(Int32.Parse(p.Groups[1].Value), Int32.Parse(p.Groups[2].Value)); i <= Math.Max(Int32.Parse(p.Groups[1].Value), Int32.Parse(p.Groups[2].Value)); i++) { cron = cron.Replace("*", limit);
s.Add(i.ToString()); }
} if (cron.Contains("-")) {
cron = cron.Replace(p.Groups[0].Value, String.Join(",", s)); MatchCollection m = new Regex("(\\d+)-(\\d+)").Matches(cron);
} foreach (Match p in m) {
} List<String> s = new List<String>();
Int32 match = 0; for (Int32 i = Math.Min(Int32.Parse(p.Groups[1].Value), Int32.Parse(p.Groups[2].Value)); i <= Math.Max(Int32.Parse(p.Groups[1].Value), Int32.Parse(p.Groups[2].Value)); i++) {
if (cron.Contains("/")) { s.Add(i.ToString());
Match m = new Regex("/(\\d+)").Match(cron); }
cron = cron.Replace(m.Groups[0].Value, ""); cron = cron.Replace(p.Groups[0].Value, String.Join(",", s));
match = Int32.Parse(m.Groups[1].Value); }
} }
Dictionary<Int32, String> ret = new Dictionary<Int32, String>(); Int32 match = 0;
if (!cron.Contains(",")) { if (cron.Contains("/")) {
ret.Add(Int32.Parse(cron), ""); Match m = new Regex("/(\\d+)").Match(cron);
} else { cron = cron.Replace(m.Groups[0].Value, "");
foreach (String item in cron.Split(',')) { match = Int32.Parse(m.Groups[1].Value);
if (!ret.ContainsKey(Int32.Parse(item))) { }
ret.Add(Int32.Parse(item), ""); Dictionary<Int32, String> ret = new Dictionary<Int32, String>();
} if (!cron.Contains(",")) {
} ret.Add(Int32.Parse(cron), "");
} } else {
if (match != 0) { foreach (String item in cron.Split(',')) {
Dictionary<Int32, String> r = new Dictionary<Int32, String>(); if (!ret.ContainsKey(Int32.Parse(item))) {
foreach (KeyValuePair<Int32, String> item in ret) { ret.Add(Int32.Parse(item), "");
if (item.Key % match == 0) { }
r.Add(item.Key, ""); }
} }
} if (match != 0) {
ret = r; Dictionary<Int32, String> r = new Dictionary<Int32, String>();
} foreach (KeyValuePair<Int32, String> item in ret) {
return ret.ContainsKey(Int32.Parse(date)); if (item.Key % match == 0) {
} r.Add(item.Key, "");
#endregion }
}
#region AModul ret = r;
public override void SetInterconnection(String cron, Action<Object> hook, Object data) => this.internalCron.Add(new Tuple<String, Action<Object>, Object>(cron, hook, data)); }
return ret.ContainsKey(Int32.Parse(date));
protected override void UpdateConfig() { } }
#endregion #endregion
#region IDisposable Support #region AModul
private Boolean disposedValue = false; public override void SetInterconnection(String cron, Action<Object> hook, Object data) => this.internalCron.Add(new Tuple<String, Action<Object>, Object>(cron, hook, data));
protected virtual void Dispose(Boolean disposing) { protected override void UpdateConfig() { }
if (!this.disposedValue) {
if (disposing) { public override void Dispose() {
if (this.thread != null) { this.threadRunning = false;
this.thread.Abort(); while(this.thread != null && this.thread.IsAlive) {
while (this.thread.ThreadState == ThreadState.Running) { Thread.Sleep(100); } Thread.Sleep(10);
} }
} this.thread = null;
this.thread = null; }
this.disposedValue = true; #endregion
} }
} }
public override void Dispose() {
this.Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}

View File

@ -7,7 +7,7 @@ using BlubbFish.Utils.IoT.Events;
using LitJson; using LitJson;
namespace BlubbFish.Utils.IoT.Bots.Moduls { namespace BlubbFish.Utils.IoT.Bots.Moduls {
public abstract class Mqtt<T> : AModul<T>, IDisposable { public abstract class Mqtt<T> : AModul<T> {
protected ABackend mqtt; protected ABackend mqtt;
protected Dictionary<String, AModul<T>> modules; protected Dictionary<String, AModul<T>> modules;
@ -34,6 +34,8 @@ namespace BlubbFish.Utils.IoT.Bots.Moduls {
public override void Interconnect(Dictionary<String, AModul<T>> moduls) => this.modules = moduls; public override void Interconnect(Dictionary<String, AModul<T>> moduls) => this.modules = moduls;
protected override void UpdateConfig() => this.Reconnect(); protected override void UpdateConfig() => this.Reconnect();
public override void Dispose() => this.Disconnect();
#endregion #endregion
protected Tuple<Boolean, MqttEvent> ChangeConfig(BackendEvent e, String topic) { protected Tuple<Boolean, MqttEvent> ChangeConfig(BackendEvent e, String topic) {
@ -74,23 +76,5 @@ namespace BlubbFish.Utils.IoT.Bots.Moduls {
} }
return new Tuple<Boolean, MqttEvent>(false, null); return new Tuple<Boolean, MqttEvent>(false, null);
} }
#region IDisposable Support
private Boolean disposedValue = false;
protected void Dispose(Boolean disposing) {
if (!this.disposedValue) {
if (disposing) {
this.Disconnect();
}
this.disposedValue = true;
}
}
public override void Dispose() {
this.Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
} }
} }

View File

@ -2,7 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace BlubbFish.Utils.IoT.Bots.Moduls { namespace BlubbFish.Utils.IoT.Bots.Moduls {
public abstract class Overtaker<T> : AModul<T>, IDisposable { public abstract class Overtaker<T> : AModul<T> {
protected readonly Dictionary<String, Dictionary<String, String>> events = new Dictionary<String, Dictionary<String, String>>(); protected readonly Dictionary<String, Dictionary<String, String>> events = new Dictionary<String, Dictionary<String, String>>();
#region Constructor #region Constructor
@ -62,23 +62,8 @@ namespace BlubbFish.Utils.IoT.Bots.Moduls {
#region AModul #region AModul
public override void Interconnect(Dictionary<String, AModul<T>> moduls) { } public override void Interconnect(Dictionary<String, AModul<T>> moduls) { }
protected override void UpdateConfig() => this.ParseIni(); protected override void UpdateConfig() => this.ParseIni();
#endregion public override void Dispose() {
#region IDisposable Support
private Boolean disposedValue = false;
protected virtual void Dispose(Boolean disposing) {
if (!this.disposedValue) {
if (disposing) {
}
this.disposedValue = true;
}
} }
#endregion
public override void Dispose() {
this.Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
} }
} }

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
using BlubbFish.Utils.IoT.Bots.Interfaces; using BlubbFish.Utils.IoT.Bots.Interfaces;
namespace BlubbFish.Utils.IoT.Bots.Moduls { namespace BlubbFish.Utils.IoT.Bots.Moduls {
public abstract class Statuspolling<T> : AModul<T>, IDisposable, IForceLoad { public abstract class Statuspolling<T> : AModul<T>, IForceLoad {
#region Constructor #region Constructor
public Statuspolling(T lib, InIReader settings) : base(lib, settings) { } public Statuspolling(T lib, InIReader settings) : base(lib, settings) { }
@ -31,22 +31,7 @@ namespace BlubbFish.Utils.IoT.Bots.Moduls {
} }
} }
protected override void UpdateConfig() { } protected override void UpdateConfig() { }
#endregion public override void Dispose() {
#region IDisposable Support
private Boolean disposedValue = false;
protected virtual void Dispose(Boolean disposing) {
if (!this.disposedValue) {
if (disposing) {
}
this.disposedValue = true;
}
}
public override void Dispose() {
this.Dispose(true);
GC.SuppressFinalize(this);
} }
#endregion #endregion
} }

View File

@ -1,5 +1,15 @@
# Changelog # Changelog
## 1.2.5 - 2022-01-20 - Better linux handling
### New Features
* Add ProcessExit Handler in ABot
* Add Output in ModulDispose
### Bugfixes
* Eleminate Hangs in Cronjob, when in startup phase it not blocks shutdown
### Changes
* Reweite ABot, remove Mono Code
* Codingstyle
## 1.2.4 - 2022-01-18 - Config enabled module loading ## 1.2.4 - 2022-01-18 - Config enabled module loading
### New Features ### New Features
* Modules can have an enabled=true|false in config, so that also enables or disables moduleloading. * Modules can have an enabled=true|false in config, so that also enables or disables moduleloading.