Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 463898664b | |||
| 2c97b8c3e5 | |||
| 0cbffd8613 | |||
| 403e6df52b | |||
| 40a3f4f385 | |||
| 3dd5f03091 | |||
| 7003690cb3 | |||
| ad5031aed9 | |||
| b9c48d673b | |||
| f88cf90d98 | |||
| 61984f53ba | |||
| 0748f0ef88 | |||
| 9d19431748 | |||
| 8538c5e5de | |||
| 28bd7f360c | |||
| f3eccb2da9 | |||
| a1ebd35418 | |||
| a7bd8ad98a | |||
| 23d00eb9cd | |||
| a67359d4f6 | |||
| c178b66d54 | |||
| 1b9252cb67 | |||
| 46e4815947 | |||
| f9d785cc7c | |||
| e01a0a4689 | |||
| 78aa7c2efe | |||
| 75cc0324bb | |||
| 124c16bd01 | |||
| 9ffb3244f5 | |||
| b897c7fa53 |
+33
-25
@@ -14,13 +14,35 @@ variables:
|
||||
# helper x fix pacchetti nuget da repo locale nexus.steamware.net
|
||||
.nuget-fix: &nuget-fix
|
||||
- |
|
||||
$hasSource = C:\Tools\nuget.exe sources list | find "`"Steamware Nexus`"" /C
|
||||
if ($hasSource -eq 0) {
|
||||
C:\Tools\nuget.exe sources Add -Name "`"Steamware Nexus`"" -Source https://nexus.steamware.net/repository/nuget-group -username "`"nugetUser`"" -password "`"viaDante16`"" -StorePasswordInClearText
|
||||
} else {
|
||||
C:\Tools\nuget.exe sources Update -Name "`"Steamware Nexus`"" -Source https://nexus.steamware.net/repository/nuget-group -username "`"nugetUser`"" -password "`"viaDante16`"" -StorePasswordInClearText
|
||||
echo "esecuzione Nuget FIX steps"
|
||||
dotnet nuget list source
|
||||
$hasSource = dotnet nuget list source | Select-String -Pattern "Steamware Nexus Proxy"
|
||||
if (! [String]::IsNullOrWhiteSpace($hasSource)) {
|
||||
dotnet nuget remove source "`"Steamware Nexus Proxy`""
|
||||
}
|
||||
echo $hasSource
|
||||
$hasSource = dotnet nuget list source | Select-String -Pattern "Steamware Nexus"
|
||||
if (! [String]::IsNullOrWhiteSpace($hasSource)) {
|
||||
dotnet nuget remove source "`"Steamware Nexus`""
|
||||
}
|
||||
$hasSource = dotnet nuget list source | Select-String -Pattern "nexus-proxy-v3"
|
||||
if (! [String]::IsNullOrWhiteSpace($hasSource)) {
|
||||
dotnet nuget remove source nexus-proxy-v3
|
||||
}
|
||||
$hasSource = dotnet nuget list source | Select-String -Pattern "nexus-hosted"
|
||||
if (! [String]::IsNullOrWhiteSpace($hasSource)) {
|
||||
dotnet nuget remove source nexus-hosted
|
||||
}
|
||||
$hasSource = dotnet nuget list source | Select-String -Pattern "Microsoft Visual Studio Offline Packages"
|
||||
if (! [String]::IsNullOrWhiteSpace($hasSource)) {
|
||||
dotnet nuget remove source 'Microsoft Visual Studio Offline Packages'
|
||||
}
|
||||
echo "Situazione sorgenti post remove:"
|
||||
dotnet nuget list source
|
||||
dotnet nuget add source https://nexus.steamware.net/repository/nuget-proxy-v3/index.json -n nexus-proxy-v3 -u nugetUser -p $NEXUS_PASSWD --store-password-in-clear-text
|
||||
dotnet nuget add source https://nexus.steamware.net/repository/nuget-hosted/ -n nexus-hosted -u nugetUser -p $NEXUS_PASSWD --store-password-in-clear-text
|
||||
$hasSource = dotnet nuget list source
|
||||
echo "Situazione sorgenti FINALE:"
|
||||
dotnet nuget list source
|
||||
|
||||
# helper x fix version number
|
||||
.version-fix: &version-fix
|
||||
@@ -47,7 +69,7 @@ variables:
|
||||
# helper creazione hash files x Nexus
|
||||
.hashBuild: &hashBuild
|
||||
- |
|
||||
$Target = "$env:INST_NAME\bin\" + $env:APP_CONF + "\en-US\" + $env:INST_NAME + ".msi"
|
||||
$Target = "$env:INST_NAME\bin\x64\" + $env:APP_CONF + "\en-US\" + $env:INST_NAME + ".msi"
|
||||
$MD5 = Get-FileHash $Target -Algorithm MD5
|
||||
$SHA1 = Get-FileHash $Target -Algorithm SHA1
|
||||
New-Item $Target".md5"
|
||||
@@ -75,7 +97,7 @@ variables:
|
||||
$fileVers = $env:NUM_DEB
|
||||
}
|
||||
echo "Curr Version: $fileVers | $version"
|
||||
$File2Send = Get-ChildItem("$env:INST_NAME\bin\" + $env:APP_CONF + "\en-US\*")
|
||||
$File2Send = Get-ChildItem("$env:INST_NAME\bin\x64\" + $env:APP_CONF + "\en-US\*")
|
||||
ForEach ($File in $File2Send) {
|
||||
$FileName = Split-Path $File -leaf
|
||||
mCurl -v -u GitLab:$NEXUS_PASSWD --upload-file $File https://nexus.steamware.net/repository/SWS/$env:INST_NAME/$version/LAST/$FileName
|
||||
@@ -109,7 +131,6 @@ variables:
|
||||
|
||||
stages:
|
||||
- build
|
||||
# - staging
|
||||
- deploy
|
||||
# - pack
|
||||
|
||||
@@ -128,20 +149,7 @@ Runner:build:
|
||||
script:
|
||||
- dotnet build $env:APP_NAME/$env:APP_NAME.csproj
|
||||
|
||||
# #--------- Staging --------- #
|
||||
# UI:staging:
|
||||
# stage: staging
|
||||
# tags:
|
||||
# - win
|
||||
# variables:
|
||||
# APP_NAME: MP.MONO.UI
|
||||
# only:
|
||||
# - develop
|
||||
# needs: ["UI:build"]
|
||||
# script:
|
||||
# - dotnet publish -p:PublishProfile=IIS01.pubxml -p:RunCodeAnalysis=false -p:Configuration=Release -p:username=jenkins -p:Password=viadante16 -p:AllowUntrustedCertificate=true $env:APP_NAME/$env:APP_NAME.csproj
|
||||
|
||||
# #--------- Deploy --------- #
|
||||
#--------- Deploy --------- #
|
||||
Runner:deploy-dev:
|
||||
stage: deploy
|
||||
tags:
|
||||
@@ -162,7 +170,7 @@ Runner:deploy-dev:
|
||||
# pubblicazione come singleApp
|
||||
- dotnet publish -p:PublishProfile=SingleAppDebug.pubxml -p:RunCodeAnalysis=false -p:Configuration=$env:APP_CONF $env:APP_NAME/$env:APP_NAME.csproj
|
||||
# creazione installer WIX
|
||||
- dotnet build $env:INST_NAME/$env:INST_NAME.wixproj -p:Configuration=$env:APP_CONF
|
||||
- dotnet build $env:INST_NAME/$env:INST_NAME.wixproj -p:Configuration=$env:APP_CONF -p:Platform=x64
|
||||
#- *artifactZipper
|
||||
- *hashBuild
|
||||
- *nexusUpload
|
||||
@@ -187,7 +195,7 @@ Runner:deploy:
|
||||
# pubblicazione come singleApp
|
||||
- dotnet publish -p:PublishProfile=SingleApp.pubxml -p:RunCodeAnalysis=false -p:Configuration=$env:APP_CONF $env:APP_NAME/$env:APP_NAME.csproj
|
||||
# creazione installer WIX
|
||||
- dotnet build $env:INST_NAME/$env:INST_NAME.wixproj -p:Configuration=$env:APP_CONF
|
||||
- dotnet build $env:INST_NAME/$env:INST_NAME.wixproj -p:Configuration=$env:APP_CONF -p:Platform=x64
|
||||
#- *artifactZipper
|
||||
- *hashBuild
|
||||
- *nexusUpload
|
||||
|
||||
@@ -21,6 +21,11 @@ namespace Maat.Core.CONF
|
||||
/// </summary>
|
||||
public MsSqlConf? MsSqlJobs { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Area configurazione task RestApi (opzionale)
|
||||
/// </summary>
|
||||
public RestApiConf? RestApiJobs { get; set; } = null;
|
||||
|
||||
#endregion Public Properties
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static Maat.Core.Enums;
|
||||
|
||||
namespace Maat.Core.CONF
|
||||
{
|
||||
/// <summary>
|
||||
/// Classe gestione configurazione parametri di base x gestione task da API REST
|
||||
/// </summary>
|
||||
public class RestApiConf
|
||||
{
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// Elenco dei server configurati x chiamate REST Api, ogni server ha la stessa base ApiUrl
|
||||
/// </summary>
|
||||
public List<SrvConf> ServerList { get; set; } = new List<SrvConf>();
|
||||
|
||||
#endregion Public Properties
|
||||
|
||||
#region Public Classes
|
||||
|
||||
public class SrvConf
|
||||
{
|
||||
#region Public Properties
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Dizionario dei metodi configurati da chiamare
|
||||
/// </summary>
|
||||
public List<CallConfig> CallList { get; set; } = new List<CallConfig>();
|
||||
|
||||
/// <summary>
|
||||
/// Path di base ApiUrl del servizio
|
||||
/// </summary>
|
||||
public string ApiUrl { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// nome univoco del servizio
|
||||
/// </summary>
|
||||
public string SrvName { get; set; } = "";
|
||||
|
||||
#endregion Public Properties
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configurazione singola chiamata
|
||||
/// </summary>
|
||||
public class CallConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Id univoco della chiamata (contatore univoco in ambito SrvName/ApiUrl..)
|
||||
/// </summary>
|
||||
public int CallId { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Modalità chiamata tra quelle disponibili
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public RestCallType CallMode { get; set; } = RestCallType.Get;
|
||||
|
||||
/// <summary>
|
||||
/// Risorsa da chiamare
|
||||
/// </summary>
|
||||
public string Resource { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Payload associato alla risorsa da chiamare
|
||||
/// </summary>
|
||||
public string Payload { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Tempo di attesa tra le chiamate... default 60 minuti
|
||||
/// </summary>
|
||||
public double WaitMinutes { get; set; } = 60;
|
||||
|
||||
#endregion Public Classes
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -10,7 +10,7 @@ namespace Maat.Core
|
||||
{
|
||||
|
||||
// dati conf REDIS Cache
|
||||
public static readonly string RedisBaseKey = "EgalWare-Maat";
|
||||
public static readonly string RedisBaseKey = "Maat";
|
||||
public static readonly string BASE_PATH = Directory.GetCurrentDirectory();
|
||||
|
||||
|
||||
|
||||
+40
-2
@@ -26,7 +26,17 @@ namespace Maat.Core
|
||||
/// <summary>
|
||||
/// Chiamata a SQL Stored Procedure
|
||||
/// </summary>
|
||||
SqlStored
|
||||
SqlStored,
|
||||
|
||||
/// <summary>
|
||||
/// Chiamata REST tipo Get
|
||||
/// </summary>
|
||||
RestCallGet,
|
||||
|
||||
///// <summary>
|
||||
///// Chiamata REST tipo Post
|
||||
///// </summary>
|
||||
//RestCallPost
|
||||
}
|
||||
|
||||
//[JsonConverter(typeof(StringEnumConverter))]
|
||||
@@ -74,7 +84,7 @@ namespace Maat.Core
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tipologia di chiamata permessa
|
||||
/// Tipologia di chiamata DB permessa
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum TaskCallType
|
||||
@@ -89,5 +99,33 @@ namespace Maat.Core
|
||||
/// </summary>
|
||||
TaskList
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tipologia di chiamata REST permessa
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum RestCallType
|
||||
{
|
||||
/// <summary>
|
||||
/// Metodo Delete
|
||||
/// </summary>
|
||||
Delete,
|
||||
|
||||
/// <summary>
|
||||
/// Metodo Get
|
||||
/// </summary>
|
||||
Get,
|
||||
|
||||
/// <summary>
|
||||
/// Metodo Post
|
||||
/// </summary>
|
||||
Post,
|
||||
|
||||
/// <summary>
|
||||
/// Metodo Put
|
||||
/// </summary>
|
||||
Put
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.2.8" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.7.33" />
|
||||
<PackageReference Include="NLog" Version="5.3.4" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Maat.Core
|
||||
{
|
||||
public class RestWaitConf
|
||||
{
|
||||
public int CallId { get; set; } = 0;
|
||||
public double WaitTime { get; set; } = 0;
|
||||
public DateTime NextExe { get; set; } = DateTime.Now;
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,12 @@ namespace Maat.Data.Controllers
|
||||
/// </summary>
|
||||
/// <param name="threadName">nome del thread di esecuzione</param>
|
||||
/// <param name="cString">stringa di connessione da impiegare</param>
|
||||
public MsSqlController(string threadName, string cString)
|
||||
/// <param name="cmdExecTOut">timeout esecuzione comandi (minuti)</param>
|
||||
public MsSqlController(string threadName, string cString, int cmdExecTOut)
|
||||
{
|
||||
tName = threadName;
|
||||
connString = cString;
|
||||
cmdTimeout = cmdExecTOut;
|
||||
Log.Info($"{tName} | Avviata classe MsSqlController");
|
||||
}
|
||||
|
||||
@@ -34,51 +36,61 @@ namespace Maat.Data.Controllers
|
||||
public DateTime CalcNextExe(TaskListModel taskRec)
|
||||
{
|
||||
DateTime dtNext = DateTime.Today;
|
||||
DateTime adesso = DateTime.Now;
|
||||
int maxIter = 1000;
|
||||
try
|
||||
{
|
||||
// calcolo next exec da tipo...
|
||||
switch (taskRec.Freq)
|
||||
// prendo come partenza la DATA di fine a cui aggiungo l'ora/minuti schedulata precedentemente
|
||||
TimeSpan oraSched = taskRec.DtNextExec.Subtract(taskRec.DtNextExec.Date);
|
||||
DateTime baseDT = taskRec.DtLastExec.Date.Add(oraSched);
|
||||
// calcolo next exec da tipo... con ciclo fino a quando supero dataora attuale...
|
||||
while (dtNext < adesso && maxIter > 0)
|
||||
{
|
||||
case TaskFreqType.ND:
|
||||
dtNext = taskRec.DtLastExec.AddDays(taskRec.Cad);
|
||||
break;
|
||||
switch (taskRec.Freq)
|
||||
{
|
||||
case TaskFreqType.ND:
|
||||
dtNext = baseDT.AddDays(taskRec.Cad);
|
||||
break;
|
||||
|
||||
case TaskFreqType.Sec:
|
||||
dtNext = taskRec.DtLastExec.AddSeconds(taskRec.Cad);
|
||||
break;
|
||||
case TaskFreqType.Sec:
|
||||
dtNext = baseDT.AddSeconds(taskRec.Cad);
|
||||
break;
|
||||
|
||||
case TaskFreqType.Min:
|
||||
dtNext = taskRec.DtLastExec.AddMinutes(taskRec.Cad);
|
||||
break;
|
||||
case TaskFreqType.Min:
|
||||
dtNext = baseDT.AddMinutes(taskRec.Cad);
|
||||
break;
|
||||
|
||||
case TaskFreqType.Hour:
|
||||
dtNext = taskRec.DtLastExec.AddHours(taskRec.Cad);
|
||||
break;
|
||||
case TaskFreqType.Hour:
|
||||
dtNext = baseDT.AddHours(taskRec.Cad);
|
||||
break;
|
||||
|
||||
case TaskFreqType.Day:
|
||||
dtNext = taskRec.DtLastExec.AddDays(taskRec.Cad);
|
||||
break;
|
||||
case TaskFreqType.Day:
|
||||
dtNext = baseDT.AddDays(taskRec.Cad);
|
||||
break;
|
||||
|
||||
case TaskFreqType.Week:
|
||||
dtNext = taskRec.DtLastExec.AddDays(7 * taskRec.Cad);
|
||||
break;
|
||||
case TaskFreqType.Week:
|
||||
dtNext = baseDT.AddDays(7 * taskRec.Cad);
|
||||
break;
|
||||
|
||||
case TaskFreqType.Month:
|
||||
dtNext = taskRec.DtLastExec.AddMonths(taskRec.Cad);
|
||||
break;
|
||||
case TaskFreqType.Month:
|
||||
dtNext = baseDT.AddMonths(taskRec.Cad);
|
||||
break;
|
||||
|
||||
case TaskFreqType.Year:
|
||||
dtNext = taskRec.DtLastExec.AddYears(taskRec.Cad);
|
||||
break;
|
||||
case TaskFreqType.Year:
|
||||
dtNext = baseDT.AddYears(taskRec.Cad);
|
||||
break;
|
||||
|
||||
default:
|
||||
dtNext = taskRec.DtLastExec.AddDays(taskRec.Cad);
|
||||
break;
|
||||
default:
|
||||
dtNext = baseDT.AddDays(taskRec.Cad);
|
||||
break;
|
||||
}
|
||||
baseDT = dtNext;
|
||||
maxIter--;
|
||||
}
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
Log.Error($"{tName} | Eccezione in CalcNextExe{Environment.NewLine}{exc}");
|
||||
Log.Error($"Eccezione in CalcNextExe{Environment.NewLine}{exc}");
|
||||
}
|
||||
return dtNext;
|
||||
}
|
||||
@@ -91,14 +103,16 @@ namespace Maat.Data.Controllers
|
||||
/// Chiamata esecuzione di un singolo task programmato
|
||||
/// </summary>
|
||||
/// <param name="TaskId"></param>
|
||||
/// <param name="SchedNext">Se true rischedula successiva chiamata</param>
|
||||
/// <returns></returns>
|
||||
public TaskResultModel ExecuteTask(int TaskId)
|
||||
public TaskResultModel ExecuteSqlTask(int TaskId, bool SchedNext)
|
||||
{
|
||||
TaskResultModel callRes = new TaskResultModel();
|
||||
using (var dbCtx = new TaskRunContext(connString))
|
||||
{
|
||||
// imposto timeout a 5 min
|
||||
dbCtx.Database.SetCommandTimeout(TimeSpan.FromMinutes(5));
|
||||
dbCtx.Database.SetCommandTimeout(TimeSpan.FromMinutes(cmdTimeout));
|
||||
string sqlCall = "";
|
||||
try
|
||||
{
|
||||
DateTime dtStart = DateTime.Now;
|
||||
@@ -112,13 +126,16 @@ namespace Maat.Data.Controllers
|
||||
// recupero comando
|
||||
string sqlCommand = currRec.Command;
|
||||
string rawParams = currRec.Args;
|
||||
// salvo la call x eventuale log errore
|
||||
sqlCall = $"EXEC {sqlCommand} {rawParams}";
|
||||
|
||||
var rawData = dbCtx
|
||||
.DbSetTaskResult
|
||||
.FromSqlRaw($"EXEC {sqlCommand} {rawParams}")
|
||||
.AsNoTracking()
|
||||
.AsEnumerable()
|
||||
.FirstOrDefault();
|
||||
callRes = rawData ?? new TaskResultModel();
|
||||
.DbSetTaskResult
|
||||
.FromSqlRaw($"EXEC {sqlCommand} {rawParams}")
|
||||
.ToList();
|
||||
|
||||
callRes = rawData.FirstOrDefault() ?? new TaskResultModel();
|
||||
|
||||
DateTime dtEnd = DateTime.Now;
|
||||
|
||||
// preparo record esecuzione...
|
||||
@@ -139,8 +156,12 @@ namespace Maat.Data.Controllers
|
||||
currRec.LastResult = resRec.Result;
|
||||
currRec.LastIsError = resRec.IsError;
|
||||
currRec.LastDuration = dtEnd.Subtract(dtStart).TotalSeconds;
|
||||
// calcolo prossima esecuzione...
|
||||
currRec.DtNextExec = CalcNextExe(currRec);
|
||||
// solo se richiesto rischedulazione ricalcola chiamata
|
||||
if (SchedNext)
|
||||
{
|
||||
// calcolo prossima esecuzione...
|
||||
currRec.DtNextExec = CalcNextExe(currRec);
|
||||
}
|
||||
// segno modificato
|
||||
dbCtx.Entry(currRec).State = EntityState.Modified;
|
||||
|
||||
@@ -150,7 +171,62 @@ namespace Maat.Data.Controllers
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
Log.Error($"{tName} | Eccezione in ExecuteTask{Environment.NewLine}{exc}");
|
||||
string excMsg = $"{tName} | Eccezione in ExecuteSqlTask{Environment.NewLine}Call eseguita:{Environment.NewLine}{sqlCall}{Environment.NewLine}------- Stack Eccezione -------{Environment.NewLine}{exc}";
|
||||
callRes.ExecResult = -1;
|
||||
callRes.TextResult = excMsg;
|
||||
Log.Error(excMsg);
|
||||
}
|
||||
}
|
||||
return callRes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Esegue registrazione di un Task generico (NON SQL)
|
||||
/// </summary>
|
||||
/// <param name="TaskId"></param>
|
||||
/// <param name="SchedNext">Se true rischedula successiva chiamata</param>
|
||||
/// <param name="ResRec">Record esecuzione task esterno</param>
|
||||
/// <returns></returns>
|
||||
public TaskResultModel TaskExecSaveExecuted(int TaskId, bool SchedNext, TaskExecModel ResRec)
|
||||
{
|
||||
TaskResultModel callRes = new TaskResultModel();
|
||||
using (var dbCtx = new TaskRunContext(connString))
|
||||
{
|
||||
try
|
||||
{
|
||||
// recupero i dati da richiamare...
|
||||
var currRec = dbCtx
|
||||
.DbSetTaskList
|
||||
.Where(x => x.TaskId == TaskId)
|
||||
.FirstOrDefault();
|
||||
if (currRec != null)
|
||||
{
|
||||
// registro task ricevuto
|
||||
dbCtx
|
||||
.DbSetTaskExe
|
||||
.Add(ResRec);
|
||||
|
||||
// aggiorno record chiamata...
|
||||
currRec.DtLastExec = ResRec.DtStart;
|
||||
currRec.LastResult = ResRec.Result;
|
||||
currRec.LastIsError = ResRec.IsError;
|
||||
currRec.LastDuration = ResRec.DtEnd.Subtract(ResRec.DtStart).TotalSeconds;
|
||||
// solo se richiesto rischedulazione ricalcola chiamata
|
||||
if (SchedNext)
|
||||
{
|
||||
// calcolo prossima esecuzione...
|
||||
currRec.DtNextExec = CalcNextExe(currRec);
|
||||
}
|
||||
// segno modificato
|
||||
dbCtx.Entry(currRec).State = EntityState.Modified;
|
||||
|
||||
// salvo modifiche!
|
||||
dbCtx.SaveChanges();
|
||||
}
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
Log.Error($"Eccezione in TaskExecSaveExecuted{Environment.NewLine}{exc}");
|
||||
}
|
||||
}
|
||||
return callRes;
|
||||
@@ -182,6 +258,7 @@ namespace Maat.Data.Controllers
|
||||
private static Logger Log = LogManager.GetCurrentClassLogger();
|
||||
private string connString = "";
|
||||
private string tName = "";
|
||||
private int cmdTimeout = 0;
|
||||
|
||||
#endregion Private Fields
|
||||
}
|
||||
|
||||
@@ -20,11 +20,21 @@ namespace Maat.Data.DbModels
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int TaskId { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gruppo task (per gestione precedenze e semaforo exec)
|
||||
/// </summary>
|
||||
public int Group { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Ordinale x esecuzione
|
||||
/// </summary>
|
||||
public int Ordinal { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Stato Task abilitato / disabilitato
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Nome Task
|
||||
/// </summary>
|
||||
@@ -64,8 +74,9 @@ namespace Maat.Data.DbModels
|
||||
/// DataOra ultima esecuzione
|
||||
/// </summary>
|
||||
public DateTime DtLastExec { get; set; } = DateTime.Today.AddYears(-10);
|
||||
|
||||
/// <summary>
|
||||
/// DataOra ultima esecuzione
|
||||
/// DataOra prossima esecuzione (prevista)
|
||||
/// </summary>
|
||||
public DateTime DtNextExec { get; set; } = DateTime.Today.AddYears(-9);
|
||||
|
||||
|
||||
+11
-10
@@ -7,23 +7,24 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.28" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="6.0.28" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="6.0.28" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.28">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.35" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="6.0.35" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="6.0.35" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.35">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.28" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.28" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.28">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.35" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.35" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.35">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.2.8" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.7.33" />
|
||||
<PackageReference Include="NLog" Version="5.3.4" />
|
||||
<PackageReference Include="RestSharp" Version="112.1.0" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Maat.Data.Services
|
||||
/// </summary>
|
||||
protected TimeSpan FastCache
|
||||
{
|
||||
get => TimeSpan.FromSeconds(cacheTtlShort * rnd.Next(900, 999) / 1000);
|
||||
get => TimeSpan.FromSeconds(cacheTtlShort * rnd.Next(900, 995) / 1000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -27,7 +27,7 @@ namespace Maat.Data.Services
|
||||
/// </summary>
|
||||
protected TimeSpan LongCache
|
||||
{
|
||||
get => TimeSpan.FromSeconds(cacheTtlLong * rnd.Next(900, 999) / 1000);
|
||||
get => TimeSpan.FromSeconds(cacheTtlLong * rnd.Next(900, 995) / 1000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -35,7 +35,7 @@ namespace Maat.Data.Services
|
||||
/// </summary>
|
||||
protected TimeSpan UltraFastCache
|
||||
{
|
||||
get => TimeSpan.FromSeconds(cacheTtlShort / 6 * rnd.Next(900, 999) / 1000);
|
||||
get => TimeSpan.FromSeconds(cacheTtlShort / 6 * rnd.Next(900, 995) / 1000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,7 +43,7 @@ namespace Maat.Data.Services
|
||||
/// </summary>
|
||||
protected TimeSpan UltraLongCache
|
||||
{
|
||||
get => TimeSpan.FromSeconds(cacheTtlLong * 10 * rnd.Next(900, 999) / 1000);
|
||||
get => TimeSpan.FromSeconds(cacheTtlLong * 10 * rnd.Next(900, 995) / 1000);
|
||||
}
|
||||
|
||||
#endregion Protected Properties
|
||||
|
||||
@@ -7,29 +7,26 @@ using static Maat.Core.Enums;
|
||||
using System.Diagnostics;
|
||||
using Newtonsoft.Json;
|
||||
using System.Xml.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Maat.Data.Services
|
||||
{
|
||||
public class MsSqlTaskService : BaseServ, IDisposable
|
||||
{
|
||||
#region Public Fields
|
||||
|
||||
public static MsSqlController dbController = null!;
|
||||
|
||||
#endregion Public Fields
|
||||
|
||||
#region Public Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Servizio gestione Task su DB
|
||||
/// </summary>
|
||||
/// <param name="threadName"></param>
|
||||
/// <param name="dbConnString"></param>
|
||||
/// <param name="redisConnString"></param>
|
||||
public MsSqlTaskService(string threadName, string dbConnString, string redisConnString)
|
||||
/// <param name="threadName">nome del thread</param>
|
||||
/// <param name="dbConnString">stringa completa di connessione</param>
|
||||
/// <param name="redisConnString">stringa completa REDIS</param>
|
||||
/// <param name="apiUrl">url x chiamate api REST</param>
|
||||
/// <param name="execTimeOut">timeout esecuzione comandi sql (in minuti)</param>
|
||||
public MsSqlTaskService(string threadName, string dbConnString, string redisConnString, string apiUrl, int execTimeOut)
|
||||
{
|
||||
Log.Info($"{tName} | Starting MsSqlTaskService");
|
||||
tName = threadName;
|
||||
Log.Info($"{tName} | Starting MsSqlTaskService");
|
||||
connString = dbConnString;
|
||||
|
||||
// setup compoenti REDIS
|
||||
@@ -43,9 +40,12 @@ namespace Maat.Data.Services
|
||||
}
|
||||
else
|
||||
{
|
||||
dbController = new MsSqlController(tName, dbConnString);
|
||||
dbController = new MsSqlController(tName, dbConnString, execTimeOut);
|
||||
Log.Info($"{tName} | DbController OK");
|
||||
}
|
||||
|
||||
// conf rest call service
|
||||
RCallService = new RestCallService(apiUrl);
|
||||
}
|
||||
|
||||
#endregion Public Constructors
|
||||
@@ -64,27 +64,58 @@ namespace Maat.Data.Services
|
||||
public List<TaskResultModel> CheckExecute()
|
||||
{
|
||||
List<TaskResultModel> answ = new List<TaskResultModel>();
|
||||
List<TaskListModel> listTask = TaskListAll(Enums.Task2ExeType.ND, "");
|
||||
List<TaskListModel> listTask = TaskListAll(tName, Enums.Task2ExeType.ND, "");
|
||||
// verifico SE ci siano task in scadenza...
|
||||
DateTime adesso = DateTime.Now;
|
||||
var task2exe = listTask.Where(x => x.DtNextExec <= adesso).ToList();
|
||||
List<TaskListModel> task2exe = listTask.Where(x => x.Enabled && x.DtNextExec <= adesso).ToList();
|
||||
int numDone = 0;
|
||||
if (task2exe != null && task2exe.Count > 0)
|
||||
{
|
||||
Log.Info($"{tName} | Found {task2exe.Count} task to execute");
|
||||
// suddivisione x gruppi
|
||||
var ListGrouped = task2exe
|
||||
.OrderBy(x => x.Group)
|
||||
.ThenBy(x => x.Ordinal)
|
||||
.GroupBy(x => x.Group)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
// ciclo, e all'interno del gruppo SE non passa uno step si ferma....
|
||||
foreach (var taskGroup in ListGrouped)
|
||||
{
|
||||
foreach (var taskRec in taskGroup.Value)
|
||||
{
|
||||
TaskResultModel result = ExecuteTask(taskRec);
|
||||
answ.Add(result);
|
||||
numDone++;
|
||||
// SE non eseguito --> esce, come anche se passato timeout (e ultima esecuzione è prima della scadenza..
|
||||
if (result.ExecResult < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if false
|
||||
foreach (var taskRec in task2exe)
|
||||
{
|
||||
TaskResultModel result = ExecuteTask(taskRec.TaskId);
|
||||
TaskResultModel result = ExecuteTask(taskRec);
|
||||
answ.Add(result);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Trace($"{tName} | Chiamata CheckExecute");
|
||||
Log.Trace($"{tName} | Chiamata CheckExecute | eseguiti {numDone} task");
|
||||
}
|
||||
// resituisco
|
||||
return answ;
|
||||
}
|
||||
|
||||
public string CheckRestServer()
|
||||
{
|
||||
return RCallService.CheckServer();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Clear database controller
|
||||
@@ -96,13 +127,61 @@ namespace Maat.Data.Services
|
||||
/// <summary>
|
||||
/// Chiamata esecuzione di un singolo task programmato
|
||||
/// </summary>
|
||||
/// <param name="tName">Nome del thread</param>
|
||||
/// <param name="TaskId">TaskId</param>
|
||||
/// <param name="TaskRec">Task richiesto</param>
|
||||
/// <returns></returns>
|
||||
public TaskResultModel ExecuteTask(int TaskId)
|
||||
public TaskResultModel ExecuteTask(TaskListModel TaskRec)
|
||||
{
|
||||
TaskResultModel dbResult = dbController.ExecuteTask(TaskId);
|
||||
Log.Info($"{tName} | Executed | TaskId: {TaskId}");
|
||||
TaskResultModel dbResult = new TaskResultModel()
|
||||
{
|
||||
Task = $"TaskId: {TaskRec.TaskId} | {TaskRec.TType}",
|
||||
ExecResult = -1,
|
||||
TextResult = "Task Not recognized"
|
||||
};
|
||||
// verifico tipo di task ed eseguo di conseguenza...
|
||||
switch (TaskRec.TType)
|
||||
{
|
||||
//case Task2ExeType.ND:
|
||||
// break;
|
||||
//case Task2ExeType.Exe:
|
||||
// break;
|
||||
//case Task2ExeType.SqlCommand:
|
||||
// break;
|
||||
case Task2ExeType.SqlStored:
|
||||
dbResult = dbController.ExecuteSqlTask(TaskRec.TaskId, true);
|
||||
Log.Info($"{tName} | Executed | TaskRec: {TaskRec}");
|
||||
break;
|
||||
|
||||
case Task2ExeType.RestCallGet:
|
||||
// in primis testo la chiamata al servizio Health
|
||||
string rAnsw = RCallService.CheckServer();
|
||||
DateTime dtStart = DateTime.Now;
|
||||
// se ok effettuo vera chiamata...
|
||||
if (rAnsw.ToUpper() == "OK")
|
||||
{
|
||||
var callResp = RCallService.CallRestGet(TaskRec.Command, TaskRec.Args);
|
||||
DateTime dtEnd = DateTime.Now;
|
||||
string formattedJson = "";
|
||||
if (!string.IsNullOrEmpty(callResp.Content))
|
||||
{
|
||||
formattedJson = JValue.Parse(callResp.Content).ToString(Formatting.Indented);
|
||||
}
|
||||
TaskExecModel tExeMod = new TaskExecModel()
|
||||
{
|
||||
DtEnd = dtEnd,
|
||||
DtStart = dtStart,
|
||||
IsError = callResp.StatusCode != System.Net.HttpStatusCode.OK,
|
||||
TaskId = TaskRec.TaskId,
|
||||
// deserializzazione come json indentato?!?
|
||||
Result = formattedJson// $"{callResp.Content}".Replace("\"", ""),
|
||||
};
|
||||
// salvo su DB
|
||||
dbResult = dbController.TaskExecSaveExecuted(TaskRec.TaskId, true, tExeMod);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// svuoto cache!
|
||||
FlushCache("Task");
|
||||
return dbResult;
|
||||
@@ -150,7 +229,7 @@ namespace Maat.Data.Services
|
||||
/// <param name="CurrFilter"></param>
|
||||
/// <param name="searchVal"></param>
|
||||
/// <returns></returns>
|
||||
public List<TaskListModel> TaskListAll(Task2ExeType TType, string searchVal = "")
|
||||
public List<TaskListModel> TaskListAll(string threadName, Task2ExeType TType, string searchVal = "")
|
||||
{
|
||||
// setup parametri costanti
|
||||
string source = "DB";
|
||||
@@ -159,9 +238,9 @@ namespace Maat.Data.Services
|
||||
List<TaskListModel> result = new List<TaskListModel>();
|
||||
// cerco in redis...
|
||||
DateTime adesso = DateTime.Now;
|
||||
string currKey = $"{Const.RedisBaseKey}:Task:List:{TType}";
|
||||
string currKey = $"{Const.RedisBaseKey}:Task:{threadName.Replace("_", ":")}:List:{TType}";
|
||||
RedisValue rawData = redisDb.StringGet(currKey);
|
||||
if (rawData.HasValue)
|
||||
if (false && rawData.HasValue && rawData.Length() > 4)
|
||||
{
|
||||
var rawResult = JsonConvert.DeserializeObject<List<TaskListModel>>($"{rawData}");
|
||||
result = rawResult ?? new List<TaskListModel>();
|
||||
@@ -215,6 +294,14 @@ namespace Maat.Data.Services
|
||||
|
||||
#endregion Private Fields
|
||||
|
||||
#region Private Properties
|
||||
|
||||
private MsSqlController dbController { get; set; } = null!;
|
||||
|
||||
private RestCallService RCallService { get; set; } = null!;
|
||||
|
||||
#endregion Private Properties
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using RestSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Maat.Data.Services
|
||||
{
|
||||
public class RestCallService
|
||||
{
|
||||
#region Public Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Init classe
|
||||
/// </summary>
|
||||
/// <param name="ApiUrl"></param>
|
||||
public RestCallService(string ApiUrl)
|
||||
{
|
||||
apiUrl = ApiUrl;// _configuration.GetValue<string>("SrvConf:Prog.ApiUrl") ?? "http://office.egalware.com/MP/PROG";
|
||||
// fix opzioni base del RestClient
|
||||
restOptStd = new RestClientOptions
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(60),
|
||||
BaseUrl = new Uri(apiUrl),
|
||||
ThrowOnAnyError = true,
|
||||
ThrowOnDeserializationError = true
|
||||
};
|
||||
}
|
||||
|
||||
private string apiUrl = "";
|
||||
|
||||
#endregion Public Constructors
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Stato server da chiamare x verifiche preliminari stato API REST
|
||||
/// </summary>
|
||||
/// <param name="ApiUrl"></param>
|
||||
/// <returns></returns>
|
||||
public string CheckServer()
|
||||
{
|
||||
string answ = "ND";
|
||||
// cerco online
|
||||
using (RestClient client = new RestClient(restOptStd))
|
||||
{
|
||||
var request = new RestRequest($"api/health", Method.Get);
|
||||
try
|
||||
{
|
||||
var response = client.ExecuteGet(request);
|
||||
//var response = await client.GetAsync(request);
|
||||
// controllo risposta
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
// verifico risposta
|
||||
if (response.Content != null)
|
||||
{
|
||||
answ = response.Content.Replace("\"", "");
|
||||
//answ= JValue.Parse(response.Content).ToString(Formatting.Indented);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
Log.Error($"Eccezione in CheckServer{Environment.NewLine}{exc}");
|
||||
}
|
||||
}
|
||||
return answ;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Effettua una generica chiamata ApiRest
|
||||
/// </summary>
|
||||
/// <param name="ApiUrl">URL Api di base</param>
|
||||
/// <param name="ApiRequest">Richiesta completa</param>
|
||||
/// <returns></returns>
|
||||
public RestResponse CallRestGet(string ApiUrl, string ApiRequest)
|
||||
{
|
||||
RestResponse response;
|
||||
var currOpt = new RestClientOptions
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(60),
|
||||
BaseUrl = new Uri(ApiUrl),
|
||||
ThrowOnAnyError = true,
|
||||
ThrowOnDeserializationError = true
|
||||
};
|
||||
// cerco online
|
||||
using (RestClient client = new RestClient(currOpt))
|
||||
{
|
||||
var request = new RestRequest(ApiRequest, Method.Get);
|
||||
response = client.Get(request);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
#endregion Public Methods
|
||||
|
||||
#region Private Fields
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Classe logger
|
||||
/// </summary>
|
||||
private static Logger Log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
#endregion Private Fields
|
||||
|
||||
#region Private Properties
|
||||
|
||||
/// <summary>
|
||||
/// Conf client RestSharp standard:
|
||||
/// - base URI al sito
|
||||
/// - timeout 1 min
|
||||
/// </summary>
|
||||
private RestClientOptions restOptStd { get; set; } = new RestClientOptions();
|
||||
|
||||
#endregion Private Properties
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,10 @@ namespace Maat.Data
|
||||
{
|
||||
modelBuilder.HasAnnotation("Relational:Collation", "SQL_Latin1_General_CP1_CI_AS");
|
||||
|
||||
// fix x ritorno tracciato senza tabella corrispondente:
|
||||
// https://stackoverflow.com/questions/50276906/dbcontext-set-cannot-create-a-dbset-for-entity-because-this-type-is-not-includ
|
||||
modelBuilder.Entity<TaskResultModel>().HasNoKey();
|
||||
|
||||
OnModelCreatingPartial(modelBuilder);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
<Compile Remove="stdin.wxs" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath>
|
||||
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
|
||||
<IntermediateOutputPath>obj\$(Platform)\$(Configuration)\</IntermediateOutputPath>
|
||||
<SuppressPdbOutput>true</SuppressPdbOutput>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -5,7 +5,7 @@
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>1.0.2404.1009</Version>
|
||||
<Version>1.2.2507.2112</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -19,16 +19,21 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="8.0.3" />
|
||||
<None Include="Properties\PublishProfiles\SingleApp.pubxml.user" />
|
||||
<None Include="Properties\PublishProfiles\SingleAppDebug.pubxml.user" />
|
||||
<None Include="Properties\PublishProfiles\SingleAppManual.pubxml.user" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.2.8" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.7.33" />
|
||||
<PackageReference Include="NLog" Version="5.3.4" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -37,11 +42,14 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="conf\JobConfig.PROG.json">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="conf\JobConfig-demo.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="conf\JobConfig.json">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="logs\stderr.log">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
|
||||
+87
-39
@@ -4,6 +4,7 @@ using Maat.Data;
|
||||
using Maat.Data.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.FileSystemGlobbing.Internal;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using StackExchange.Redis;
|
||||
@@ -12,6 +13,7 @@ using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using static Maat.Core.CONF.RestApiConf;
|
||||
|
||||
|
||||
// init parte config, vedere https://blog.hildenco.com/2020/05/configuration-in-net-core-console.html
|
||||
@@ -29,6 +31,12 @@ string redisConf = config.GetConnectionString("Redis") ?? "?????????????????????
|
||||
string confPath = Path.Combine(Directory.GetCurrentDirectory(), "conf");
|
||||
Logger Log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
string apiUrlStd = config.GetValue<string>("SrvConf:Prog.ApiUrl") ?? "";
|
||||
if (string.IsNullOrEmpty(apiUrlStd))
|
||||
{
|
||||
Log.Error($"Errore: ApiUrl vuota!");
|
||||
}
|
||||
|
||||
// gestore della configurazione principale da cui derivare gestione...
|
||||
JobConfigConf currConf = new JobConfigConf();
|
||||
|
||||
@@ -44,6 +52,7 @@ bool verboseLog = false;
|
||||
bool logWriting = false;
|
||||
string appName = "Maat";
|
||||
int MinSaveIntSec = 60;
|
||||
int SqlCmdTimeoutMinutes = 10;
|
||||
int StepSec = 60;
|
||||
string JobConfigFile = "";
|
||||
|
||||
@@ -78,7 +87,7 @@ setupConf();
|
||||
if (currConf != null)
|
||||
{
|
||||
Log.Info("Starting job threads...");
|
||||
// cero i thread x tipo...
|
||||
// cero i thread x tipo MsSql...
|
||||
if (currConf.MsSqlJobs == null)
|
||||
{
|
||||
Log.Info("No MsSqlJob, skipping...");
|
||||
@@ -105,8 +114,8 @@ if (currConf != null)
|
||||
{
|
||||
// compongo connstring
|
||||
string dbConnStr = $"Server={currSrv.ServerName}; Database={dbName}; User ID={currSrv.DbUser}; Password={currSrv.DbPasswd}; integrated security=False; MultipleActiveResultSets=True; App={appName}";
|
||||
string threadName = $"ThdMsSql_{dbName}"; ;
|
||||
Thread newThread = new Thread(() => doCronDB(threadName, dbConnStr));
|
||||
string threadName = $"ThdMsSql_{currSrv.ServerName}_{dbName}";
|
||||
Thread newThread = new Thread(() => doCronDB(threadName, dbConnStr, SqlCmdTimeoutMinutes));
|
||||
newThread.Name = threadName;
|
||||
jobsThreadList.Add(newThread);
|
||||
Log.Info($"Added {newThread.Name} to jobsThreadList");
|
||||
@@ -115,6 +124,41 @@ if (currConf != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cero i thread x tipo Rest...
|
||||
if (currConf.RestApiJobs == null)
|
||||
{
|
||||
Log.Info("No RestApiJobs, skipping...");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currConf.RestApiJobs.ServerList == null || currConf.RestApiJobs.ServerList.Count == 0)
|
||||
{
|
||||
Log.Info("No Servers found in RestApiJobs.ServerList, skipping...");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var currSrv in currConf.RestApiJobs.ServerList)
|
||||
{
|
||||
// verifico di avere i singoli DB da processare...
|
||||
|
||||
if (currSrv.CallList == null || currSrv.CallList.Count == 0)
|
||||
{
|
||||
Log.Info("No DB found in RestApiJobs.ServerList.CallList, skipping...");
|
||||
}
|
||||
else
|
||||
{
|
||||
// thread unico x ogni server
|
||||
string threadName = $"ThdRest_{currSrv.SrvName}";
|
||||
Thread newThread = new Thread(() => doCronRest(threadName, currSrv.ApiUrl, currSrv.CallList));
|
||||
newThread.Name = threadName;
|
||||
jobsThreadList.Add(newThread);
|
||||
Log.Info($"Added {newThread.Name} to jobsThreadList");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// avvio tutti i thread preparati...
|
||||
foreach (var cThread in jobsThreadList)
|
||||
{
|
||||
@@ -149,6 +193,7 @@ void setupConf()
|
||||
StepSec = config.GetValue<int>("SrvConf:StepSec");
|
||||
MinSaveIntSec = config.GetValue<int>("SrvConf:MinSaveIntSec");
|
||||
var rawJCF = config.GetValue<string>("SrvConf:JobConfigFile");
|
||||
SqlCmdTimeoutMinutes = config.GetValue<int>("SrvConf:SqlCmdTimeoutMinutes");
|
||||
if (string.IsNullOrEmpty(rawJCF))
|
||||
{
|
||||
Log.Error($"Cannot start: missing JobConfigFile configuration");
|
||||
@@ -160,7 +205,7 @@ void setupConf()
|
||||
}
|
||||
}
|
||||
|
||||
void saveAndSendMessage(string memKey, string notifyChannel, string message)
|
||||
void saveAndSendMessage(string memKey, string channelName, string message)
|
||||
{
|
||||
// effettuo la scrittura nell'area di memoria indicata SE passato intervallo minimo
|
||||
bool doSave = true;
|
||||
@@ -186,10 +231,11 @@ void saveAndSendMessage(string memKey, string notifyChannel, string message)
|
||||
}
|
||||
|
||||
// invio notifica tramite il canale richiesto
|
||||
RedisChannel notifyChannel = new RedisChannel(channelName, RedisChannel.PatternMode.Auto);
|
||||
sub.Publish(notifyChannel, message);
|
||||
if (verboseLog)
|
||||
{
|
||||
Log.Info($"[{notifyChannel}] key: {memKey} | val/message: {message}");
|
||||
Log.Info($"[{channelName}] key: {memKey} | val/message: {message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -197,16 +243,6 @@ void saveAndSendMessage(string memKey, string notifyChannel, string message)
|
||||
{
|
||||
if (!logWriting)
|
||||
{
|
||||
#if false
|
||||
if (LogSimulator.ContainsKey(notifyChannel))
|
||||
{
|
||||
LogSimulator[notifyChannel]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogSimulator.Add(notifyChannel, 1);
|
||||
}
|
||||
#endif
|
||||
logWriting = true;
|
||||
// vedo se loggare...
|
||||
DateTime adesso = DateTime.Now;
|
||||
@@ -214,16 +250,6 @@ void saveAndSendMessage(string memKey, string notifyChannel, string message)
|
||||
{
|
||||
lastLog = adesso;
|
||||
Log.Info(lineSep);
|
||||
|
||||
#if false
|
||||
// lavoro su copia...
|
||||
var LogSimulatorCopy = new Dictionary<string, int>(LogSimulator);
|
||||
foreach (var item in LogSimulatorCopy)
|
||||
{
|
||||
Log.Info($"Redis mQueue {item.Key,-20}{item.Value,12}");
|
||||
}
|
||||
Log.Info(lineSep);
|
||||
#endif
|
||||
}
|
||||
logWriting = false;
|
||||
}
|
||||
@@ -236,16 +262,17 @@ void saveAndSendMessage(string memKey, string notifyChannel, string message)
|
||||
}
|
||||
|
||||
|
||||
void doCronDB(string threadName, string dbConnStr)
|
||||
void doCronDB(string threadName, string dbConnStr, int cmdTimeOut)
|
||||
{
|
||||
string tName = threadName;
|
||||
Log.Info($"{tName} | Starting dbCronDb task");
|
||||
// periodo standard 1 minuto
|
||||
int stepPeriod = 60000;
|
||||
MsSqlTaskService tRun = new MsSqlTaskService(tName, dbConnStr, redisConf);
|
||||
MsSqlTaskService tRun = new MsSqlTaskService(tName, dbConnStr, redisConf, apiUrlStd, cmdTimeOut);
|
||||
// ciclo infinito esegue task poi attende quanto manca a prox scadenza...
|
||||
do
|
||||
{
|
||||
//var testServer = tRun.CheckRestServer();
|
||||
// eseguo i miei task e recupero esito
|
||||
var newStatus = tRun.CheckExecute();
|
||||
|
||||
@@ -277,21 +304,43 @@ void doCronDB(string threadName, string dbConnStr)
|
||||
}
|
||||
|
||||
|
||||
void doCronTaskRest(string baseUrl)
|
||||
void doCronRest(string threadName, string baseUrl, List<CallConfig> callList)
|
||||
{
|
||||
string tName = threadName;
|
||||
Log.Info($"{tName} | Starting doCronRest task");
|
||||
// periodo standard 1 minuto
|
||||
int stepPeriod = 60000;
|
||||
|
||||
// nuovo runner x chiamate REST... si aspetta una lista di URL da chiamare
|
||||
|
||||
#if false
|
||||
|
||||
// per ora un db CABLATO, poi saranno tanti DB (o tanti thread?)
|
||||
TaskRunnerService tRun = new TaskRunnerService(dbConnStr, redisConf);
|
||||
#endif
|
||||
// init servizio x chiamate REST...
|
||||
RestCallService sRun = new RestCallService(baseUrl);
|
||||
// il servizio usa la callList di oggetti da chiamare con indicazione tempistiche, inizializzo la tabella delle attese...
|
||||
DateTime adesso = DateTime.Now;
|
||||
var waitList = callList.Select(x => new RestWaitConf()
|
||||
{
|
||||
CallId = x.CallId,
|
||||
WaitTime = x.WaitMinutes,
|
||||
NextExe = adesso.AddMinutes(x.WaitMinutes)
|
||||
}).ToList();
|
||||
// ciclo infinito esegue task poi attende quanto manca al prox minuto...
|
||||
do
|
||||
{
|
||||
// verifico in primis quale servizio sia eventualmente scaduto...
|
||||
DateTime exeTime = DateTime.Now;
|
||||
foreach (var item in waitList)
|
||||
{
|
||||
if (item.NextExe < exeTime)
|
||||
{
|
||||
// recupero la call...
|
||||
var currCall = callList.Where(x => x.CallId == item.CallId).FirstOrDefault();
|
||||
if (currCall != null)
|
||||
{
|
||||
// eseguo call
|
||||
var callResp = sRun.CallRestGet(baseUrl, currCall.Resource);
|
||||
}
|
||||
// rischedulo!
|
||||
item.NextExe = exeTime.AddMinutes(item.WaitTime);
|
||||
}
|
||||
}
|
||||
#if false
|
||||
// eseguo i miei task e recupero esito
|
||||
var newStatus = tRun.CheckExecute();
|
||||
@@ -310,10 +359,9 @@ void doCronTaskRest(string baseUrl)
|
||||
#endif
|
||||
|
||||
// calcolo quanto manca al prossimo step
|
||||
DateTime adesso = DateTime.Now;
|
||||
DateTime nextDt = adesso.Ceil(TimeSpan.FromMilliseconds(stepPeriod));
|
||||
double waitTime = nextDt.Subtract(adesso).TotalMilliseconds;
|
||||
if (waitTime == 0)
|
||||
DateTime nextDt = exeTime.Ceil(TimeSpan.FromMilliseconds(stepPeriod));
|
||||
double waitTime = nextDt.Subtract(exeTime).TotalMilliseconds;
|
||||
if (waitTime <= 0)
|
||||
{
|
||||
waitTime = stepPeriod * 90 / 100;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<History>False|2024-04-05T06:22:12.1466170Z;False|2024-04-04T09:20:19.1194551+02:00;False|2024-04-04T07:54:11.6078436+02:00;False|2024-04-03T18:57:56.8357860+02:00;False|2024-04-03T18:57:31.1394991+02:00;False|2024-04-03T18:56:30.3461610+02:00;False|2024-04-03T18:55:50.6292479+02:00;False|2024-04-03T18:54:46.2558736+02:00;False|2024-04-03T18:53:40.5084714+02:00;False|2024-04-03T18:52:58.5201680+02:00;</History>
|
||||
<History>True|2024-10-29T13:34:44.2509436Z||;False|2024-04-05T08:22:12.1466170+02:00||;False|2024-04-04T09:20:19.1194551+02:00||;False|2024-04-04T07:54:11.6078436+02:00||;False|2024-04-03T18:57:56.8357860+02:00||;False|2024-04-03T18:57:31.1394991+02:00||;False|2024-04-03T18:56:30.3461610+02:00||;False|2024-04-03T18:55:50.6292479+02:00||;False|2024-04-03T18:54:46.2558736+02:00||;False|2024-04-03T18:53:40.5084714+02:00||;False|2024-04-03T18:52:58.5201680+02:00||;</History>
|
||||
<LastFailureDetails />
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
+17
-2
@@ -1,5 +1,20 @@
|
||||
Per installare il simulatore come servizio impiegare nssm come gestore servizio.
|
||||
Per installare come servizio impiegare nssm come gestore servizio.
|
||||
|
||||
IN particolare installare con
|
||||
|
||||
nssm.exe install MP-MONO-SIM
|
||||
nssm.exe install MP-MONO-SIM
|
||||
|
||||
|
||||
nota compilazione: aggiunta forzatura progetto a 64bit:
|
||||
- vedere yaml
|
||||
|
||||
# creazione installer WIX
|
||||
- dotnet build $env:INST_NAME/$env:INST_NAME.wixproj -p:Configuration=$env:APP_CONF -p:Platform=x64
|
||||
|
||||
- vedere wixproject
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
|
||||
<IntermediateOutputPath>obj\$(Platform)\$(Configuration)\</IntermediateOutputPath>
|
||||
<SuppressPdbOutput>true</SuppressPdbOutput>
|
||||
</PropertyGroup>
|
||||
@@ -7,7 +7,8 @@
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"Redis": "localhost:26379, serviceName=devel, DefaultDatabase=15, connectTimeout=5000, syncTimeout=5000, asyncTimeout=5000, abortConnect=false, ssl=false, allowAdmin=true"
|
||||
"Redis": "localhost:6379, DefaultDatabase=14, connectTimeout=5000, syncTimeout=5000, asyncTimeout=5000, abortConnect=false, ssl=false, allowAdmin=true"
|
||||
//"Redis": "redis.ufficio:26379, serviceName=devel, DefaultDatabase=14, connectTimeout=5000, syncTimeout=5000, asyncTimeout=5000, abortConnect=false, ssl=false, allowAdmin=true"
|
||||
},
|
||||
"DbConfig": {
|
||||
"Server": "localhost",
|
||||
@@ -18,10 +19,10 @@
|
||||
"MailKit": {
|
||||
"SMTP": {
|
||||
"Address": "smtp.gmail.com",
|
||||
"Port": "465",
|
||||
"Account": "steamwarebot@gmail.com",
|
||||
"Password": "drmfsls16",
|
||||
"SenderEmail": "steamwarebot@gmail.com",
|
||||
"Port": "587",
|
||||
"Account": "services@steamware.net",
|
||||
"Password": "rzdwvdhvyxtbrzoq",
|
||||
"SenderEmail": "services@steamware.net",
|
||||
"SenderName": "Steamware Email BOT"
|
||||
}
|
||||
}
|
||||
@@ -30,6 +31,8 @@
|
||||
"AppName": "Maat.Runner",
|
||||
"StepSec": 60,
|
||||
"MinSaveIntSec": 15,
|
||||
"JobConfigFile": "JobConfig.json"
|
||||
"JobConfigFile": "JobConfig.json",
|
||||
"Prog.ApiUrl": "https://office.egalware.com/MP/PROG",
|
||||
"SqlCmdTimeoutMinutes": 5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
{
|
||||
"CallMode": "TaskList",
|
||||
"DbList": [
|
||||
"MoonPro",
|
||||
"MoonPro_PROG",
|
||||
"MoonPro_STATS"
|
||||
],
|
||||
"DbPasswd": "utente",
|
||||
"DbUser": "password",
|
||||
"DbPasswd": "db_password_here",
|
||||
"DbUser": "db_user_here",
|
||||
"ServerName": "SQL2016DEV"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"MsSqlJobs": {
|
||||
"ServerList": [
|
||||
{
|
||||
"CallMode": "TaskList",
|
||||
"DbList": [
|
||||
"MoonPro_PROG",
|
||||
"MoonPro_STATS"
|
||||
],
|
||||
"DbPasswd": "viadante16",
|
||||
"DbUser": "steamware",
|
||||
"ServerName": "SQL2016DEV"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"MsSqlJobs": {
|
||||
"ServerList": [
|
||||
{
|
||||
"CallMode": "TaskList",
|
||||
"DbList": [
|
||||
"MoonPro_PROG"
|
||||
],
|
||||
"DbPasswd": "viadante16",
|
||||
"DbUser": "steamware",
|
||||
"ServerName": "SQL2016DEV"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"MsSqlJobs": {
|
||||
"ServerList": [
|
||||
{
|
||||
"CallMode": "TaskList",
|
||||
"DbList": [
|
||||
"MoonPro_STATS"
|
||||
],
|
||||
"DbPasswd": "viadante16",
|
||||
"DbUser": "steamware",
|
||||
"ServerName": "SQL2016DEV"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
"CallMode": "TaskList",
|
||||
"DbList": [
|
||||
"MoonPro_PROG",
|
||||
"MoonPro_STATS"
|
||||
],
|
||||
"DbPasswd": "viadante16",
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.1.
|
||||
1.2.
|
||||
@@ -8,7 +8,13 @@
|
||||
<ul>{{LAST-CHANGES}}</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>v.1.* →</b>
|
||||
<b>v.1.2.* →</b>
|
||||
<ul>
|
||||
<li>Aggiunta gestione task REST (es MP-PROG)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>v.1.1.* →</b>
|
||||
<ul>
|
||||
<li>Prima release applicazione</li>
|
||||
<li>Installer WIX</li>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<body>
|
||||
<i>MagMan - Wood Warehouse Management System</i>
|
||||
<h4>Versione: 1.0.2404.0412</h4>
|
||||
<i>Maat.Installer - Manager for Application & Applicative Task</i>
|
||||
<h4>Versione: 1.2.2507.2112</h4>
|
||||
<br /> Note di rilascio:
|
||||
<ul>
|
||||
<li>
|
||||
@@ -8,20 +8,26 @@
|
||||
<ul>{{LAST-CHANGES}}</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>v.1.* →</b>
|
||||
<b>v.1.2.* →</b>
|
||||
<ul>
|
||||
<li>Prima release dotnet6</li>
|
||||
<li>Integrazione EFCore</li>
|
||||
<li>Integrazione EgtB&W</li>
|
||||
<li>Aggiunta gestione task REST (es MP-PROG)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>v.1.1.* →</b>
|
||||
<ul>
|
||||
<li>Prima release applicazione</li>
|
||||
<li>Installer WIX</li>
|
||||
<li>Integrazione processo gestione servizi</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div>
|
||||
<div style="float: left;">
|
||||
<img src="logoSteamware.png" />
|
||||
<img src="LogoEgw.png" />
|
||||
</div>
|
||||
<div style="float: right;">
|
||||
<a href="https://www.steamware.net/IOT" target="_blank">© Steamware 2006-2021</a>
|
||||
<a href="https://www.egalware.net/support" target="_blank">© EgalWare 2021-2025</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.0.2404.0412
|
||||
1.2.2507.2112
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>1.0.2404.0412</version>
|
||||
<url>http://nexus.steamware.net/repository/SWS/MagMan/stable/0/MagMan.UI.zip</url>
|
||||
<changelog>http://nexus.steamware.net/repository/SWS/MagMan/stable/0/ChangeLog.html</changelog>
|
||||
<version>1.2.2507.2112</version>
|
||||
<url>http://nexus.steamware.net/repository/SWS/Maat.Installer/stable/LAST/Maat.Installer.msi</url>
|
||||
<changelog>http://nexus.steamware.net/repository/SWS/Maat.Installer/stable/LAST/ChangeLog.html</changelog>
|
||||
<mandatory>false</mandatory>
|
||||
</item>
|
||||
|
||||
Reference in New Issue
Block a user