Projektdateien hinzufügen.
This commit is contained in:
13
BUILD_INFO.txt
Normal file
13
BUILD_INFO.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FileInterface Watcher + CronScheduler - COMPLETE
|
||||||
|
Date: 2026-08-19
|
||||||
|
Target: .NET Framework 4.8, x86
|
||||||
|
IMS API: IMSApiDotNet.dll 10.0.0.3 included under FileinterfaceCronScheduler/lib
|
||||||
|
Features:
|
||||||
|
- FileSystemWatcher jobs configurable through HTTP web client
|
||||||
|
- Cron jobs configurable through HTTP web client
|
||||||
|
- Both subsystems run concurrently and share the IMS API client/session
|
||||||
|
- Unlimited inArgs per job
|
||||||
|
- Optional file move after successful customFunction
|
||||||
|
- Watcher Created/Changed/Renamed selection, recursion and debounce
|
||||||
|
- Live watcher reload after saving/deleting configuration
|
||||||
|
- HTTP Basic Authentication; default admin/admin; configurable in App.config
|
||||||
21
FileinterfaceCronScheduler.sln
Normal file
21
FileinterfaceCronScheduler.sln
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileinterfaceCronScheduler", "FileinterfaceCronScheduler\FileinterfaceCronScheduler.csproj", "{D7E916B9-969E-4CFD-B6A3-527B7091D45A}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{D7E916B9-969E-4CFD-B6A3-527B7091D45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D7E916B9-969E-4CFD-B6A3-527B7091D45A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D7E916B9-969E-4CFD-B6A3-527B7091D45A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D7E916B9-969E-4CFD-B6A3-527B7091D45A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
43
FileinterfaceCronScheduler/App.config
Normal file
43
FileinterfaceCronScheduler/App.config
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<startup useLegacyV2RuntimeActivationPolicy="true">
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||||
|
</startup>
|
||||||
|
<appSettings>
|
||||||
|
<!-- =============================================================== -->
|
||||||
|
<!-- iTAC IMSApiDotNet / Login -->
|
||||||
|
<!-- Values correspond to the original FileInterface configuration. -->
|
||||||
|
<!-- =============================================================== -->
|
||||||
|
<add key="IMS.AppID" value="FileInterfaceCronScheduler" />
|
||||||
|
<add key="IMS.ServerUrl" value="http://YOUR-IMS-SERVER:PORT" />
|
||||||
|
<add key="IMS.StationNumber" value="YOUR_STATION" />
|
||||||
|
<add key="IMS.ClientNumber" value="1" />
|
||||||
|
<add key="IMS.RegistrationType" value="S" />
|
||||||
|
<!-- Relative paths are resolved from the EXE directory. -->
|
||||||
|
<add key="IMS.PropertyDirectory" value="." />
|
||||||
|
<!-- false = Cronjobs duerfen die IMS-CustomFunction parallel aufrufen. -->
|
||||||
|
<add key="IMS.SerializeCalls" value="false" />
|
||||||
|
<!-- Scheduler -->
|
||||||
|
<add key="Scheduler.ConfigFile" value="cronjobs.json" />
|
||||||
|
<add key="Watcher.ConfigFile" value="watcherjobs.json" />
|
||||||
|
<add key="Scheduler.WebPrefix" value="http://127.0.0.1:8095/" />
|
||||||
|
<!-- HTTP Basic Authentication for the administration web client. -->
|
||||||
|
<!-- Default login: admin / admin. Change both values before production use. -->
|
||||||
|
<!-- Set Scheduler.WebUser empty only if authentication shall explicitly be disabled. -->
|
||||||
|
<add key="Scheduler.WebUser" value="admin" />
|
||||||
|
<add key="Scheduler.WebPassword" value="admin" />
|
||||||
|
<add key="ClientSettingsProvider.ServiceUri" value="" />
|
||||||
|
</appSettings>
|
||||||
|
<system.web>
|
||||||
|
<membership defaultProvider="ClientAuthenticationMembershipProvider">
|
||||||
|
<providers>
|
||||||
|
<add name="ClientAuthenticationMembershipProvider" type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" />
|
||||||
|
</providers>
|
||||||
|
</membership>
|
||||||
|
<roleManager defaultProvider="ClientRoleProvider" enabled="true">
|
||||||
|
<providers>
|
||||||
|
<add name="ClientRoleProvider" type="System.Web.ClientServices.Providers.ClientRoleProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" cacheTimeout="86400" />
|
||||||
|
</providers>
|
||||||
|
</roleManager>
|
||||||
|
</system.web>
|
||||||
|
</configuration>
|
||||||
96
FileinterfaceCronScheduler/FileinterfaceCronScheduler.csproj
Normal file
96
FileinterfaceCronScheduler/FileinterfaceCronScheduler.csproj
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{D7E916B9-969E-4CFD-B6A3-527B7091D45A}</ProjectGuid>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<RootNamespace>FileinterfaceCronScheduler</RootNamespace>
|
||||||
|
<AssemblyName>FileinterfaceCronScheduler</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
<LangVersion>7.3</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>x86</PlatformTarget>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<PlatformTarget>x86</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<StartupObject>FileinterfaceCronScheduler.Program</StartupObject>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<SignManifests>false</SignManifests>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="IMSApiDotNet">
|
||||||
|
<HintPath>lib\IMSApiDotNet.dll</HintPath>
|
||||||
|
<Private>true</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Configuration" />
|
||||||
|
<Reference Include="System.ServiceProcess" />
|
||||||
|
<Reference Include="System.Web.Extensions" />
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="FileinterfaceCronSchedulerService.cs">
|
||||||
|
<SubType>Component</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="RuntimeFactory.cs" />
|
||||||
|
<Compile Include="ImsCustomFunctionInvoker.cs" />
|
||||||
|
<Compile Include="IMSApi\ImsApiClient.cs" />
|
||||||
|
<Compile Include="Scheduler\CronExpression.cs" />
|
||||||
|
<Compile Include="Scheduler\CronJobConfig.cs" />
|
||||||
|
<Compile Include="Scheduler\CronJobExecutor.cs" />
|
||||||
|
<Compile Include="Scheduler\CronJobRepository.cs" />
|
||||||
|
<Compile Include="Scheduler\CronScheduler.cs" />
|
||||||
|
<Compile Include="Scheduler\WatcherJobConfig.cs" />
|
||||||
|
<Compile Include="Scheduler\WatcherJobRepository.cs" />
|
||||||
|
<Compile Include="Scheduler\WatcherJobExecutor.cs" />
|
||||||
|
<Compile Include="Scheduler\FileSystemWatcherManager.cs" />
|
||||||
|
<Compile Include="Scheduler\CustomFunctionInvoker.cs" />
|
||||||
|
<Compile Include="Scheduler\JobLog.cs" />
|
||||||
|
<Compile Include="Scheduler\SchedulerRuntime.cs" />
|
||||||
|
<Compile Include="Scheduler\WebAdminServer.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="App.config" />
|
||||||
|
<None Include="lib\README.txt" />
|
||||||
|
<Content Include="ihas.properties">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="cronjobs.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="watcherjobs.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<Target Name="CheckIMSApiDll" BeforeTargets="ResolveAssemblyReferences">
|
||||||
|
<Error Condition="!Exists('$(MSBuildProjectDirectory)\lib\IMSApiDotNet.dll')" Text="IMSApiDotNet.dll fehlt. Erwarteter Pfad: FileinterfaceCronScheduler\lib\IMSApiDotNet.dll" />
|
||||||
|
</Target>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.ServiceProcess;
|
||||||
|
using FileInterface.Scheduler;
|
||||||
|
|
||||||
|
namespace FileinterfaceCronScheduler
|
||||||
|
{
|
||||||
|
public sealed class FileinterfaceCronSchedulerService : ServiceBase
|
||||||
|
{
|
||||||
|
private SchedulerRuntime _runtime;
|
||||||
|
|
||||||
|
public FileinterfaceCronSchedulerService()
|
||||||
|
{
|
||||||
|
ServiceName = "FileinterfaceCronScheduler";
|
||||||
|
CanStop = true;
|
||||||
|
CanPauseAndContinue = false;
|
||||||
|
AutoLog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnStart(string[] args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_runtime = RuntimeFactory.Create();
|
||||||
|
_runtime.Start();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if (_runtime != null)
|
||||||
|
{
|
||||||
|
_runtime.Dispose();
|
||||||
|
_runtime = null;
|
||||||
|
}
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnStop()
|
||||||
|
{
|
||||||
|
if (_runtime != null)
|
||||||
|
{
|
||||||
|
_runtime.Dispose();
|
||||||
|
_runtime = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
229
FileinterfaceCronScheduler/IMSApi/ImsApiClient.cs
Normal file
229
FileinterfaceCronScheduler/IMSApi/ImsApiClient.cs
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
using System;
|
||||||
|
using System.Configuration;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using FileInterface.Scheduler;
|
||||||
|
using com.itac.mes.imsapi.client.dotnet;
|
||||||
|
using com.itac.mes.imsapi.domain.container;
|
||||||
|
|
||||||
|
namespace FileinterfaceCronScheduler.IMSApi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes IMSApiDotNet, logs in once and exposes customFunction to the scheduler.
|
||||||
|
/// Configuration is read from App.config.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ImsApiClient : ICustomFunctionInvoker, IDisposable
|
||||||
|
{
|
||||||
|
private readonly object _sync = new object();
|
||||||
|
private readonly object _callSync = new object();
|
||||||
|
private IIMSApiDotNet _imsapi;
|
||||||
|
private IMSApiSessionContextStruct _sessionContext;
|
||||||
|
private bool _initialized;
|
||||||
|
private bool _disposed;
|
||||||
|
private readonly bool _serializeCalls = String.Equals(ConfigurationManager.AppSettings["IMS.SerializeCalls"], "true", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public bool IsInitialized { get { lock (_sync) return _initialized; } }
|
||||||
|
public bool IsLoggedIn { get { lock (_sync) return _sessionContext != null; } }
|
||||||
|
public long SessionId { get { lock (_sync) return _sessionContext == null ? 0L : _sessionContext.sessionId; } }
|
||||||
|
|
||||||
|
public void Connect()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
EnsureInitializedNoLock();
|
||||||
|
EnsureLoggedInNoLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureInitializedNoLock()
|
||||||
|
{
|
||||||
|
if (_initialized && _imsapi != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string appId = Required("IMS.AppID", "AppID");
|
||||||
|
string serverUrl = Required("IMS.ServerUrl", "Server_URL");
|
||||||
|
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
string propDir = Get("IMS.PropertyDirectory", baseDir);
|
||||||
|
if (!Path.IsPathRooted(propDir))
|
||||||
|
propDir = Path.Combine(baseDir, propDir);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(propDir);
|
||||||
|
string ihas = Path.Combine(propDir, "ihas.properties");
|
||||||
|
if (!File.Exists(ihas))
|
||||||
|
File.WriteAllText(ihas, String.Empty);
|
||||||
|
|
||||||
|
Trace.TraceInformation("Initializing IMSApiDotNet. AppID={0}, Server={1}, PropDir={2}", appId, serverUrl, propDir);
|
||||||
|
|
||||||
|
IMSApiDotNet.setProperty("itac.appid", appId);
|
||||||
|
IMSApiDotNet.setProperty("itac.artes.clusternodes", serverUrl);
|
||||||
|
IMSApiDotNet.setProperty("itac.propdir", propDir);
|
||||||
|
|
||||||
|
_imsapi = IMSApiDotNet.loadLibrary();
|
||||||
|
if (_imsapi == null)
|
||||||
|
throw new InvalidOperationException("IMSApiDotNet.loadLibrary() returned null.");
|
||||||
|
|
||||||
|
string version;
|
||||||
|
int versionResult = _imsapi.imsapiGetLibraryVersion(out version);
|
||||||
|
if (versionResult == IMSApiDotNetConstants.RES_OK)
|
||||||
|
Trace.TraceInformation("IMSApiDotNet library version: " + version);
|
||||||
|
else
|
||||||
|
Trace.TraceWarning("imsapiGetLibraryVersion returned " + versionResult);
|
||||||
|
|
||||||
|
int initResult = _imsapi.imsapiInit();
|
||||||
|
if (initResult != IMSApiDotNetConstants.RES_OK)
|
||||||
|
throw new InvalidOperationException("imsapiInit failed. Result=" + initResult);
|
||||||
|
|
||||||
|
_initialized = true;
|
||||||
|
Trace.TraceInformation("IMSApiDotNet initialized successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureLoggedInNoLock()
|
||||||
|
{
|
||||||
|
if (_sessionContext != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string station = Required("IMS.StationNumber", "StationNr");
|
||||||
|
// Original FileInterface contains the historic typo 'CleintNO'. Both names are supported.
|
||||||
|
string client = FirstNonEmpty(
|
||||||
|
ConfigurationManager.AppSettings["IMS.ClientNumber"],
|
||||||
|
ConfigurationManager.AppSettings["ClientNO"],
|
||||||
|
ConfigurationManager.AppSettings["CleintNO"]);
|
||||||
|
if (String.IsNullOrWhiteSpace(client))
|
||||||
|
throw new ConfigurationErrorsException("Missing IMS client number. Configure IMS.ClientNumber (or legacy CleintNO).");
|
||||||
|
|
||||||
|
string registrationType = Get("IMS.RegistrationType", "S");
|
||||||
|
var validation = new IMSApiSessionValidationStruct
|
||||||
|
{
|
||||||
|
stationNumber = station,
|
||||||
|
client = client,
|
||||||
|
registrationType = registrationType
|
||||||
|
};
|
||||||
|
|
||||||
|
Trace.TraceInformation("Calling regLogin(). Station={0}, Client={1}, RegistrationType={2}", station, client, registrationType);
|
||||||
|
|
||||||
|
IMSApiSessionContextStruct newSessionContext;
|
||||||
|
int loginResult = _imsapi.regLogin(validation, out newSessionContext);
|
||||||
|
if (loginResult != IMSApiDotNetConstants.RES_OK || newSessionContext == null)
|
||||||
|
throw new InvalidOperationException("IMS regLogin failed. Result=" + loginResult);
|
||||||
|
|
||||||
|
_sessionContext = newSessionContext;
|
||||||
|
Trace.TraceInformation("IMS login successful. SessionId=" + _sessionContext.sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomFunctionCallResult Invoke(string customFunctionName, string[] inArgs)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrWhiteSpace(customFunctionName))
|
||||||
|
return CustomFunctionCallResult.Fail("CustomFunction is empty.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IIMSApiDotNet api;
|
||||||
|
IMSApiSessionContextStruct session;
|
||||||
|
|
||||||
|
// Only connection/login state is protected. The actual CustomFunction call
|
||||||
|
// can run concurrently for independent cron jobs.
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
EnsureInitializedNoLock();
|
||||||
|
EnsureLoggedInNoLock();
|
||||||
|
api = _imsapi;
|
||||||
|
session = _sessionContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_serializeCalls)
|
||||||
|
{
|
||||||
|
lock (_callSync)
|
||||||
|
return InvokeCore(api, session, customFunctionName, inArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return InvokeCore(api, session, customFunctionName, inArgs);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.TraceError("IMS customFunction failed: " + ex);
|
||||||
|
return CustomFunctionCallResult.Fail(ex.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CustomFunctionCallResult InvokeCore(
|
||||||
|
IIMSApiDotNet api,
|
||||||
|
IMSApiSessionContextStruct session,
|
||||||
|
string customFunctionName,
|
||||||
|
string[] inArgs)
|
||||||
|
{
|
||||||
|
string[] outArgs;
|
||||||
|
string customErrorString;
|
||||||
|
int result = api.customFunction(
|
||||||
|
session,
|
||||||
|
customFunctionName,
|
||||||
|
inArgs ?? new string[0],
|
||||||
|
out outArgs,
|
||||||
|
out customErrorString);
|
||||||
|
|
||||||
|
string outText = outArgs == null || outArgs.Length == 0
|
||||||
|
? String.Empty
|
||||||
|
: String.Join(" | ", outArgs);
|
||||||
|
|
||||||
|
if (result != IMSApiDotNetConstants.RES_OK)
|
||||||
|
{
|
||||||
|
return CustomFunctionCallResult.Fail(
|
||||||
|
"IMS result=" + result +
|
||||||
|
(String.IsNullOrWhiteSpace(customErrorString) ? String.Empty : "; Error=" + customErrorString) +
|
||||||
|
(String.IsNullOrWhiteSpace(outText) ? String.Empty : "; OutArgs=" + outText));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomFunctionCallResult.Ok(
|
||||||
|
"IMS result=0" +
|
||||||
|
(String.IsNullOrWhiteSpace(customErrorString) ? String.Empty : "; Info=" + customErrorString) +
|
||||||
|
(String.IsNullOrWhiteSpace(outText) ? String.Empty : "; OutArgs=" + outText));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
// The FileInterface source currently has no regLogout/imsapiFinish call.
|
||||||
|
// We therefore release managed references here. If the deployed IMSAPI
|
||||||
|
// version exposes an explicit logout method, it can safely be added here.
|
||||||
|
_sessionContext = null;
|
||||||
|
_imsapi = null;
|
||||||
|
_initialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Get(string key, string fallback)
|
||||||
|
{
|
||||||
|
string value = ConfigurationManager.AppSettings[key];
|
||||||
|
return String.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Required(string preferredKey, string legacyKey)
|
||||||
|
{
|
||||||
|
string value = FirstNonEmpty(
|
||||||
|
ConfigurationManager.AppSettings[preferredKey],
|
||||||
|
ConfigurationManager.AppSettings[legacyKey]);
|
||||||
|
if (String.IsNullOrWhiteSpace(value))
|
||||||
|
throw new ConfigurationErrorsException("Missing setting '" + preferredKey + "' (legacy: '" + legacyKey + "').");
|
||||||
|
return value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FirstNonEmpty(params string[] values)
|
||||||
|
{
|
||||||
|
if (values == null) return null;
|
||||||
|
foreach (string value in values)
|
||||||
|
if (!String.IsNullOrWhiteSpace(value)) return value.Trim();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowIfDisposed()
|
||||||
|
{
|
||||||
|
if (_disposed) throw new ObjectDisposedException(GetType().FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
FileinterfaceCronScheduler/ImsCustomFunctionInvoker.cs
Normal file
37
FileinterfaceCronScheduler/ImsCustomFunctionInvoker.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using FileInterface.Scheduler;
|
||||||
|
using FileinterfaceCronScheduler.IMSApi;
|
||||||
|
|
||||||
|
namespace FileinterfaceCronScheduler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Thin scheduler adapter around the real IMSApiClient.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ImsCustomFunctionInvoker : ICustomFunctionInvoker, IDisposable
|
||||||
|
{
|
||||||
|
private readonly ImsApiClient _client;
|
||||||
|
|
||||||
|
public ImsCustomFunctionInvoker()
|
||||||
|
{
|
||||||
|
_client = new ImsApiClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Connect()
|
||||||
|
{
|
||||||
|
_client.Connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsLoggedIn { get { return _client.IsLoggedIn; } }
|
||||||
|
public long SessionId { get { return _client.SessionId; } }
|
||||||
|
|
||||||
|
public CustomFunctionCallResult Invoke(string customFunctionName, string[] inArgs)
|
||||||
|
{
|
||||||
|
return _client.Invoke(customFunctionName, inArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
FileinterfaceCronScheduler/Program.cs
Normal file
44
FileinterfaceCronScheduler/Program.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.ServiceProcess;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace FileinterfaceCronScheduler
|
||||||
|
{
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
private static int Main(string[] args)
|
||||||
|
{
|
||||||
|
bool forceService = args.Any(a => String.Equals(a, "--service", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (!Environment.UserInteractive || forceService)
|
||||||
|
{
|
||||||
|
ServiceBase.Run(new FileinterfaceCronSchedulerService());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Title = "FileInterface Watcher + CronScheduler";
|
||||||
|
Console.WriteLine("FileInterface Watcher + CronScheduler");
|
||||||
|
Console.WriteLine("====================================");
|
||||||
|
Console.WriteLine("Konsole/Debug-Modus. Mit --service als Windows-Dienst starten.");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
using (var runtime = RuntimeFactory.Create())
|
||||||
|
using (var quit = new ManualResetEvent(false))
|
||||||
|
{
|
||||||
|
Console.CancelKeyPress += (s, e) =>
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
quit.Set();
|
||||||
|
};
|
||||||
|
|
||||||
|
runtime.Start();
|
||||||
|
Console.WriteLine("FileSystemWatcher, CronScheduler und Webinterface gestartet.");
|
||||||
|
Console.WriteLine("Beenden mit Strg+C.");
|
||||||
|
quit.WaitOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
FileinterfaceCronScheduler/Properties/AssemblyInfo.cs
Normal file
15
FileinterfaceCronScheduler/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
[assembly: AssemblyTitle("FileinterfaceCronScheduler")]
|
||||||
|
[assembly: AssemblyDescription("Cron-based FileInterface for iTAC IMS CustomFunctions")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("FileinterfaceCronScheduler")]
|
||||||
|
[assembly: AssemblyCopyright("")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
[assembly: Guid("e0d85515-96cb-4f8c-97c3-9ec7079780fc")]
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||||
47
FileinterfaceCronScheduler/RuntimeFactory.cs
Normal file
47
FileinterfaceCronScheduler/RuntimeFactory.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Configuration;
|
||||||
|
using System.IO;
|
||||||
|
using FileInterface.Scheduler;
|
||||||
|
|
||||||
|
namespace FileinterfaceCronScheduler
|
||||||
|
{
|
||||||
|
internal static class RuntimeFactory
|
||||||
|
{
|
||||||
|
public static SchedulerRuntime Create()
|
||||||
|
{
|
||||||
|
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
string cronConfigFile = Get("Scheduler.ConfigFile", "cronjobs.json");
|
||||||
|
string watcherConfigFile = Get("Watcher.ConfigFile", "watcherjobs.json");
|
||||||
|
|
||||||
|
if (!Path.IsPathRooted(cronConfigFile)) cronConfigFile = Path.Combine(baseDir, cronConfigFile);
|
||||||
|
if (!Path.IsPathRooted(watcherConfigFile)) watcherConfigFile = Path.Combine(baseDir, watcherConfigFile);
|
||||||
|
|
||||||
|
var options = new SchedulerOptions
|
||||||
|
{
|
||||||
|
ConfigFile = cronConfigFile,
|
||||||
|
WatcherConfigFile = watcherConfigFile,
|
||||||
|
WebPrefix = Get("Scheduler.WebPrefix", "http://127.0.0.1:8095/"),
|
||||||
|
WebUser = Get("Scheduler.WebUser", "admin"),
|
||||||
|
WebPassword = Get("Scheduler.WebPassword", "admin")
|
||||||
|
};
|
||||||
|
|
||||||
|
var invoker = new ImsCustomFunctionInvoker();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
invoker.Connect();
|
||||||
|
return new SchedulerRuntime(invoker, options, invoker);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
invoker.Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Get(string key, string fallback)
|
||||||
|
{
|
||||||
|
string value = ConfigurationManager.AppSettings[key];
|
||||||
|
return String.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
131
FileinterfaceCronScheduler/Scheduler/CronExpression.cs
Normal file
131
FileinterfaceCronScheduler/Scheduler/CronExpression.cs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Standard 5-field cron expression: minute hour day-of-month month day-of-week.
|
||||||
|
/// Supported: *, */n, a,b,c, a-b, a-b/n. Day-of-week: 0 or 7 = Sunday.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CronExpression
|
||||||
|
{
|
||||||
|
private readonly HashSet<int> _minutes;
|
||||||
|
private readonly HashSet<int> _hours;
|
||||||
|
private readonly HashSet<int> _days;
|
||||||
|
private readonly HashSet<int> _months;
|
||||||
|
private readonly HashSet<int> _daysOfWeek;
|
||||||
|
|
||||||
|
public string Expression { get; private set; }
|
||||||
|
|
||||||
|
private CronExpression(string expression,
|
||||||
|
HashSet<int> minutes,
|
||||||
|
HashSet<int> hours,
|
||||||
|
HashSet<int> days,
|
||||||
|
HashSet<int> months,
|
||||||
|
HashSet<int> daysOfWeek)
|
||||||
|
{
|
||||||
|
Expression = expression;
|
||||||
|
_minutes = minutes;
|
||||||
|
_hours = hours;
|
||||||
|
_days = days;
|
||||||
|
_months = months;
|
||||||
|
_daysOfWeek = daysOfWeek;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CronExpression Parse(string expression)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrWhiteSpace(expression))
|
||||||
|
throw new FormatException("Cron expression is empty.");
|
||||||
|
|
||||||
|
string[] parts = expression.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (parts.Length != 5)
|
||||||
|
throw new FormatException("Cron must contain exactly 5 fields: minute hour day month day-of-week.");
|
||||||
|
|
||||||
|
return new CronExpression(
|
||||||
|
expression.Trim(),
|
||||||
|
ParseField(parts[0], 0, 59, false),
|
||||||
|
ParseField(parts[1], 0, 23, false),
|
||||||
|
ParseField(parts[2], 1, 31, false),
|
||||||
|
ParseField(parts[3], 1, 12, false),
|
||||||
|
ParseField(parts[4], 0, 7, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMatch(DateTime localTime)
|
||||||
|
{
|
||||||
|
int dow = (int)localTime.DayOfWeek;
|
||||||
|
return _minutes.Contains(localTime.Minute)
|
||||||
|
&& _hours.Contains(localTime.Hour)
|
||||||
|
&& _days.Contains(localTime.Day)
|
||||||
|
&& _months.Contains(localTime.Month)
|
||||||
|
&& _daysOfWeek.Contains(dow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<int> ParseField(string text, int min, int max, bool normalizeSunday)
|
||||||
|
{
|
||||||
|
var result = new HashSet<int>();
|
||||||
|
string[] segments = text.Split(',');
|
||||||
|
foreach (string rawSegment in segments)
|
||||||
|
{
|
||||||
|
string segment = rawSegment.Trim();
|
||||||
|
if (segment.Length == 0)
|
||||||
|
throw new FormatException("Empty cron field segment.");
|
||||||
|
|
||||||
|
int step = 1;
|
||||||
|
string rangePart = segment;
|
||||||
|
int slash = segment.IndexOf('/');
|
||||||
|
if (slash >= 0)
|
||||||
|
{
|
||||||
|
rangePart = segment.Substring(0, slash);
|
||||||
|
string stepText = segment.Substring(slash + 1);
|
||||||
|
if (!Int32.TryParse(stepText, NumberStyles.Integer, CultureInfo.InvariantCulture, out step) || step <= 0)
|
||||||
|
throw new FormatException("Invalid cron step: " + segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
int start;
|
||||||
|
int end;
|
||||||
|
if (rangePart == "*")
|
||||||
|
{
|
||||||
|
start = min;
|
||||||
|
end = max;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int dash = rangePart.IndexOf('-');
|
||||||
|
if (dash >= 0)
|
||||||
|
{
|
||||||
|
start = ParseNumber(rangePart.Substring(0, dash), min, max);
|
||||||
|
end = ParseNumber(rangePart.Substring(dash + 1), min, max);
|
||||||
|
if (start > end)
|
||||||
|
throw new FormatException("Cron range start is greater than end: " + segment);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
start = ParseNumber(rangePart, min, max);
|
||||||
|
end = start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int value = start; value <= end; value += step)
|
||||||
|
{
|
||||||
|
int normalized = normalizeSunday && value == 7 ? 0 : value;
|
||||||
|
result.Add(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Count == 0)
|
||||||
|
throw new FormatException("Cron field produced no values: " + text);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ParseNumber(string text, int min, int max)
|
||||||
|
{
|
||||||
|
int value;
|
||||||
|
if (!Int32.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
|
||||||
|
throw new FormatException("Invalid cron number: " + text);
|
||||||
|
if (value < min || value > max)
|
||||||
|
throw new FormatException("Cron value out of range: " + value + " (allowed " + min + "-" + max + ")");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
FileinterfaceCronScheduler/Scheduler/CronJobConfig.cs
Normal file
69
FileinterfaceCronScheduler/Scheduler/CronJobConfig.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
public sealed class CronJobConfig
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
public string Cron { get; set; }
|
||||||
|
|
||||||
|
// File processing. If CallOnceWithoutFile=true, SourceDirectory/SearchPattern are ignored.
|
||||||
|
public bool CallOnceWithoutFile { get; set; }
|
||||||
|
public string SourceDirectory { get; set; }
|
||||||
|
public string SearchPattern { get; set; }
|
||||||
|
public bool IncludeSubdirectories { get; set; }
|
||||||
|
|
||||||
|
// Existing FileInterface-style preprocessing.
|
||||||
|
public bool RemoveEmptyLines { get; set; }
|
||||||
|
public string JoinSeparator { get; set; }
|
||||||
|
|
||||||
|
// iTAC CustomFunction.
|
||||||
|
public string CustomFunction { get; set; }
|
||||||
|
public List<string> InArgs { get; set; }
|
||||||
|
|
||||||
|
// File handling after successful CF call.
|
||||||
|
public bool MoveFileAfterSuccess { get; set; }
|
||||||
|
public string MoveTargetDirectory { get; set; }
|
||||||
|
public bool OverwriteTargetFile { get; set; }
|
||||||
|
|
||||||
|
public CronJobConfig()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString("N");
|
||||||
|
Name = "New job";
|
||||||
|
Enabled = true;
|
||||||
|
Cron = "*/5 * * * *";
|
||||||
|
CallOnceWithoutFile = false;
|
||||||
|
SourceDirectory = "";
|
||||||
|
SearchPattern = "*.*";
|
||||||
|
IncludeSubdirectories = false;
|
||||||
|
RemoveEmptyLines = true;
|
||||||
|
JoinSeparator = ";";
|
||||||
|
CustomFunction = "";
|
||||||
|
InArgs = new List<string>();
|
||||||
|
MoveFileAfterSuccess = false;
|
||||||
|
MoveTargetDirectory = "";
|
||||||
|
OverwriteTargetFile = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SchedulerOptions
|
||||||
|
{
|
||||||
|
public string ConfigFile { get; set; }
|
||||||
|
public string WatcherConfigFile { get; set; }
|
||||||
|
public string WebPrefix { get; set; }
|
||||||
|
public string WebUser { get; set; }
|
||||||
|
public string WebPassword { get; set; }
|
||||||
|
|
||||||
|
public SchedulerOptions()
|
||||||
|
{
|
||||||
|
ConfigFile = "cronjobs.json";
|
||||||
|
WatcherConfigFile = "watcherjobs.json";
|
||||||
|
WebPrefix = "http://127.0.0.1:8095/";
|
||||||
|
WebUser = "";
|
||||||
|
WebPassword = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
FileinterfaceCronScheduler/Scheduler/CronJobExecutor.cs
Normal file
145
FileinterfaceCronScheduler/Scheduler/CronJobExecutor.cs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
public sealed class CronJobExecutor
|
||||||
|
{
|
||||||
|
private readonly ICustomFunctionInvoker _invoker;
|
||||||
|
private readonly JobLog _log;
|
||||||
|
|
||||||
|
public CronJobExecutor(ICustomFunctionInvoker invoker, JobLog log)
|
||||||
|
{
|
||||||
|
_invoker = invoker;
|
||||||
|
_log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(CronJobConfig job)
|
||||||
|
{
|
||||||
|
if (job.CallOnceWithoutFile)
|
||||||
|
{
|
||||||
|
ExecuteSingle(job, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(job.SourceDirectory))
|
||||||
|
{
|
||||||
|
_log.Error(job, "Source directory does not exist: " + job.SourceDirectory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchOption option = job.IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
||||||
|
string[] files;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
files = Directory.GetFiles(job.SourceDirectory, String.IsNullOrWhiteSpace(job.SearchPattern) ? "*.*" : job.SearchPattern, option)
|
||||||
|
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.Error(job, "Cannot enumerate source files: " + ex.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Info(job, "Found " + files.Length + " file(s).");
|
||||||
|
foreach (string file in files)
|
||||||
|
{
|
||||||
|
try { ExecuteSingle(job, file); }
|
||||||
|
catch (Exception ex) { _log.Error(job, "File failed: " + file + " - " + ex.Message); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteSingle(CronJobConfig job, string file)
|
||||||
|
{
|
||||||
|
string rawContent = "";
|
||||||
|
string processedContent = "";
|
||||||
|
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
// Opening with FileShare.ReadWrite prevents common producer/consumer lock issues.
|
||||||
|
using (var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||||
|
using (var reader = new StreamReader(stream, Encoding.UTF8, true))
|
||||||
|
rawContent = reader.ReadToEnd();
|
||||||
|
|
||||||
|
processedContent = ProcessContent(rawContent, job.RemoveEmptyLines, job.JoinSeparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] inArgs = (job.InArgs ?? new List<string>())
|
||||||
|
.Select(arg => Expand(arg, job, file, rawContent, processedContent))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
_log.Info(job, "Calling CustomFunction '" + job.CustomFunction + "' with " + inArgs.Length + " inArg(s)" + (file == null ? "." : " for " + Path.GetFileName(file) + "."));
|
||||||
|
CustomFunctionCallResult result = _invoker.Invoke(job.CustomFunction, inArgs);
|
||||||
|
if (result == null) result = CustomFunctionCallResult.Fail("Invoker returned null.");
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
_log.Error(job, "CustomFunction failed: " + result.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Info(job, "CustomFunction succeeded: " + result.Message);
|
||||||
|
if (file != null && job.MoveFileAfterSuccess)
|
||||||
|
{
|
||||||
|
string movedTo = MoveProcessedFile(file, job.MoveTargetDirectory, job.OverwriteTargetFile);
|
||||||
|
_log.Info(job, "Moved file to: " + movedTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ProcessContent(string text, bool removeEmptyLines, string separator)
|
||||||
|
{
|
||||||
|
if (!removeEmptyLines) return text ?? "";
|
||||||
|
string[] lines = (text ?? "").Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
|
||||||
|
return String.Join(separator ?? ";", lines.Where(l => !String.IsNullOrWhiteSpace(l)).Select(l => l.Trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Expand(string template, CronJobConfig job, string file, string rawContent, string processedContent)
|
||||||
|
{
|
||||||
|
string value = template ?? "";
|
||||||
|
string fullPath = file ?? "";
|
||||||
|
string fileName = file == null ? "" : Path.GetFileName(file);
|
||||||
|
string extension = file == null ? "" : Path.GetExtension(file);
|
||||||
|
string directory = file == null ? "" : Path.GetDirectoryName(file);
|
||||||
|
|
||||||
|
return value
|
||||||
|
.Replace("{{CONTENT}}", processedContent ?? "")
|
||||||
|
.Replace("{{RAW_CONTENT}}", rawContent ?? "")
|
||||||
|
.Replace("{{FILE_NAME}}", fileName ?? "")
|
||||||
|
.Replace("{{FILE_PATH}}", fullPath)
|
||||||
|
.Replace("{{FILE_DIR}}", directory ?? "")
|
||||||
|
.Replace("{{FILE_EXT}}", extension ?? "")
|
||||||
|
.Replace("{{JOB_NAME}}", job.Name ?? "")
|
||||||
|
.Replace("{{NOW}}", DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffK"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string MoveProcessedFile(string source, string targetDirectory, bool overwrite)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(targetDirectory);
|
||||||
|
string target = Path.Combine(targetDirectory, Path.GetFileName(source));
|
||||||
|
|
||||||
|
if (File.Exists(target))
|
||||||
|
{
|
||||||
|
if (overwrite) File.Delete(target);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string name = Path.GetFileNameWithoutExtension(source);
|
||||||
|
string ext = Path.GetExtension(source);
|
||||||
|
target = Path.Combine(targetDirectory, name + "_" + DateTime.Now.ToString("yyyyMMdd_HHmmss_fff") + ext);
|
||||||
|
int i = 1;
|
||||||
|
while (File.Exists(target))
|
||||||
|
{
|
||||||
|
target = Path.Combine(targetDirectory, name + "_" + DateTime.Now.ToString("yyyyMMdd_HHmmss_fff") + "_" + i + ext);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Move(source, target);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
FileinterfaceCronScheduler/Scheduler/CronJobRepository.cs
Normal file
120
FileinterfaceCronScheduler/Scheduler/CronJobRepository.cs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Web.Script.Serialization;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
public sealed class CronJobRepository
|
||||||
|
{
|
||||||
|
private readonly object _sync = new object();
|
||||||
|
private readonly string _fileName;
|
||||||
|
private readonly JavaScriptSerializer _serializer = new JavaScriptSerializer();
|
||||||
|
private List<CronJobConfig> _jobs;
|
||||||
|
|
||||||
|
public CronJobRepository(string fileName)
|
||||||
|
{
|
||||||
|
_fileName = Path.GetFullPath(fileName);
|
||||||
|
_serializer.MaxJsonLength = Int32.MaxValue;
|
||||||
|
_jobs = LoadInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<CronJobConfig> GetAll()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
// Clone through JSON so callers cannot mutate repository state behind our lock.
|
||||||
|
return Clone(_jobs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CronJobConfig GetById(string id)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
CronJobConfig job = _jobs.FirstOrDefault(j => String.Equals(j.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
return job == null ? null : Clone(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CronJobConfig Save(CronJobConfig job)
|
||||||
|
{
|
||||||
|
if (job == null) throw new ArgumentNullException("job");
|
||||||
|
Validate(job);
|
||||||
|
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrWhiteSpace(job.Id))
|
||||||
|
job.Id = Guid.NewGuid().ToString("N");
|
||||||
|
|
||||||
|
int index = _jobs.FindIndex(j => String.Equals(j.Id, job.Id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (index >= 0) _jobs[index] = Clone(job);
|
||||||
|
else _jobs.Add(Clone(job));
|
||||||
|
|
||||||
|
SaveInternal();
|
||||||
|
return Clone(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Delete(string id)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
int count = _jobs.RemoveAll(j => String.Equals(j.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (count > 0) SaveInternal();
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CronJobConfig> LoadInternal()
|
||||||
|
{
|
||||||
|
if (!File.Exists(_fileName)) return new List<CronJobConfig>();
|
||||||
|
string json = File.ReadAllText(_fileName, Encoding.UTF8);
|
||||||
|
if (String.IsNullOrWhiteSpace(json)) return new List<CronJobConfig>();
|
||||||
|
return _serializer.Deserialize<List<CronJobConfig>>(json) ?? new List<CronJobConfig>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveInternal()
|
||||||
|
{
|
||||||
|
string dir = Path.GetDirectoryName(_fileName);
|
||||||
|
if (!String.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
|
string json = _serializer.Serialize(_jobs);
|
||||||
|
string tmp = _fileName + ".tmp";
|
||||||
|
File.WriteAllText(tmp, json, new UTF8Encoding(false));
|
||||||
|
|
||||||
|
if (File.Exists(_fileName))
|
||||||
|
{
|
||||||
|
string backup = _fileName + ".bak";
|
||||||
|
try { File.Replace(tmp, _fileName, backup, true); }
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
File.Delete(_fileName);
|
||||||
|
File.Move(tmp, _fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else File.Move(tmp, _fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Validate(CronJobConfig job)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrWhiteSpace(job.Name)) throw new InvalidOperationException("Name is required.");
|
||||||
|
CronExpression.Parse(job.Cron);
|
||||||
|
if (String.IsNullOrWhiteSpace(job.CustomFunction)) throw new InvalidOperationException("CustomFunction is required.");
|
||||||
|
if (!job.CallOnceWithoutFile && String.IsNullOrWhiteSpace(job.SourceDirectory))
|
||||||
|
throw new InvalidOperationException("SourceDirectory is required for file jobs.");
|
||||||
|
if (job.MoveFileAfterSuccess && String.IsNullOrWhiteSpace(job.MoveTargetDirectory))
|
||||||
|
throw new InvalidOperationException("MoveTargetDirectory is required when MoveFileAfterSuccess=true.");
|
||||||
|
if (job.InArgs == null) job.InArgs = new List<string>();
|
||||||
|
if (String.IsNullOrWhiteSpace(job.SearchPattern)) job.SearchPattern = "*.*";
|
||||||
|
if (job.JoinSeparator == null) job.JoinSeparator = ";";
|
||||||
|
}
|
||||||
|
|
||||||
|
private T Clone<T>(T value)
|
||||||
|
{
|
||||||
|
return _serializer.Deserialize<T>(_serializer.Serialize(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
FileinterfaceCronScheduler/Scheduler/CronScheduler.cs
Normal file
106
FileinterfaceCronScheduler/Scheduler/CronScheduler.cs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
public sealed class CronScheduler : IDisposable
|
||||||
|
{
|
||||||
|
private readonly CronJobRepository _repository;
|
||||||
|
private readonly CronJobExecutor _executor;
|
||||||
|
private readonly JobLog _log;
|
||||||
|
private readonly ConcurrentDictionary<string, byte> _running = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly ConcurrentDictionary<string, string> _lastMinuteRun = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private Timer _timer;
|
||||||
|
private int _checking;
|
||||||
|
|
||||||
|
public CronScheduler(CronJobRepository repository, CronJobExecutor executor, JobLog log)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
_executor = executor;
|
||||||
|
_log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (_timer != null) return;
|
||||||
|
_timer = new Timer(CheckDueJobs, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RunNow(string jobId)
|
||||||
|
{
|
||||||
|
CronJobConfig job = _repository.GetById(jobId);
|
||||||
|
if (job == null) return false;
|
||||||
|
QueueExecution(job, "manual");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<string> GetRunningJobIds()
|
||||||
|
{
|
||||||
|
return new List<string>(_running.Keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckDueJobs(object state)
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _checking, 1) == 1) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DateTime now = DateTime.Now;
|
||||||
|
string minuteStamp = now.ToString("yyyyMMddHHmm");
|
||||||
|
foreach (CronJobConfig job in _repository.GetAll())
|
||||||
|
{
|
||||||
|
if (!job.Enabled) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CronExpression cron = CronExpression.Parse(job.Cron);
|
||||||
|
if (!cron.IsMatch(now)) continue;
|
||||||
|
|
||||||
|
string previous;
|
||||||
|
if (_lastMinuteRun.TryGetValue(job.Id, out previous) && previous == minuteStamp) continue;
|
||||||
|
_lastMinuteRun[job.Id] = minuteStamp;
|
||||||
|
QueueExecution(job, "cron " + job.Cron);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.Error(job, "Invalid cron/configuration: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { Interlocked.Exchange(ref _checking, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void QueueExecution(CronJobConfig job, string reason)
|
||||||
|
{
|
||||||
|
byte ignored;
|
||||||
|
if (!_running.TryAdd(job.Id, 0))
|
||||||
|
{
|
||||||
|
_log.Info(job, "Skipped " + reason + " start because this job is still running.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_log.Info(job, "Job started (" + reason + ").");
|
||||||
|
_executor.Execute(job);
|
||||||
|
_log.Info(job, "Job finished.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.Error(job, "Job crashed: " + ex);
|
||||||
|
}
|
||||||
|
finally { _running.TryRemove(job.Id, out ignored); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Timer timer = _timer;
|
||||||
|
_timer = null;
|
||||||
|
if (timer != null) timer.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
public sealed class CustomFunctionCallResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
|
||||||
|
public static CustomFunctionCallResult Ok(string message)
|
||||||
|
{
|
||||||
|
return new CustomFunctionCallResult { Success = true, Message = message ?? "OK" };
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CustomFunctionCallResult Fail(string message)
|
||||||
|
{
|
||||||
|
return new CustomFunctionCallResult { Success = false, Message = message ?? "CustomFunction failed" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ICustomFunctionInvoker
|
||||||
|
{
|
||||||
|
CustomFunctionCallResult Invoke(string customFunctionName, string[] inArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lets the existing FileInterface IMS code remain untouched. Wire the current
|
||||||
|
/// IMS CustomFunction call into this delegate in Program.cs / service startup.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DelegateCustomFunctionInvoker : ICustomFunctionInvoker
|
||||||
|
{
|
||||||
|
private readonly Func<string, string[], CustomFunctionCallResult> _handler;
|
||||||
|
|
||||||
|
public DelegateCustomFunctionInvoker(Func<string, string[], CustomFunctionCallResult> handler)
|
||||||
|
{
|
||||||
|
if (handler == null) throw new ArgumentNullException("handler");
|
||||||
|
_handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomFunctionCallResult Invoke(string customFunctionName, string[] inArgs)
|
||||||
|
{
|
||||||
|
return _handler(customFunctionName, inArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
172
FileinterfaceCronScheduler/Scheduler/FileSystemWatcherManager.cs
Normal file
172
FileinterfaceCronScheduler/Scheduler/FileSystemWatcherManager.cs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
public sealed class FileSystemWatcherManager : IDisposable
|
||||||
|
{
|
||||||
|
private sealed class ActiveWatcher : IDisposable
|
||||||
|
{
|
||||||
|
public WatcherJobConfig Job;
|
||||||
|
public FileSystemWatcher Watcher;
|
||||||
|
public void Dispose() { if (Watcher != null) { Watcher.EnableRaisingEvents = false; Watcher.Dispose(); } }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object _sync = new object();
|
||||||
|
private readonly WatcherJobRepository _repository;
|
||||||
|
private readonly WatcherJobExecutor _executor;
|
||||||
|
private readonly JobLog _log;
|
||||||
|
private readonly Dictionary<string, ActiveWatcher> _watchers = new Dictionary<string, ActiveWatcher>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly ConcurrentDictionary<string, DateTime> _lastEvents = new ConcurrentDictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly ConcurrentDictionary<string, byte> _runningFiles = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public FileSystemWatcherManager(WatcherJobRepository repository, WatcherJobExecutor executor, JobLog log)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
_executor = executor;
|
||||||
|
_log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start() { Reload(); }
|
||||||
|
|
||||||
|
public void Reload()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
foreach (ActiveWatcher old in _watchers.Values) old.Dispose();
|
||||||
|
_watchers.Clear();
|
||||||
|
|
||||||
|
foreach (WatcherJobConfig job in _repository.GetAll())
|
||||||
|
{
|
||||||
|
if (!job.Enabled) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(job.SourceDirectory))
|
||||||
|
{
|
||||||
|
_log.Error(job, "Watcher source directory does not exist: " + job.SourceDirectory);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fsw = new FileSystemWatcher(job.SourceDirectory, String.IsNullOrWhiteSpace(job.SearchPattern) ? "*.*" : job.SearchPattern)
|
||||||
|
{
|
||||||
|
IncludeSubdirectories = job.IncludeSubdirectories,
|
||||||
|
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.CreationTime,
|
||||||
|
EnableRaisingEvents = false
|
||||||
|
};
|
||||||
|
|
||||||
|
WatcherJobConfig captured = job;
|
||||||
|
if (job.WatchCreated) fsw.Created += (s, e) => OnFileEvent(captured, e.FullPath, "Created");
|
||||||
|
if (job.WatchChanged) fsw.Changed += (s, e) => OnFileEvent(captured, e.FullPath, "Changed");
|
||||||
|
if (job.WatchRenamed) fsw.Renamed += (s, e) => OnFileEvent(captured, e.FullPath, "Renamed");
|
||||||
|
fsw.Error += (s, e) => _log.Error(captured, "FileSystemWatcher error: " + (e.GetException() == null ? "unknown" : e.GetException().Message));
|
||||||
|
|
||||||
|
var active = new ActiveWatcher { Job = job, Watcher = fsw };
|
||||||
|
_watchers[job.Id] = active;
|
||||||
|
fsw.EnableRaisingEvents = true;
|
||||||
|
_log.Info(job, "FileSystemWatcher active: " + job.SourceDirectory + "\\" + job.SearchPattern);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.Error(job, "Cannot start FileSystemWatcher: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<string> GetActiveWatcherIds()
|
||||||
|
{
|
||||||
|
lock (_sync) return _watchers.Keys.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ProcessExistingNow(string watcherId)
|
||||||
|
{
|
||||||
|
WatcherJobConfig job = _repository.GetById(watcherId);
|
||||||
|
if (job == null) return false;
|
||||||
|
Task.Run(() => ProcessExisting(job));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessExisting(WatcherJobConfig job)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(job.SourceDirectory))
|
||||||
|
{
|
||||||
|
_log.Error(job, "Source directory does not exist: " + job.SourceDirectory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SearchOption option = job.IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
||||||
|
string[] files = Directory.GetFiles(job.SourceDirectory, String.IsNullOrWhiteSpace(job.SearchPattern) ? "*.*" : job.SearchPattern, option)
|
||||||
|
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||||
|
_log.Info(job, "Manual watcher scan found " + files.Length + " file(s).");
|
||||||
|
foreach (string file in files) QueueFile(job, file, "ManualScan", false);
|
||||||
|
}
|
||||||
|
catch (Exception ex) { _log.Error(job, "Manual watcher scan failed: " + ex.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFileEvent(WatcherJobConfig job, string fullPath, string reason)
|
||||||
|
{
|
||||||
|
if (!job.Enabled || String.IsNullOrWhiteSpace(fullPath)) return;
|
||||||
|
if (IsMoveTarget(job, fullPath)) return;
|
||||||
|
QueueFile(job, fullPath, reason, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void QueueFile(WatcherJobConfig job, string fullPath, string reason, bool debounce)
|
||||||
|
{
|
||||||
|
string key = job.Id + "|" + fullPath;
|
||||||
|
DateTime now = DateTime.UtcNow;
|
||||||
|
if (debounce)
|
||||||
|
{
|
||||||
|
DateTime previous;
|
||||||
|
if (_lastEvents.TryGetValue(key, out previous) && (now - previous).TotalMilliseconds < Math.Max(0, job.DebounceMilliseconds))
|
||||||
|
return;
|
||||||
|
_lastEvents[key] = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte ignored;
|
||||||
|
if (!_runningFiles.TryAdd(key, 0)) return;
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(fullPath)) return;
|
||||||
|
_log.Info(job, "File event queued: " + reason + " - " + fullPath);
|
||||||
|
_executor.ExecuteFile(job, fullPath, reason);
|
||||||
|
}
|
||||||
|
catch (Exception ex) { _log.Error(job, "Watcher processing failed for " + fullPath + ": " + ex); }
|
||||||
|
finally { _runningFiles.TryRemove(key, out ignored); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsMoveTarget(WatcherJobConfig job, string fullPath)
|
||||||
|
{
|
||||||
|
if (!job.MoveFileAfterSuccess || String.IsNullOrWhiteSpace(job.MoveTargetDirectory)) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string file = Path.GetFullPath(fullPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
|
string target = Path.GetFullPath(job.MoveTargetDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
|
return file.StartsWith(target, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowIfDisposed() { if (_disposed) throw new ObjectDisposedException(GetType().FullName); }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
foreach (ActiveWatcher w in _watchers.Values) w.Dispose();
|
||||||
|
_watchers.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
FileinterfaceCronScheduler/Scheduler/JobLog.cs
Normal file
60
FileinterfaceCronScheduler/Scheduler/JobLog.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
public sealed class JobLogEntry
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
public string JobType { get; set; }
|
||||||
|
public string JobId { get; set; }
|
||||||
|
public string JobName { get; set; }
|
||||||
|
public string Level { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class JobLog
|
||||||
|
{
|
||||||
|
private readonly object _sync = new object();
|
||||||
|
private readonly Queue<JobLogEntry> _entries = new Queue<JobLogEntry>();
|
||||||
|
private readonly int _maxEntries;
|
||||||
|
|
||||||
|
public JobLog(int maxEntries)
|
||||||
|
{
|
||||||
|
_maxEntries = Math.Max(100, maxEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Info(CronJobConfig job, string message) { Add("CRON", job == null ? "" : job.Id, job == null ? "" : job.Name, "INFO", message); }
|
||||||
|
public void Error(CronJobConfig job, string message) { Add("CRON", job == null ? "" : job.Id, job == null ? "" : job.Name, "ERROR", message); }
|
||||||
|
public void Info(WatcherJobConfig job, string message) { Add("WATCHER", job == null ? "" : job.Id, job == null ? "" : job.Name, "INFO", message); }
|
||||||
|
public void Error(WatcherJobConfig job, string message) { Add("WATCHER", job == null ? "" : job.Id, job == null ? "" : job.Name, "ERROR", message); }
|
||||||
|
public void SystemInfo(string message) { Add("SYSTEM", "", "System", "INFO", message); }
|
||||||
|
public void SystemError(string message) { Add("SYSTEM", "", "System", "ERROR", message); }
|
||||||
|
|
||||||
|
public IList<JobLogEntry> GetLatest(int limit)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
return _entries.Reverse().Take(Math.Max(1, Math.Min(limit, _maxEntries))).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Add(string jobType, string jobId, string jobName, string level, string message)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_entries.Enqueue(new JobLogEntry
|
||||||
|
{
|
||||||
|
Timestamp = DateTime.Now,
|
||||||
|
JobType = jobType ?? "",
|
||||||
|
JobId = jobId ?? "",
|
||||||
|
JobName = jobName ?? "",
|
||||||
|
Level = level,
|
||||||
|
Message = message ?? ""
|
||||||
|
});
|
||||||
|
while (_entries.Count > _maxEntries) _entries.Dequeue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
FileinterfaceCronScheduler/Scheduler/SchedulerRuntime.cs
Normal file
57
FileinterfaceCronScheduler/Scheduler/SchedulerRuntime.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
public sealed class SchedulerRuntime : IDisposable
|
||||||
|
{
|
||||||
|
private readonly CronScheduler _scheduler;
|
||||||
|
private readonly FileSystemWatcherManager _watcherManager;
|
||||||
|
private readonly WebAdminServer _web;
|
||||||
|
private readonly IDisposable _ownedResource;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public SchedulerRuntime(ICustomFunctionInvoker invoker, SchedulerOptions options)
|
||||||
|
: this(invoker, options, null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public SchedulerRuntime(ICustomFunctionInvoker invoker, SchedulerOptions options, IDisposable ownedResource)
|
||||||
|
{
|
||||||
|
if (invoker == null) throw new ArgumentNullException("invoker");
|
||||||
|
if (options == null) options = new SchedulerOptions();
|
||||||
|
|
||||||
|
_ownedResource = ownedResource;
|
||||||
|
var cronRepository = new CronJobRepository(options.ConfigFile);
|
||||||
|
var watcherRepository = new WatcherJobRepository(options.WatcherConfigFile);
|
||||||
|
var log = new JobLog(5000);
|
||||||
|
var cronExecutor = new CronJobExecutor(invoker, log);
|
||||||
|
var watcherExecutor = new WatcherJobExecutor(invoker, log);
|
||||||
|
_scheduler = new CronScheduler(cronRepository, cronExecutor, log);
|
||||||
|
_watcherManager = new FileSystemWatcherManager(watcherRepository, watcherExecutor, log);
|
||||||
|
_web = new WebAdminServer(cronRepository, _scheduler, watcherRepository, _watcherManager, log, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_scheduler.Start();
|
||||||
|
_watcherManager.Start();
|
||||||
|
_web.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
try { _web.Dispose(); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { _watcherManager.Dispose(); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { _scheduler.Dispose(); }
|
||||||
|
finally { if (_ownedResource != null) _ownedResource.Dispose(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
FileinterfaceCronScheduler/Scheduler/WatcherJobConfig.cs
Normal file
56
FileinterfaceCronScheduler/Scheduler/WatcherJobConfig.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
public sealed class WatcherJobConfig
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
|
public string SourceDirectory { get; set; }
|
||||||
|
public string SearchPattern { get; set; }
|
||||||
|
public bool IncludeSubdirectories { get; set; }
|
||||||
|
|
||||||
|
public bool WatchCreated { get; set; }
|
||||||
|
public bool WatchChanged { get; set; }
|
||||||
|
public bool WatchRenamed { get; set; }
|
||||||
|
public int DebounceMilliseconds { get; set; }
|
||||||
|
public int FileReadyRetries { get; set; }
|
||||||
|
public int FileReadyDelayMilliseconds { get; set; }
|
||||||
|
|
||||||
|
public bool RemoveEmptyLines { get; set; }
|
||||||
|
public string JoinSeparator { get; set; }
|
||||||
|
|
||||||
|
public string CustomFunction { get; set; }
|
||||||
|
public List<string> InArgs { get; set; }
|
||||||
|
|
||||||
|
public bool MoveFileAfterSuccess { get; set; }
|
||||||
|
public string MoveTargetDirectory { get; set; }
|
||||||
|
public bool OverwriteTargetFile { get; set; }
|
||||||
|
|
||||||
|
public WatcherJobConfig()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString("N");
|
||||||
|
Name = "New watcher";
|
||||||
|
Enabled = true;
|
||||||
|
SourceDirectory = "";
|
||||||
|
SearchPattern = "*.*";
|
||||||
|
IncludeSubdirectories = false;
|
||||||
|
WatchCreated = true;
|
||||||
|
WatchChanged = true;
|
||||||
|
WatchRenamed = true;
|
||||||
|
DebounceMilliseconds = 1000;
|
||||||
|
FileReadyRetries = 10;
|
||||||
|
FileReadyDelayMilliseconds = 250;
|
||||||
|
RemoveEmptyLines = true;
|
||||||
|
JoinSeparator = ";";
|
||||||
|
CustomFunction = "";
|
||||||
|
InArgs = new List<string>();
|
||||||
|
MoveFileAfterSuccess = false;
|
||||||
|
MoveTargetDirectory = "";
|
||||||
|
OverwriteTargetFile = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
149
FileinterfaceCronScheduler/Scheduler/WatcherJobExecutor.cs
Normal file
149
FileinterfaceCronScheduler/Scheduler/WatcherJobExecutor.cs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
public sealed class WatcherJobExecutor
|
||||||
|
{
|
||||||
|
private readonly ICustomFunctionInvoker _invoker;
|
||||||
|
private readonly JobLog _log;
|
||||||
|
|
||||||
|
public WatcherJobExecutor(ICustomFunctionInvoker invoker, JobLog log)
|
||||||
|
{
|
||||||
|
_invoker = invoker;
|
||||||
|
_log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecuteFile(WatcherJobConfig job, string file, string reason)
|
||||||
|
{
|
||||||
|
if (job == null) throw new ArgumentNullException("job");
|
||||||
|
if (String.IsNullOrWhiteSpace(file)) return;
|
||||||
|
if (!WaitUntilReadable(file, job.FileReadyRetries, job.FileReadyDelayMilliseconds))
|
||||||
|
{
|
||||||
|
_log.Error(job, "File is not readable after retries: " + file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string rawContent;
|
||||||
|
using (var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||||
|
using (var reader = new StreamReader(stream, Encoding.UTF8, true))
|
||||||
|
rawContent = reader.ReadToEnd();
|
||||||
|
|
||||||
|
string processedContent = ProcessContent(rawContent, job.RemoveEmptyLines, job.JoinSeparator);
|
||||||
|
string[] inArgs = (job.InArgs ?? new List<string>())
|
||||||
|
.Select(arg => Expand(arg, job, file, rawContent, processedContent, reason))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
_log.Info(job, "Calling CustomFunction '" + job.CustomFunction + "' with " + inArgs.Length + " inArg(s) for " + Path.GetFileName(file) + " (" + reason + ").");
|
||||||
|
CustomFunctionCallResult result = _invoker.Invoke(job.CustomFunction, inArgs);
|
||||||
|
if (result == null) result = CustomFunctionCallResult.Fail("Invoker returned null.");
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
_log.Error(job, "CustomFunction failed: " + result.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Info(job, "CustomFunction succeeded: " + result.Message);
|
||||||
|
if (job.MoveFileAfterSuccess && File.Exists(file))
|
||||||
|
{
|
||||||
|
string movedTo = MoveProcessedFile(file, job.MoveTargetDirectory, job.OverwriteTargetFile);
|
||||||
|
_log.Info(job, "Moved file to: " + movedTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool WaitUntilReadable(string file, int retries, int delayMs)
|
||||||
|
{
|
||||||
|
retries = Math.Max(2, retries);
|
||||||
|
delayMs = Math.Max(10, delayMs);
|
||||||
|
long previousLength = -1;
|
||||||
|
DateTime previousWrite = DateTime.MinValue;
|
||||||
|
int stableChecks = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < retries; i++)
|
||||||
|
{
|
||||||
|
if (!File.Exists(file)) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = new FileInfo(file);
|
||||||
|
long length = info.Length;
|
||||||
|
DateTime lastWrite = info.LastWriteTimeUtc;
|
||||||
|
using (var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||||
|
{
|
||||||
|
if (stream.Length != length) length = stream.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length == previousLength && lastWrite == previousWrite)
|
||||||
|
{
|
||||||
|
stableChecks++;
|
||||||
|
if (stableChecks >= 1) return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stableChecks = 0;
|
||||||
|
previousLength = length;
|
||||||
|
previousWrite = lastWrite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException) { stableChecks = 0; }
|
||||||
|
catch (UnauthorizedAccessException) { stableChecks = 0; }
|
||||||
|
|
||||||
|
Thread.Sleep(delayMs);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ProcessContent(string text, bool removeEmptyLines, string separator)
|
||||||
|
{
|
||||||
|
if (!removeEmptyLines) return text ?? "";
|
||||||
|
string[] lines = (text ?? "").Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
|
||||||
|
return String.Join(separator ?? ";", lines.Where(l => !String.IsNullOrWhiteSpace(l)).Select(l => l.Trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Expand(string template, WatcherJobConfig job, string file, string rawContent, string processedContent, string reason)
|
||||||
|
{
|
||||||
|
string value = template ?? "";
|
||||||
|
string fileName = Path.GetFileName(file) ?? "";
|
||||||
|
string extension = Path.GetExtension(file) ?? "";
|
||||||
|
string directory = Path.GetDirectoryName(file) ?? "";
|
||||||
|
return value
|
||||||
|
.Replace("{{CONTENT}}", processedContent ?? "")
|
||||||
|
.Replace("{{RAW_CONTENT}}", rawContent ?? "")
|
||||||
|
.Replace("{{FILE_NAME}}", fileName)
|
||||||
|
.Replace("{{FILE_PATH}}", file ?? "")
|
||||||
|
.Replace("{{FILE_DIR}}", directory)
|
||||||
|
.Replace("{{FILE_EXT}}", extension)
|
||||||
|
.Replace("{{JOB_NAME}}", job.Name ?? "")
|
||||||
|
.Replace("{{EVENT}}", reason ?? "")
|
||||||
|
.Replace("{{NOW}}", DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffK"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string MoveProcessedFile(string source, string targetDirectory, bool overwrite)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(targetDirectory);
|
||||||
|
string target = Path.Combine(targetDirectory, Path.GetFileName(source));
|
||||||
|
if (File.Exists(target))
|
||||||
|
{
|
||||||
|
if (overwrite) File.Delete(target);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string name = Path.GetFileNameWithoutExtension(source);
|
||||||
|
string ext = Path.GetExtension(source);
|
||||||
|
target = Path.Combine(targetDirectory, name + "_" + DateTime.Now.ToString("yyyyMMdd_HHmmss_fff") + ext);
|
||||||
|
int i = 1;
|
||||||
|
while (File.Exists(target))
|
||||||
|
{
|
||||||
|
target = Path.Combine(targetDirectory, name + "_" + DateTime.Now.ToString("yyyyMMdd_HHmmss_fff") + "_" + i + ext);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File.Move(source, target);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
FileinterfaceCronScheduler/Scheduler/WatcherJobRepository.cs
Normal file
109
FileinterfaceCronScheduler/Scheduler/WatcherJobRepository.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Web.Script.Serialization;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
public sealed class WatcherJobRepository
|
||||||
|
{
|
||||||
|
private readonly object _sync = new object();
|
||||||
|
private readonly string _fileName;
|
||||||
|
private readonly JavaScriptSerializer _serializer = new JavaScriptSerializer();
|
||||||
|
private List<WatcherJobConfig> _jobs;
|
||||||
|
|
||||||
|
public WatcherJobRepository(string fileName)
|
||||||
|
{
|
||||||
|
_fileName = Path.GetFullPath(fileName);
|
||||||
|
_serializer.MaxJsonLength = Int32.MaxValue;
|
||||||
|
_jobs = LoadInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<WatcherJobConfig> GetAll()
|
||||||
|
{
|
||||||
|
lock (_sync) return Clone(_jobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WatcherJobConfig GetById(string id)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
WatcherJobConfig job = _jobs.FirstOrDefault(j => String.Equals(j.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
return job == null ? null : Clone(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public WatcherJobConfig Save(WatcherJobConfig job)
|
||||||
|
{
|
||||||
|
if (job == null) throw new ArgumentNullException("job");
|
||||||
|
Validate(job);
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrWhiteSpace(job.Id)) job.Id = Guid.NewGuid().ToString("N");
|
||||||
|
int index = _jobs.FindIndex(j => String.Equals(j.Id, job.Id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (index >= 0) _jobs[index] = Clone(job); else _jobs.Add(Clone(job));
|
||||||
|
SaveInternal();
|
||||||
|
return Clone(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Delete(string id)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
int count = _jobs.RemoveAll(j => String.Equals(j.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (count > 0) SaveInternal();
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<WatcherJobConfig> LoadInternal()
|
||||||
|
{
|
||||||
|
if (!File.Exists(_fileName)) return new List<WatcherJobConfig>();
|
||||||
|
string json = File.ReadAllText(_fileName, Encoding.UTF8);
|
||||||
|
if (String.IsNullOrWhiteSpace(json)) return new List<WatcherJobConfig>();
|
||||||
|
return _serializer.Deserialize<List<WatcherJobConfig>>(json) ?? new List<WatcherJobConfig>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveInternal()
|
||||||
|
{
|
||||||
|
string dir = Path.GetDirectoryName(_fileName);
|
||||||
|
if (!String.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir);
|
||||||
|
string json = _serializer.Serialize(_jobs);
|
||||||
|
string tmp = _fileName + ".tmp";
|
||||||
|
File.WriteAllText(tmp, json, new UTF8Encoding(false));
|
||||||
|
if (File.Exists(_fileName))
|
||||||
|
{
|
||||||
|
string backup = _fileName + ".bak";
|
||||||
|
try { File.Replace(tmp, _fileName, backup, true); }
|
||||||
|
catch { File.Delete(_fileName); File.Move(tmp, _fileName); }
|
||||||
|
}
|
||||||
|
else File.Move(tmp, _fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Validate(WatcherJobConfig job)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrWhiteSpace(job.Name)) throw new InvalidOperationException("Name is required.");
|
||||||
|
if (String.IsNullOrWhiteSpace(job.SourceDirectory)) throw new InvalidOperationException("SourceDirectory is required.");
|
||||||
|
if (String.IsNullOrWhiteSpace(job.CustomFunction)) throw new InvalidOperationException("CustomFunction is required.");
|
||||||
|
if (!job.WatchCreated && !job.WatchChanged && !job.WatchRenamed)
|
||||||
|
throw new InvalidOperationException("At least one watcher event (Created, Changed, Renamed) must be enabled.");
|
||||||
|
if (job.MoveFileAfterSuccess && String.IsNullOrWhiteSpace(job.MoveTargetDirectory))
|
||||||
|
throw new InvalidOperationException("MoveTargetDirectory is required when MoveFileAfterSuccess=true.");
|
||||||
|
if (job.InArgs == null) job.InArgs = new List<string>();
|
||||||
|
if (String.IsNullOrWhiteSpace(job.SearchPattern)) job.SearchPattern = "*.*";
|
||||||
|
if (job.JoinSeparator == null) job.JoinSeparator = ";";
|
||||||
|
if (job.DebounceMilliseconds < 0) job.DebounceMilliseconds = 0;
|
||||||
|
if (job.FileReadyRetries < 1) job.FileReadyRetries = 1;
|
||||||
|
if (job.FileReadyRetries > 1000) job.FileReadyRetries = 1000;
|
||||||
|
if (job.FileReadyDelayMilliseconds < 10) job.FileReadyDelayMilliseconds = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
private T Clone<T>(T value)
|
||||||
|
{
|
||||||
|
return _serializer.Deserialize<T>(_serializer.Serialize(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
278
FileinterfaceCronScheduler/Scheduler/WebAdminServer.cs
Normal file
278
FileinterfaceCronScheduler/Scheduler/WebAdminServer.cs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web.Script.Serialization;
|
||||||
|
|
||||||
|
namespace FileInterface.Scheduler
|
||||||
|
{
|
||||||
|
public sealed class WebAdminServer : IDisposable
|
||||||
|
{
|
||||||
|
private readonly HttpListener _listener = new HttpListener();
|
||||||
|
private readonly CronJobRepository _cronRepository;
|
||||||
|
private readonly CronScheduler _scheduler;
|
||||||
|
private readonly WatcherJobRepository _watcherRepository;
|
||||||
|
private readonly FileSystemWatcherManager _watcherManager;
|
||||||
|
private readonly JobLog _log;
|
||||||
|
private readonly SchedulerOptions _options;
|
||||||
|
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
|
||||||
|
private CancellationTokenSource _cts;
|
||||||
|
|
||||||
|
public WebAdminServer(
|
||||||
|
CronJobRepository cronRepository,
|
||||||
|
CronScheduler scheduler,
|
||||||
|
WatcherJobRepository watcherRepository,
|
||||||
|
FileSystemWatcherManager watcherManager,
|
||||||
|
JobLog log,
|
||||||
|
SchedulerOptions options)
|
||||||
|
{
|
||||||
|
_cronRepository = cronRepository;
|
||||||
|
_scheduler = scheduler;
|
||||||
|
_watcherRepository = watcherRepository;
|
||||||
|
_watcherManager = watcherManager;
|
||||||
|
_log = log;
|
||||||
|
_options = options;
|
||||||
|
_listener.Prefixes.Add(options.WebPrefix);
|
||||||
|
_json.MaxJsonLength = Int32.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (_listener.IsListening) return;
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
_listener.Start();
|
||||||
|
Task.Run(() => AcceptLoop(_cts.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AcceptLoop(CancellationToken token)
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
HttpListenerContext context;
|
||||||
|
try { context = await _listener.GetContextAsync(); }
|
||||||
|
catch when (token.IsCancellationRequested) { break; }
|
||||||
|
catch { continue; }
|
||||||
|
Task.Run(() => Handle(context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Handle(HttpListenerContext ctx)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!IsAuthorized(ctx))
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 401;
|
||||||
|
ctx.Response.AddHeader("WWW-Authenticate", "Basic realm=FileInterface Admin");
|
||||||
|
ctx.Response.Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string path = ctx.Request.Url.AbsolutePath.TrimEnd('/');
|
||||||
|
if (path == "") path = "/";
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "GET" && path == "/") { WriteHtml(ctx, Html); return; }
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "GET" && (path == "/api/cronjobs" || path == "/api/jobs"))
|
||||||
|
{ WriteJson(ctx, _cronRepository.GetAll()); return; }
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "POST" && (path == "/api/cronjobs/save" || path == "/api/jobs/save"))
|
||||||
|
{
|
||||||
|
CronJobConfig job = _json.Deserialize<CronJobConfig>(ReadBody(ctx.Request));
|
||||||
|
WriteJson(ctx, _cronRepository.Save(job)); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "POST" && (path == "/api/cronjobs/delete" || path == "/api/jobs/delete"))
|
||||||
|
{ WriteJson(ctx, new { deleted = _cronRepository.Delete(ctx.Request.QueryString["id"] ?? "") }); return; }
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "POST" && (path == "/api/cronjobs/run" || path == "/api/jobs/run"))
|
||||||
|
{ WriteJson(ctx, new { started = _scheduler.RunNow(ctx.Request.QueryString["id"] ?? "") }); return; }
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "GET" && path == "/api/watchers")
|
||||||
|
{ WriteJson(ctx, _watcherRepository.GetAll()); return; }
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "POST" && path == "/api/watchers/save")
|
||||||
|
{
|
||||||
|
WatcherJobConfig job = _json.Deserialize<WatcherJobConfig>(ReadBody(ctx.Request));
|
||||||
|
WatcherJobConfig saved = _watcherRepository.Save(job);
|
||||||
|
_watcherManager.Reload();
|
||||||
|
WriteJson(ctx, saved); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "POST" && path == "/api/watchers/delete")
|
||||||
|
{
|
||||||
|
bool deleted = _watcherRepository.Delete(ctx.Request.QueryString["id"] ?? "");
|
||||||
|
_watcherManager.Reload();
|
||||||
|
WriteJson(ctx, new { deleted = deleted }); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "POST" && path == "/api/watchers/reload")
|
||||||
|
{ _watcherManager.Reload(); WriteJson(ctx, new { reloaded = true }); return; }
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "POST" && path == "/api/watchers/scan")
|
||||||
|
{ WriteJson(ctx, new { started = _watcherManager.ProcessExistingNow(ctx.Request.QueryString["id"] ?? "") }); return; }
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "GET" && path == "/api/logs")
|
||||||
|
{
|
||||||
|
int limit = 300;
|
||||||
|
int parsed;
|
||||||
|
if (Int32.TryParse(ctx.Request.QueryString["limit"], out parsed) && parsed > 0) limit = parsed;
|
||||||
|
WriteJson(ctx, _log.GetLatest(limit)); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.Request.HttpMethod == "GET" && path == "/api/status")
|
||||||
|
{
|
||||||
|
WriteJson(ctx, new
|
||||||
|
{
|
||||||
|
runningCronJobIds = _scheduler.GetRunningJobIds(),
|
||||||
|
activeWatcherIds = _watcherManager.GetActiveWatcherIds(),
|
||||||
|
serverTime = DateTime.Now
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.StatusCode = 404;
|
||||||
|
WriteJson(ctx, new { error = "Not found" });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
try { ctx.Response.StatusCode = 500; WriteJson(ctx, new { error = ex.Message }); }
|
||||||
|
catch { try { ctx.Response.Close(); } catch { } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsAuthorized(HttpListenerContext ctx)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrWhiteSpace(_options.WebUser)) return true;
|
||||||
|
string header = ctx.Request.Headers["Authorization"];
|
||||||
|
if (String.IsNullOrWhiteSpace(header) || !header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(header.Substring(6).Trim()));
|
||||||
|
int colon = decoded.IndexOf(':');
|
||||||
|
if (colon < 0) return false;
|
||||||
|
return decoded.Substring(0, colon) == _options.WebUser && decoded.Substring(colon + 1) == (_options.WebPassword ?? "");
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadBody(HttpListenerRequest request)
|
||||||
|
{
|
||||||
|
using (var reader = new StreamReader(request.InputStream, request.ContentEncoding ?? Encoding.UTF8))
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteJson(HttpListenerContext ctx, object value)
|
||||||
|
{
|
||||||
|
byte[] data = Encoding.UTF8.GetBytes(_json.Serialize(value));
|
||||||
|
ctx.Response.ContentType = "application/json; charset=utf-8";
|
||||||
|
ctx.Response.ContentLength64 = data.Length;
|
||||||
|
ctx.Response.OutputStream.Write(data, 0, data.Length);
|
||||||
|
ctx.Response.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteHtml(HttpListenerContext ctx, string html)
|
||||||
|
{
|
||||||
|
byte[] data = Encoding.UTF8.GetBytes(html);
|
||||||
|
ctx.Response.ContentType = "text/html; charset=utf-8";
|
||||||
|
ctx.Response.ContentLength64 = data.Length;
|
||||||
|
ctx.Response.OutputStream.Write(data, 0, data.Length);
|
||||||
|
ctx.Response.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_cts != null) _cts.Cancel();
|
||||||
|
try { _listener.Stop(); } catch { }
|
||||||
|
try { _listener.Close(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string Html = @"<!doctype html>
|
||||||
|
<html lang='de'>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||||
|
<title>FileInterface Admin</title>
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box}body{font-family:Segoe UI,Arial,sans-serif;margin:0;background:#f4f6f8;color:#1f2937}header{background:#111827;color:white;padding:18px 24px}main{max-width:1400px;margin:20px auto;padding:0 16px}.tabs{display:flex;gap:8px;margin-bottom:16px}.tab{background:#e5e7eb}.tab.active{background:#2563eb;color:white}.panel{display:none}.panel.active{display:block}.grid{display:grid;grid-template-columns:minmax(300px,.9fr) minmax(520px,1.5fr);gap:18px}.card{background:white;border:1px solid #dfe3e8;border-radius:10px;padding:16px;box-shadow:0 1px 3px #00000012}button{border:0;border-radius:6px;padding:8px 12px;cursor:pointer}.primary{background:#2563eb;color:white}.danger{background:#dc2626;color:white}.secondary{background:#e5e7eb}.success{background:#15803d;color:white}input,textarea,select{width:100%;padding:8px;border:1px solid #cbd5e1;border-radius:6px}input[type=checkbox]{width:auto}label{font-weight:600;font-size:13px;display:block;margin-top:10px}.row{display:grid;grid-template-columns:1fr 1fr;gap:10px}.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}.item{border-bottom:1px solid #e5e7eb;padding:10px 0}.item:last-child{border:0}.muted{color:#64748b;font-size:12px}.arg{display:flex;gap:6px;margin:6px 0}.arg input{flex:1}.log{font-family:Consolas,monospace;font-size:12px;max-height:340px;overflow:auto;background:#0f172a;color:#e2e8f0;padding:10px;border-radius:6px;white-space:pre-wrap}.ok{color:#15803d}.off{color:#9ca3af}.badge{display:inline-block;padding:2px 7px;border-radius:12px;font-size:11px;background:#e5e7eb}.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px}.help{background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;padding:8px;margin-top:10px;font-size:12px}@media(max-width:950px){.grid{grid-template-columns:1fr}.row,.row3{grid-template-columns:1fr}}
|
||||||
|
</style></head>
|
||||||
|
<body><header><h2 style='margin:0'>FileInterface – HTTP Administration</h2><div style='color:#cbd5e1;font-size:12px'>FileSystemWatcher + CronScheduler · iTAC IMS CustomFunctions</div></header>
|
||||||
|
<main>
|
||||||
|
<div class='tabs'><button id='tabWatcher' class='tab active' onclick=""showTab('watcher')"">FileSystemWatcher</button><button id='tabCron' class='tab' onclick=""showTab('cron')"">CronScheduler</button></div>
|
||||||
|
|
||||||
|
<section id='watcherPanel' class='panel active'><div class='grid'>
|
||||||
|
<div class='card'><div style='display:flex;justify-content:space-between;align-items:center'><h3>Watcher</h3><button class='primary' onclick='newWatcher()'>+ Neuer Watcher</button></div><div id='watcherList'></div></div>
|
||||||
|
<div class='card'><h3 id='watcherTitle'>Watcher bearbeiten</h3><input id='wid' type='hidden'>
|
||||||
|
<label>Name</label><input id='wname'>
|
||||||
|
<label><input id='wenabled' type='checkbox'> Aktiv</label>
|
||||||
|
<div class='row'><div><label>Quellverzeichnis</label><input id='wsource' placeholder='C:\Import'></div><div><label>Dateifilter</label><input id='wpattern' placeholder='*.xml'></div></div>
|
||||||
|
<label><input id='wrecursive' type='checkbox'> Unterverzeichnisse einbeziehen</label>
|
||||||
|
<div class='row3'><label><input id='wcreated' type='checkbox'> Created</label><label><input id='wchanged' type='checkbox'> Changed</label><label><input id='wrenamed' type='checkbox'> Renamed</label></div>
|
||||||
|
<div class='row3'><div><label>Debounce (ms)</label><input id='wdebounce' type='number' min='0'></div><div><label>File-Ready Versuche</label><input id='wretries' type='number' min='1'></div><div><label>Abstand (ms)</label><input id='wretrydelay' type='number' min='10'></div></div>
|
||||||
|
<div class='row'><div><label><input id='wremoveempty' type='checkbox'> Leere Zeilen entfernen</label></div><div><label>Zeilentrenner</label><input id='wseparator' placeholder=';'></div></div>
|
||||||
|
<label>CustomFunction</label><input id='wcf' placeholder='Namespace.functionName'>
|
||||||
|
<label>inArgs <span class='muted'>beliebig viele</span></label><div id='wargs'></div><button class='secondary' onclick='addWatcherArg()'>+ inArg</button>
|
||||||
|
<div class='help'>Platzhalter: {{CONTENT}}, {{RAW_CONTENT}}, {{FILE_NAME}}, {{FILE_PATH}}, {{FILE_DIR}}, {{FILE_EXT}}, {{JOB_NAME}}, {{EVENT}}, {{NOW}}</div>
|
||||||
|
<hr style='margin:16px 0;border:0;border-top:1px solid #e5e7eb'>
|
||||||
|
<label><input id='wmove' type='checkbox'> Datei nach erfolgreichem CF-Aufruf verschieben</label>
|
||||||
|
<div class='row'><div><label>Zielverzeichnis</label><input id='wtarget'></div><div><label><input id='woverwrite' type='checkbox'> Zieldatei überschreiben</label></div></div>
|
||||||
|
<div class='toolbar'><button class='primary' onclick='saveWatcher()'>Speichern</button><button class='success' onclick='scanWatcher()'>Vorhandene Dateien verarbeiten</button><button class='secondary' onclick='reloadWatchers()'>Watcher neu laden</button><button class='danger' onclick='deleteWatcher()'>Löschen</button></div><div id='wmsg' class='muted' style='margin-top:10px'></div>
|
||||||
|
</div></div></section>
|
||||||
|
|
||||||
|
<section id='cronPanel' class='panel'><div class='grid'>
|
||||||
|
<div class='card'><div style='display:flex;justify-content:space-between;align-items:center'><h3>Cronjobs</h3><button class='primary' onclick='newCron()'>+ Neuer Cronjob</button></div><div id='cronList'></div></div>
|
||||||
|
<div class='card'><h3 id='cronTitle'>Cronjob bearbeiten</h3><input id='cid' type='hidden'>
|
||||||
|
<label>Name</label><input id='cname'>
|
||||||
|
<div class='row'><div><label>Cron (5 Felder)</label><input id='ccron' placeholder='*/5 * * * *'></div><div><label><input id='cenabled' type='checkbox'> Aktiv</label></div></div>
|
||||||
|
<label><input id='cwithoutfile' type='checkbox'> CustomFunction einmal ohne Datei aufrufen</label>
|
||||||
|
<div class='row'><div><label>Quellverzeichnis</label><input id='csource'></div><div><label>Dateifilter</label><input id='cpattern' placeholder='*.xml'></div></div>
|
||||||
|
<label><input id='crecursive' type='checkbox'> Unterverzeichnisse einbeziehen</label>
|
||||||
|
<div class='row'><div><label><input id='cremoveempty' type='checkbox'> Leere Zeilen entfernen</label></div><div><label>Zeilentrenner</label><input id='cseparator' placeholder=';'></div></div>
|
||||||
|
<label>CustomFunction</label><input id='ccf' placeholder='Namespace.functionName'>
|
||||||
|
<label>inArgs <span class='muted'>beliebig viele</span></label><div id='cargs'></div><button class='secondary' onclick='addCronArg()'>+ inArg</button>
|
||||||
|
<div class='help'>Platzhalter: {{CONTENT}}, {{RAW_CONTENT}}, {{FILE_NAME}}, {{FILE_PATH}}, {{FILE_DIR}}, {{FILE_EXT}}, {{JOB_NAME}}, {{NOW}}</div>
|
||||||
|
<hr style='margin:16px 0;border:0;border-top:1px solid #e5e7eb'>
|
||||||
|
<label><input id='cmove' type='checkbox'> Datei nach erfolgreichem CF-Aufruf verschieben</label>
|
||||||
|
<div class='row'><div><label>Zielverzeichnis</label><input id='ctarget'></div><div><label><input id='coverwrite' type='checkbox'> Zieldatei überschreiben</label></div></div>
|
||||||
|
<div class='toolbar'><button class='primary' onclick='saveCron()'>Speichern</button><button class='success' onclick='runCron()'>Jetzt ausführen</button><button class='danger' onclick='deleteCron()'>Löschen</button></div><div id='cmsg' class='muted' style='margin-top:10px'></div>
|
||||||
|
</div></div></section>
|
||||||
|
|
||||||
|
<div class='card' style='margin-top:18px'><div style='display:flex;justify-content:space-between;align-items:center'><h3>Runtime / Log</h3><div id='status' class='muted'></div><button class='secondary' onclick='loadLogs()'>Aktualisieren</button></div><div id='log' class='log'></div></div>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
const $=id=>document.getElementById(id);let watchers=[],crons=[];
|
||||||
|
async function api(url,opt){const r=await fetch(url,opt);const t=await r.text();let x;try{x=JSON.parse(t)}catch(e){throw new Error(t||r.statusText)}if(!r.ok)throw new Error(x.error||r.statusText);return x}
|
||||||
|
function showTab(t){$('watcherPanel').classList.toggle('active',t==='watcher');$('cronPanel').classList.toggle('active',t==='cron');$('tabWatcher').classList.toggle('active',t==='watcher');$('tabCron').classList.toggle('active',t==='cron')}
|
||||||
|
function esc(s){return String(s||'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]))}
|
||||||
|
function addArg(containerId,v){const d=document.createElement('div');d.className='arg';const i=document.createElement('input');i.value=v==null?'':v;const b=document.createElement('button');b.type='button';b.textContent='−';b.className='danger';b.onclick=()=>d.remove();d.append(i,b);$(containerId).append(d)}
|
||||||
|
function addWatcherArg(v=''){addArg('wargs',v)}function addCronArg(v=''){addArg('cargs',v)}
|
||||||
|
|
||||||
|
async function loadWatchers(){watchers=await api('/api/watchers');renderWatchers();if(watchers.length&&!$('wid').value)editWatcher(watchers[0].Id)}
|
||||||
|
function renderWatchers(){$('watcherList').innerHTML=watchers.length?watchers.map(j=>`<div class='item'><b>${esc(j.Name)}</b> <span class='${j.Enabled?'ok':'off'}'>${j.Enabled?'aktiv':'inaktiv'}</span><div class='muted'>${esc(j.SourceDirectory)} · ${esc(j.SearchPattern)} · ${esc(j.CustomFunction)}</div><div class='muted'>Events: ${[j.WatchCreated?'Created':'',j.WatchChanged?'Changed':'',j.WatchRenamed?'Renamed':''].filter(Boolean).join(', ')}</div><button class='secondary' style='margin-top:6px' data-id='${esc(j.Id)}' onclick='editWatcher(this.dataset.id)'>Bearbeiten</button></div>`).join(''):'<div class=muted>Noch keine Watcher.</div>'}
|
||||||
|
function newWatcher(){fillWatcher({Id:'',Name:'Neuer Watcher',Enabled:true,SourceDirectory:'',SearchPattern:'*.*',IncludeSubdirectories:false,WatchCreated:true,WatchChanged:true,WatchRenamed:true,DebounceMilliseconds:1000,FileReadyRetries:10,FileReadyDelayMilliseconds:250,RemoveEmptyLines:true,JoinSeparator:';',CustomFunction:'',InArgs:['{{CONTENT}}'],MoveFileAfterSuccess:false,MoveTargetDirectory:'',OverwriteTargetFile:false})}
|
||||||
|
function editWatcher(id){const j=watchers.find(x=>x.Id===id);if(j)fillWatcher(j)}
|
||||||
|
function fillWatcher(j){$('wid').value=j.Id||'';$('wname').value=j.Name||'';$('wenabled').checked=!!j.Enabled;$('wsource').value=j.SourceDirectory||'';$('wpattern').value=j.SearchPattern||'*.*';$('wrecursive').checked=!!j.IncludeSubdirectories;$('wcreated').checked=!!j.WatchCreated;$('wchanged').checked=!!j.WatchChanged;$('wrenamed').checked=!!j.WatchRenamed;$('wdebounce').value=j.DebounceMilliseconds==null?1000:j.DebounceMilliseconds;$('wretries').value=j.FileReadyRetries||10;$('wretrydelay').value=j.FileReadyDelayMilliseconds||250;$('wremoveempty').checked=!!j.RemoveEmptyLines;$('wseparator').value=j.JoinSeparator==null?';':j.JoinSeparator;$('wcf').value=j.CustomFunction||'';$('wmove').checked=!!j.MoveFileAfterSuccess;$('wtarget').value=j.MoveTargetDirectory||'';$('woverwrite').checked=!!j.OverwriteTargetFile;$('wargs').innerHTML='';(j.InArgs||[]).forEach(addWatcherArg);$('watcherTitle').textContent=j.Id?'Watcher bearbeiten':'Neuer Watcher'}
|
||||||
|
function watcherModel(){return {Id:$('wid').value,Name:$('wname').value,Enabled:$('wenabled').checked,SourceDirectory:$('wsource').value,SearchPattern:$('wpattern').value,IncludeSubdirectories:$('wrecursive').checked,WatchCreated:$('wcreated').checked,WatchChanged:$('wchanged').checked,WatchRenamed:$('wrenamed').checked,DebounceMilliseconds:Number($('wdebounce').value||0),FileReadyRetries:Number($('wretries').value||10),FileReadyDelayMilliseconds:Number($('wretrydelay').value||250),RemoveEmptyLines:$('wremoveempty').checked,JoinSeparator:$('wseparator').value,CustomFunction:$('wcf').value,InArgs:[...$('wargs').querySelectorAll('input')].map(x=>x.value),MoveFileAfterSuccess:$('wmove').checked,MoveTargetDirectory:$('wtarget').value,OverwriteTargetFile:$('woverwrite').checked}}
|
||||||
|
async function saveWatcher(){try{const j=await api('/api/watchers/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(watcherModel())});$('wid').value=j.Id;$('wmsg').textContent='Gespeichert und FileSystemWatcher neu geladen.';await loadWatchers();await loadStatus()}catch(e){$('wmsg').textContent='Fehler: '+e.message}}
|
||||||
|
async function deleteWatcher(){if(!$('wid').value||!confirm('Watcher wirklich löschen?'))return;await api('/api/watchers/delete?id='+encodeURIComponent($('wid').value),{method:'POST'});$('wid').value='';newWatcher();await loadWatchers();await loadStatus()}
|
||||||
|
async function reloadWatchers(){try{await api('/api/watchers/reload',{method:'POST'});$('wmsg').textContent='Alle FileSystemWatcher neu geladen.';await loadStatus()}catch(e){$('wmsg').textContent='Fehler: '+e.message}}
|
||||||
|
async function scanWatcher(){if(!$('wid').value){$('wmsg').textContent='Bitte zuerst speichern.';return}const r=await api('/api/watchers/scan?id='+encodeURIComponent($('wid').value),{method:'POST'});$('wmsg').textContent=r.started?'Verzeichnisverarbeitung gestartet.':'Watcher nicht gefunden.';setTimeout(loadLogs,500)}
|
||||||
|
|
||||||
|
async function loadCrons(){crons=await api('/api/cronjobs');renderCrons();if(crons.length&&!$('cid').value)editCron(crons[0].Id)}
|
||||||
|
function renderCrons(){$('cronList').innerHTML=crons.length?crons.map(j=>`<div class='item'><b>${esc(j.Name)}</b> <span class='${j.Enabled?'ok':'off'}'>${j.Enabled?'aktiv':'inaktiv'}</span><div class='muted'>${esc(j.Cron)} · ${esc(j.CustomFunction)}</div><button class='secondary' style='margin-top:6px' data-id='${esc(j.Id)}' onclick='editCron(this.dataset.id)'>Bearbeiten</button></div>`).join(''):'<div class=muted>Noch keine Cronjobs.</div>'}
|
||||||
|
function newCron(){fillCron({Id:'',Name:'Neuer Cronjob',Enabled:true,Cron:'*/5 * * * *',CallOnceWithoutFile:false,SourceDirectory:'',SearchPattern:'*.*',IncludeSubdirectories:false,RemoveEmptyLines:true,JoinSeparator:';',CustomFunction:'',InArgs:['{{CONTENT}}'],MoveFileAfterSuccess:false,MoveTargetDirectory:'',OverwriteTargetFile:false})}
|
||||||
|
function editCron(id){const j=crons.find(x=>x.Id===id);if(j)fillCron(j)}
|
||||||
|
function fillCron(j){$('cid').value=j.Id||'';$('cname').value=j.Name||'';$('ccron').value=j.Cron||'';$('cenabled').checked=!!j.Enabled;$('cwithoutfile').checked=!!j.CallOnceWithoutFile;$('csource').value=j.SourceDirectory||'';$('cpattern').value=j.SearchPattern||'*.*';$('crecursive').checked=!!j.IncludeSubdirectories;$('cremoveempty').checked=!!j.RemoveEmptyLines;$('cseparator').value=j.JoinSeparator==null?';':j.JoinSeparator;$('ccf').value=j.CustomFunction||'';$('cmove').checked=!!j.MoveFileAfterSuccess;$('ctarget').value=j.MoveTargetDirectory||'';$('coverwrite').checked=!!j.OverwriteTargetFile;$('cargs').innerHTML='';(j.InArgs||[]).forEach(addCronArg);$('cronTitle').textContent=j.Id?'Cronjob bearbeiten':'Neuer Cronjob'}
|
||||||
|
function cronModel(){return {Id:$('cid').value,Name:$('cname').value,Enabled:$('cenabled').checked,Cron:$('ccron').value,CallOnceWithoutFile:$('cwithoutfile').checked,SourceDirectory:$('csource').value,SearchPattern:$('cpattern').value,IncludeSubdirectories:$('crecursive').checked,RemoveEmptyLines:$('cremoveempty').checked,JoinSeparator:$('cseparator').value,CustomFunction:$('ccf').value,InArgs:[...$('cargs').querySelectorAll('input')].map(x=>x.value),MoveFileAfterSuccess:$('cmove').checked,MoveTargetDirectory:$('ctarget').value,OverwriteTargetFile:$('coverwrite').checked}}
|
||||||
|
async function saveCron(){try{const j=await api('/api/cronjobs/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(cronModel())});$('cid').value=j.Id;$('cmsg').textContent='Gespeichert.';await loadCrons()}catch(e){$('cmsg').textContent='Fehler: '+e.message}}
|
||||||
|
async function deleteCron(){if(!$('cid').value||!confirm('Cronjob wirklich löschen?'))return;await api('/api/cronjobs/delete?id='+encodeURIComponent($('cid').value),{method:'POST'});$('cid').value='';newCron();await loadCrons()}
|
||||||
|
async function runCron(){if(!$('cid').value){$('cmsg').textContent='Bitte zuerst speichern.';return}const r=await api('/api/cronjobs/run?id='+encodeURIComponent($('cid').value),{method:'POST'});$('cmsg').textContent=r.started?'Cronjob gestartet.':'Cronjob nicht gefunden.';setTimeout(loadLogs,500)}
|
||||||
|
|
||||||
|
async function loadStatus(){try{const s=await api('/api/status');$('status').textContent=`Aktive Watcher: ${s.activeWatcherIds.length} · Laufende Cronjobs: ${s.runningCronJobIds.length}`}catch(e){$('status').textContent='Statusfehler: '+e.message}}
|
||||||
|
async function loadLogs(){try{const l=await api('/api/logs?limit=400');$('log').textContent=l.map(x=>`${formatDate(x.Timestamp)} [${x.Level}] [${x.JobType||''}] ${x.JobName}: ${x.Message}`).join('\n')}catch(e){$('log').textContent='Logfehler: '+e.message}}
|
||||||
|
function formatDate(v){if(typeof v==='string')return v;return JSON.stringify(v)}
|
||||||
|
newWatcher();newCron();Promise.all([loadWatchers(),loadCrons(),loadStatus(),loadLogs()]);setInterval(()=>{loadStatus();loadLogs()},10000);
|
||||||
|
</script></body></html>";
|
||||||
|
}
|
||||||
|
}
|
||||||
23
FileinterfaceCronScheduler/cronjobs.json
Normal file
23
FileinterfaceCronScheduler/cronjobs.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"Id": "example_cogi_import",
|
||||||
|
"Name": "Beispiel - COGI Import",
|
||||||
|
"Enabled": false,
|
||||||
|
"Cron": "*/5 * * * *",
|
||||||
|
"CallOnceWithoutFile": false,
|
||||||
|
"SourceDirectory": "C:\\FileInterface\\Input",
|
||||||
|
"SearchPattern": "*.xml",
|
||||||
|
"IncludeSubdirectories": false,
|
||||||
|
"RemoveEmptyLines": true,
|
||||||
|
"JoinSeparator": ";",
|
||||||
|
"CustomFunction": "BroseREST.restCallCogi1",
|
||||||
|
"InArgs": [
|
||||||
|
"RPCServices-WS",
|
||||||
|
"{{CONTENT}}",
|
||||||
|
"{{FILE_NAME}}"
|
||||||
|
],
|
||||||
|
"MoveFileAfterSuccess": true,
|
||||||
|
"MoveTargetDirectory": "C:\\FileInterface\\Archive",
|
||||||
|
"OverwriteTargetFile": false
|
||||||
|
}
|
||||||
|
]
|
||||||
0
FileinterfaceCronScheduler/ihas.properties
Normal file
0
FileinterfaceCronScheduler/ihas.properties
Normal file
BIN
FileinterfaceCronScheduler/lib/IMSApiDotNet.dll
Normal file
BIN
FileinterfaceCronScheduler/lib/IMSApiDotNet.dll
Normal file
Binary file not shown.
14
FileinterfaceCronScheduler/lib/README.txt
Normal file
14
FileinterfaceCronScheduler/lib/README.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
IMSApiDotNet.dll
|
||||||
|
================
|
||||||
|
|
||||||
|
Die IMSApiDotNet.dll ist in diesem Projekt bereits enthalten und wird vom
|
||||||
|
Visual-Studio-Projekt ueber folgende Referenz eingebunden:
|
||||||
|
|
||||||
|
lib\IMSApiDotNet.dll
|
||||||
|
|
||||||
|
Erkannte DLL-Version:
|
||||||
|
IMSApiDotNet 10.0.0-3
|
||||||
|
FileVersion 10.0.0.3
|
||||||
|
Plattform: x86 / 32 Bit
|
||||||
|
|
||||||
|
Das Projekt ist deshalb ebenfalls mit PlatformTarget=x86 konfiguriert.
|
||||||
28
FileinterfaceCronScheduler/watcherjobs.json
Normal file
28
FileinterfaceCronScheduler/watcherjobs.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"Id": "example_file_watcher",
|
||||||
|
"Name": "Beispiel - XML Eingang",
|
||||||
|
"Enabled": false,
|
||||||
|
"SourceDirectory": "C:\\FileInterface\\Input",
|
||||||
|
"SearchPattern": "*.xml",
|
||||||
|
"IncludeSubdirectories": false,
|
||||||
|
"WatchCreated": true,
|
||||||
|
"WatchChanged": true,
|
||||||
|
"WatchRenamed": true,
|
||||||
|
"DebounceMilliseconds": 1000,
|
||||||
|
"FileReadyRetries": 10,
|
||||||
|
"FileReadyDelayMilliseconds": 250,
|
||||||
|
"RemoveEmptyLines": true,
|
||||||
|
"JoinSeparator": ";",
|
||||||
|
"CustomFunction": "BroseREST.restCallCogi1",
|
||||||
|
"InArgs": [
|
||||||
|
"RPCServices-WS",
|
||||||
|
"{{CONTENT}}",
|
||||||
|
"{{FILE_NAME}}",
|
||||||
|
"{{EVENT}}"
|
||||||
|
],
|
||||||
|
"MoveFileAfterSuccess": true,
|
||||||
|
"MoveTargetDirectory": "C:\\FileInterface\\Archive",
|
||||||
|
"OverwriteTargetFile": false
|
||||||
|
}
|
||||||
|
]
|
||||||
220
README.md
Normal file
220
README.md
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# FileInterface – FileSystemWatcher + CronScheduler
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## HTTP-Webclient Login
|
||||||
|
|
||||||
|
Der Webclient ist standardmaessig per HTTP Basic Authentication geschuetzt.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Benutzer: admin
|
||||||
|
Kennwort: admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Zugangsdaten koennen in `FileinterfaceCronScheduler/App.config` geaendert werden:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<add key="Scheduler.WebUser" value="admin" />
|
||||||
|
<add key="Scheduler.WebPassword" value="admin" />
|
||||||
|
```
|
||||||
|
|
||||||
|
Wichtig: Fuer den produktiven Betrieb sollte das Standardkennwort geaendert werden.
|
||||||
|
|
||||||
|
Eigenständiges Visual-Studio-/C#-Projekt für ereignisbasierte **und** zeitgesteuerte Datei-Verarbeitung über iTAC IMSAPI CustomFunctions.
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
Beide Mechanismen laufen gleichzeitig und unabhängig voneinander:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP Webclient
|
||||||
|
|
|
||||||
|
+-- FileSystemWatcher-Konfiguration --> beliebig viele FileSystemWatcher
|
||||||
|
|
|
||||||
|
+-- Cron-Konfiguration ---------------> beliebig viele Cronjobs
|
||||||
|
|
|
||||||
|
+-----------------------------+
|
||||||
|
|
|
||||||
|
gemeinsame IMSAPI Session
|
||||||
|
|
|
||||||
|
customFunction(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enthalten
|
||||||
|
|
||||||
|
- `FileinterfaceCronScheduler.sln`
|
||||||
|
- .NET Framework 4.8 / x86
|
||||||
|
- Windows-Dienst und Konsolenmodus
|
||||||
|
- enthaltene `IMSApiDotNet.dll` Version 10.0.0-3
|
||||||
|
- IMSAPI Initialisierung und `regLogin(...)`
|
||||||
|
- beliebig viele FileSystemWatcher
|
||||||
|
- beliebig viele Cronjobs
|
||||||
|
- zentrale HTTP-Administration
|
||||||
|
- beliebig viele `inArgs` pro Watcher/Cronjob
|
||||||
|
- eigene CustomFunction pro Watcher/Cronjob
|
||||||
|
- Datei nach Erfolg optional verschieben
|
||||||
|
- Laufzeit-Log und Status im Browser
|
||||||
|
|
||||||
|
## HTTP Webclient
|
||||||
|
|
||||||
|
Standard:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:8095/
|
||||||
|
```
|
||||||
|
|
||||||
|
Für Zugriff aus dem Netzwerk in `App.config`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<add key="Scheduler.WebPrefix" value="http://+:8095/" />
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach `scripts\allow-webinterface-network.bat` als Administrator ausführen. Für Netzwerkzugriff sollten `Scheduler.WebUser` und `Scheduler.WebPassword` gesetzt werden.
|
||||||
|
|
||||||
|
Der Webclient besitzt zwei Bereiche:
|
||||||
|
|
||||||
|
### FileSystemWatcher
|
||||||
|
|
||||||
|
Pro Watcher konfigurierbar:
|
||||||
|
|
||||||
|
- Aktiv / Inaktiv
|
||||||
|
- Quellverzeichnis
|
||||||
|
- Dateifilter, z. B. `*.xml`
|
||||||
|
- Unterverzeichnisse
|
||||||
|
- Events `Created`, `Changed`, `Renamed`
|
||||||
|
- Debounce in Millisekunden
|
||||||
|
- File-Ready Wiederholungen / Wartezeit
|
||||||
|
- CustomFunction
|
||||||
|
- beliebig viele `inArgs`
|
||||||
|
- leere Zeilen entfernen
|
||||||
|
- Zeilentrenner
|
||||||
|
- Datei nach Erfolg verschieben Ja/Nein
|
||||||
|
- Zielverzeichnis
|
||||||
|
- bestehende Zieldatei überschreiben Ja/Nein
|
||||||
|
- vorhandene Dateien manuell verarbeiten
|
||||||
|
- Watcher ohne Dienstneustart neu laden
|
||||||
|
|
||||||
|
Änderungen werden beim Speichern sofort persistent geschrieben und die FileSystemWatcher werden automatisch neu aufgebaut.
|
||||||
|
|
||||||
|
### CronScheduler
|
||||||
|
|
||||||
|
Pro Cronjob konfigurierbar:
|
||||||
|
|
||||||
|
- Aktiv / Inaktiv
|
||||||
|
- Cron-Ausdruck mit 5 Feldern
|
||||||
|
- optional CustomFunction ohne Datei aufrufen
|
||||||
|
- Quellverzeichnis / Filter / Unterverzeichnisse
|
||||||
|
- CustomFunction
|
||||||
|
- beliebig viele `inArgs`
|
||||||
|
- Datei nach Erfolg verschieben Ja/Nein
|
||||||
|
- manuell `Jetzt ausführen`
|
||||||
|
|
||||||
|
Beispiele:
|
||||||
|
|
||||||
|
```text
|
||||||
|
*/5 * * * *
|
||||||
|
0 */2 * * *
|
||||||
|
15 2 * * *
|
||||||
|
0 6 * * 1-5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platzhalter
|
||||||
|
|
||||||
|
Für Watcher und Cronjobs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{{CONTENT}}
|
||||||
|
{{RAW_CONTENT}}
|
||||||
|
{{FILE_NAME}}
|
||||||
|
{{FILE_PATH}}
|
||||||
|
{{FILE_DIR}}
|
||||||
|
{{FILE_EXT}}
|
||||||
|
{{JOB_NAME}}
|
||||||
|
{{NOW}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nur beim FileSystemWatcher zusätzlich:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{{EVENT}}
|
||||||
|
```
|
||||||
|
|
||||||
|
`{{EVENT}}` enthält z. B. `Created`, `Changed`, `Renamed` oder `ManualScan`.
|
||||||
|
|
||||||
|
## Persistente Konfiguration
|
||||||
|
|
||||||
|
Die Jobs sind getrennt gespeichert:
|
||||||
|
|
||||||
|
```text
|
||||||
|
cronjobs.json
|
||||||
|
watcherjobs.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Pfade können in `App.config` geändert werden:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<add key="Scheduler.ConfigFile" value="cronjobs.json" />
|
||||||
|
<add key="Watcher.ConfigFile" value="watcherjobs.json" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## IMSAPI
|
||||||
|
|
||||||
|
`IMSApiDotNet.dll` liegt bereits unter:
|
||||||
|
|
||||||
|
```text
|
||||||
|
FileinterfaceCronScheduler\lib\IMSApiDotNet.dll
|
||||||
|
```
|
||||||
|
|
||||||
|
Beim Programmstart:
|
||||||
|
|
||||||
|
1. `IMSApiDotNet.setProperty(...)`
|
||||||
|
2. `IMSApiDotNet.loadLibrary()`
|
||||||
|
3. `imsapiInit()`
|
||||||
|
4. `regLogin(...)`
|
||||||
|
5. Start FileSystemWatcher + CronScheduler + HTTP-Webclient
|
||||||
|
|
||||||
|
Die Aufrufe verwenden anschließend dieselbe Session:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
imsapi.customFunction(
|
||||||
|
sessionContext,
|
||||||
|
customFunctionName,
|
||||||
|
inArgs,
|
||||||
|
out outArgs,
|
||||||
|
out customErrorString
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## IMS-Konfiguration
|
||||||
|
|
||||||
|
In `FileinterfaceCronScheduler\App.config`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<add key="IMS.AppID" value="FileInterfaceCronScheduler" />
|
||||||
|
<add key="IMS.ServerUrl" value="http://YOUR-IMS-SERVER:PORT" />
|
||||||
|
<add key="IMS.StationNumber" value="YOUR_STATION" />
|
||||||
|
<add key="IMS.ClientNumber" value="1" />
|
||||||
|
<add key="IMS.RegistrationType" value="S" />
|
||||||
|
<add key="IMS.PropertyDirectory" value="." />
|
||||||
|
<add key="IMS.SerializeCalls" value="false" />
|
||||||
|
```
|
||||||
|
|
||||||
|
`IMS.SerializeCalls=false` erlaubt unabhängigen Watchern und Cronjobs parallele CustomFunction-Aufrufe. Wenn die eingesetzte IMSAPI-Umgebung das nicht verträgt, kann auf `true` gestellt werden.
|
||||||
|
|
||||||
|
## Visual Studio
|
||||||
|
|
||||||
|
1. `FileinterfaceCronScheduler.sln` öffnen.
|
||||||
|
2. `App.config` konfigurieren.
|
||||||
|
3. Build `Debug` oder `Release`; das Projekt kompiliert als x86.
|
||||||
|
4. Starten.
|
||||||
|
5. Browser auf `http://127.0.0.1:8095/` öffnen.
|
||||||
|
|
||||||
|
## Windows-Dienst
|
||||||
|
|
||||||
|
Unter `scripts`:
|
||||||
|
|
||||||
|
- `install-service.bat`
|
||||||
|
- `uninstall-service.bat`
|
||||||
|
- `allow-webinterface-network.bat`
|
||||||
|
|
||||||
|
Vor der Dienstinstallation zuerst Release bauen.
|
||||||
4
scripts/allow-webinterface-network.bat
Normal file
4
scripts/allow-webinterface-network.bat
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@echo off
|
||||||
|
REM Als Administrator ausfuehren, wenn Scheduler.WebPrefix = http://+:8095/ genutzt wird.
|
||||||
|
netsh http add urlacl url=http://+:8095/ user=Everyone
|
||||||
|
netsh advfirewall firewall add rule name="FileInterface HTTP Admin 8095" dir=in action=allow protocol=TCP localport=8095
|
||||||
22
scripts/install-service.bat
Normal file
22
scripts/install-service.bat
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
set SERVICE_NAME=FileinterfaceCronScheduler
|
||||||
|
set EXE=%~dp0..\FileinterfaceCronScheduler\bin\Release\FileinterfaceCronScheduler.exe
|
||||||
|
|
||||||
|
if not exist "%EXE%" (
|
||||||
|
echo EXE nicht gefunden: %EXE%
|
||||||
|
echo Bitte Solution zuerst im Release-Modus bauen.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
sc.exe query "%SERVICE_NAME%" >nul 2>&1
|
||||||
|
if %errorlevel%==0 (
|
||||||
|
echo Dienst %SERVICE_NAME% existiert bereits.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
sc.exe create "%SERVICE_NAME%" binPath= "\"%EXE%\" --service" start= auto DisplayName= "FileInterface Watcher + CronScheduler"
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
sc.exe description "%SERVICE_NAME%" "FileSystemWatcher und CronScheduler fuer iTAC IMS CustomFunctions mit HTTP-Administration"
|
||||||
|
sc.exe start "%SERVICE_NAME%"
|
||||||
|
endlocal
|
||||||
7
scripts/uninstall-service.bat
Normal file
7
scripts/uninstall-service.bat
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
set SERVICE_NAME=FileinterfaceCronScheduler
|
||||||
|
sc.exe stop "%SERVICE_NAME%"
|
||||||
|
timeout /t 2 /nobreak >nul
|
||||||
|
sc.exe delete "%SERVICE_NAME%"
|
||||||
|
endlocal
|
||||||
Reference in New Issue
Block a user