467 lines
22 KiB
C#
467 lines
22 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace HikvisionAttendanceService;
|
|
|
|
/// <summary>
|
|
/// Console test harness: run the same SDK + pipeline as the Windows Service without installing the service.
|
|
/// Usage: HikvisionAttendanceService.exe --test
|
|
/// HikvisionAttendanceService.exe --test --fetch "DS-K1T642MFW-7378360" --from "2026-03-01 08:00:00" --to "2026-03-28 23:59:59"
|
|
/// HikvisionAttendanceService.exe --test --export-templates "DS-K1T642MFW-FM7378360" --card "1001" [--out "C:\SdkLog\templates.json"]
|
|
/// HikvisionAttendanceService.exe --test --export-all-templates-isapi "DS-K1T642MFW-FM7378360" [--outDir "C:\SdkLog\templates_isapi"] [--pageSize 30] [--maxUsers 5000]
|
|
/// HikvisionAttendanceService.exe --test --user-sync-once (runs one RunUserFaceSyncCycle from serviceconfig.json; requires EnableUserSync + source/targets)
|
|
/// </summary>
|
|
internal static class AttendanceTestMode
|
|
{
|
|
private const string BuildMarker = "CFGDIAG_20260331_1";
|
|
|
|
public static async Task RunAsync(string[] args)
|
|
{
|
|
Environment.CurrentDirectory = AppContext.BaseDirectory;
|
|
|
|
var baseDir = AppContext.BaseDirectory;
|
|
var serviceCfgPath = Path.Combine(baseDir, "serviceconfig.json");
|
|
var appSettingsPath = Path.Combine(baseDir, "appsettings.json");
|
|
var chosenConfigPath = File.Exists(serviceCfgPath) ? serviceCfgPath : appSettingsPath;
|
|
|
|
// Startup diagnostics: prove which config file is used and what devices it contains.
|
|
// (Logger needs LogDirectory, so we bootstrap a temporary logger first.)
|
|
var bootstrapLogger = new HikvisionAttendanceWindowsService.FileLogger(@"C:\Users\Public\HikvisionAttendanceService");
|
|
bootstrapLogger.Info("TEST config: baseDir=\"" + baseDir + "\"");
|
|
bootstrapLogger.Info("TEST config: candidate serviceconfig.json=\"" + serviceCfgPath + "\" exists=" + File.Exists(serviceCfgPath));
|
|
bootstrapLogger.Info("TEST config: candidate appsettings.json=\"" + appSettingsPath + "\" exists=" + File.Exists(appSettingsPath));
|
|
bootstrapLogger.Info("TEST config: chosenConfigPath=\"" + chosenConfigPath + "\" exists=" + File.Exists(chosenConfigPath));
|
|
|
|
var config = HikvisionAttendanceWindowsService.HikvisionServiceConfig.Load(chosenConfigPath);
|
|
var logger = new HikvisionAttendanceWindowsService.FileLogger(config.LogDirectory);
|
|
logger.Info("TEST config: loadedFromPath=\"" + chosenConfigPath + "\" exists=" + File.Exists(chosenConfigPath));
|
|
logger.Info("TEST config: devicesLoadedCount=" + (config.Devices == null ? "(null)" : config.Devices.Count.ToString()));
|
|
if (config.Devices == null || config.Devices.Count == 0)
|
|
{
|
|
logger.Warn("TEST config: Devices is null/empty; StartDevices will skip all logins.");
|
|
}
|
|
else
|
|
{
|
|
foreach (var d in config.Devices)
|
|
{
|
|
logger.Info("TEST config: device entry: DeviceId=\"" + (d.DeviceId ?? "") + "\" Ip=\"" + (d.Ip ?? "") + "\" Port=" + d.Port +
|
|
" Username=\"" + (d.Username ?? "") + "\"");
|
|
}
|
|
}
|
|
|
|
var rawJoined = args.Length == 0 ? "(none)" : string.Join(" ", args.Select(QuoteForLog));
|
|
logger.Info("TEST mode entry: build=" + BuildMarker +
|
|
" baseDir=\"" + baseDir + "\"" +
|
|
" chosenConfigPath=\"" + chosenConfigPath + "\" chosenExists=" + File.Exists(chosenConfigPath) +
|
|
" rawArgCount=" + args.Length + " rawArgs=" + rawJoined);
|
|
|
|
var userSyncOnceWanted = args.Any(a => string.Equals(a, "--user-sync-once", StringComparison.OrdinalIgnoreCase));
|
|
var fetchWanted = args.Any(a => string.Equals(a, "--fetch", StringComparison.OrdinalIgnoreCase));
|
|
var exportAllWanted = args.Any(a => string.Equals(a, "--export-all-templates", StringComparison.OrdinalIgnoreCase));
|
|
var exportAllIsapiWanted = args.Any(a => string.Equals(a, "--export-all-templates-isapi", StringComparison.OrdinalIgnoreCase));
|
|
var exportTemplatesWanted = args.Any(a => string.Equals(a, "--export-templates", StringComparison.OrdinalIgnoreCase));
|
|
var exportParseOk = TryParseExportTemplateArgs(args, out var exportDeviceId, out var exportCardNo, out var exportOutPath, out var exportParseFailure);
|
|
var exportAllParseOk = TryParseExportAllTemplatesArgs(args, out var exportAllDeviceId, out var exportAllOutDir, out var exportAllPageSize, out var exportAllMaxUsers, out var exportAllParseFailure);
|
|
var exportAllIsapiParseOk = TryParseExportAllIsapiTemplatesArgs(args, out var exportAllIsapiDeviceId, out var exportAllIsapiOutDir, out var exportAllIsapiPageSize, out var exportAllIsapiMaxUsers, out var exportAllIsapiParseFailure);
|
|
var parsedOk = TryParseFetchArgs(args, out var deviceId, out var from, out var to, out var parseFailure);
|
|
logger.Info("TEST mode parse: exportTemplatesFlagPresent=" + exportTemplatesWanted + ", exportBranchWillRun=" + exportParseOk +
|
|
", exportDeviceId=" + (string.IsNullOrEmpty(exportDeviceId) ? "(empty)" : exportDeviceId) +
|
|
", exportCardNo=" + (string.IsNullOrEmpty(exportCardNo) ? "(empty)" : exportCardNo) +
|
|
", exportOutPath=" + (string.IsNullOrEmpty(exportOutPath) ? "(default under LogDirectory)" : exportOutPath) +
|
|
(exportParseOk ? "" : ", exportParseFailed=" + exportParseFailure));
|
|
logger.Info("TEST mode parse: exportAllFlagPresent=" + exportAllWanted + ", exportAllBranchWillRun=" + exportAllParseOk +
|
|
", exportAllDeviceId=" + (string.IsNullOrEmpty(exportAllDeviceId) ? "(empty)" : exportAllDeviceId) +
|
|
", exportAllOutDir=" + (string.IsNullOrEmpty(exportAllOutDir) ? "(default under LogDirectory)" : exportAllOutDir) +
|
|
", pageSize=" + exportAllPageSize +
|
|
", maxUsers=" + exportAllMaxUsers +
|
|
(exportAllParseOk ? "" : ", exportAllParseFailed=" + exportAllParseFailure));
|
|
logger.Info("TEST mode parse: exportAllIsapiFlagPresent=" + exportAllIsapiWanted + ", exportAllIsapiBranchWillRun=" + exportAllIsapiParseOk +
|
|
", exportAllIsapiDeviceId=" + (string.IsNullOrEmpty(exportAllIsapiDeviceId) ? "(empty)" : exportAllIsapiDeviceId) +
|
|
", exportAllIsapiOutDir=" + (string.IsNullOrEmpty(exportAllIsapiOutDir) ? "(default under LogDirectory)" : exportAllIsapiOutDir) +
|
|
", pageSize=" + exportAllIsapiPageSize +
|
|
", maxUsers=" + exportAllIsapiMaxUsers +
|
|
(exportAllIsapiParseOk ? "" : ", exportAllIsapiParseFailed=" + exportAllIsapiParseFailure));
|
|
logger.Info("TEST mode parse: userSyncOnceFlagPresent=" + userSyncOnceWanted);
|
|
logger.Info("TEST mode parse: fetchFlagPresent=" + fetchWanted + ", fetchBranchWillRun=" + parsedOk +
|
|
", parsedDeviceId=" + (string.IsNullOrEmpty(deviceId) ? "(empty)" : deviceId) +
|
|
", parsedFrom=" + (from == default ? "(unset)" : from.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)) +
|
|
", parsedTo=" + (to == default ? "(unset)" : to.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)) +
|
|
(parsedOk ? "" : ", fetchParseFailed=" + parseFailure));
|
|
|
|
Console.WriteLine("HikvisionAttendanceService TEST mode");
|
|
Console.WriteLine("Log directory: " + config.LogDirectory);
|
|
Console.WriteLine("Press Ctrl+C to exit.");
|
|
if (exportTemplatesWanted && !exportParseOk)
|
|
Console.WriteLine("Template export not started: " + exportParseFailure);
|
|
if (exportAllWanted && !exportAllParseOk)
|
|
Console.WriteLine("All template export not started: " + exportAllParseFailure);
|
|
if (exportAllIsapiWanted && !exportAllIsapiParseOk)
|
|
Console.WriteLine("All ISAPI template export not started: " + exportAllIsapiParseFailure);
|
|
if (!parsedOk && fetchWanted)
|
|
Console.WriteLine("Fetch not started: " + parseFailure);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
Console.CancelKeyPress += (_, e) =>
|
|
{
|
|
e.Cancel = true;
|
|
cts.Cancel();
|
|
};
|
|
|
|
using var manager = new HikvisionAttendanceManager(config, logger);
|
|
var run = manager.RunAsync(cts.Token);
|
|
|
|
await Task.Delay(3500, CancellationToken.None).ConfigureAwait(false);
|
|
|
|
if (userSyncOnceWanted)
|
|
{
|
|
logger.Info("TEST mode: --user-sync-once running RunUserFaceSyncCycle (uses SourceDeviceId, TargetDeviceIds, SyncPolicies from config).");
|
|
Console.WriteLine("Running one user/face sync cycle...");
|
|
try
|
|
{
|
|
await Task.Run(() => manager.RunUserFaceSyncCycle(cts.Token), cts.Token).ConfigureAwait(false);
|
|
Console.WriteLine("User sync cycle finished. See logs under: " + config.LogDirectory);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Error("TEST mode: user-sync-once failed", ex);
|
|
Console.WriteLine("User sync failed: " + ex.Message);
|
|
}
|
|
|
|
Console.WriteLine("Stopping test host...");
|
|
cts.Cancel();
|
|
}
|
|
else if (exportAllIsapiParseOk)
|
|
{
|
|
logger.Info("TEST mode: entering export-all templates ISAPI branch deviceId=" + exportAllIsapiDeviceId +
|
|
", outDir=" + (string.IsNullOrEmpty(exportAllIsapiOutDir) ? "(default LogDirectory)" : exportAllIsapiOutDir) +
|
|
", pageSize=" + exportAllIsapiPageSize +
|
|
", maxUsers=" + exportAllIsapiMaxUsers);
|
|
|
|
Console.WriteLine($"Exporting ALL face + fingerprint templates via ISAPI: device={exportAllIsapiDeviceId}, pageSize={exportAllIsapiPageSize}, maxUsers={exportAllIsapiMaxUsers}");
|
|
|
|
string jsonPath = "";
|
|
string exportErr = "";
|
|
bool exportOk = await Task.Run(() =>
|
|
manager.TryExportAllUsersTemplatesToIsapiFile(
|
|
exportAllIsapiDeviceId,
|
|
string.IsNullOrEmpty(exportAllIsapiOutDir) ? null : exportAllIsapiOutDir,
|
|
exportAllIsapiPageSize,
|
|
exportAllIsapiMaxUsers,
|
|
cts.Token,
|
|
out jsonPath,
|
|
out exportErr), cts.Token).ConfigureAwait(false);
|
|
|
|
if (exportOk)
|
|
{
|
|
logger.Info("TEST mode: export-all (ISAPI) templates finished OK jsonPath=" + jsonPath);
|
|
Console.WriteLine("ISAPI export-all templates written: " + jsonPath);
|
|
}
|
|
else
|
|
{
|
|
logger.Warn("TEST mode: export-all (ISAPI) templates failed: " + exportErr);
|
|
Console.WriteLine("ISAPI export-all templates failed: " + exportErr);
|
|
}
|
|
|
|
Console.WriteLine("Stopping test host...");
|
|
cts.Cancel();
|
|
}
|
|
else if (exportAllParseOk)
|
|
{
|
|
logger.Info("TEST mode: entering export-all templates branch deviceId=" + exportAllDeviceId +
|
|
", outDir=" + (string.IsNullOrEmpty(exportAllOutDir) ? "(default LogDirectory)" : exportAllOutDir) +
|
|
", pageSize=" + exportAllPageSize +
|
|
", maxUsers=" + exportAllMaxUsers);
|
|
|
|
Console.WriteLine($"Exporting ALL face + fingerprint templates: device={exportAllDeviceId}, pageSize={exportAllPageSize}, maxUsers={exportAllMaxUsers}");
|
|
|
|
string jsonPath = "";
|
|
string exportErr = "";
|
|
bool exportOk = await Task.Run(() =>
|
|
manager.TryExportAllUsersTemplatesToFile(
|
|
exportAllDeviceId,
|
|
string.IsNullOrEmpty(exportAllOutDir) ? null : exportAllOutDir,
|
|
exportAllPageSize,
|
|
exportAllMaxUsers,
|
|
cts.Token,
|
|
out jsonPath,
|
|
out exportErr), cts.Token).ConfigureAwait(false);
|
|
|
|
if (exportOk)
|
|
{
|
|
logger.Info("TEST mode: export-all templates finished OK jsonPath=" + jsonPath);
|
|
Console.WriteLine("All templates export written: " + jsonPath);
|
|
}
|
|
else
|
|
{
|
|
logger.Warn("TEST mode: export-all templates failed: " + exportErr);
|
|
Console.WriteLine("Export-all templates failed: " + exportErr);
|
|
}
|
|
|
|
Console.WriteLine("Stopping test host...");
|
|
cts.Cancel();
|
|
}
|
|
else if (exportParseOk)
|
|
{
|
|
logger.Info("TEST mode: entering template export branch deviceId=" + exportDeviceId + " cardNo=" + exportCardNo +
|
|
(string.IsNullOrEmpty(exportOutPath) ? "" : " out=" + exportOutPath));
|
|
Console.WriteLine($"Exporting face + fingerprint templates: device={exportDeviceId}, card={exportCardNo}");
|
|
string writtenPath = "";
|
|
string exportErr = "";
|
|
bool exportOk = await Task.Run(() =>
|
|
manager.TryExportUserTemplatesToFile(exportDeviceId, exportCardNo,
|
|
string.IsNullOrEmpty(exportOutPath) ? null : exportOutPath, out writtenPath, out exportErr), cts.Token).ConfigureAwait(false);
|
|
if (exportOk)
|
|
{
|
|
logger.Info("TEST mode: template export finished OK path=" + writtenPath);
|
|
Console.WriteLine("Template export written: " + writtenPath);
|
|
}
|
|
else
|
|
{
|
|
logger.Warn("TEST mode: template export failed: " + exportErr);
|
|
Console.WriteLine("Template export failed: " + exportErr);
|
|
}
|
|
|
|
Console.WriteLine("Stopping test host...");
|
|
cts.Cancel();
|
|
}
|
|
else if (parsedOk)
|
|
{
|
|
logger.Info("TEST mode: entering CLI fetch branch for deviceId=" + deviceId +
|
|
" from=" + from.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) +
|
|
" to=" + to.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture));
|
|
Console.WriteLine($"Fetching historical ACS events: device={deviceId}, from={from}, to={to}");
|
|
int n = await manager.FetchAttendanceRecordsAsync(deviceId, from, to, cts.Token).ConfigureAwait(false);
|
|
logger.Info("TEST mode: CLI fetch finished, recordsParsedOk=" + n);
|
|
Console.WriteLine("Fetch completed, records enqueued: " + n);
|
|
Console.WriteLine("Stopping test host...");
|
|
cts.Cancel();
|
|
}
|
|
else
|
|
{
|
|
logger.Info("TEST mode: live-only branch (no CLI fetch). " + (fetchWanted ? parseFailure : "Reason: " + parseFailure));
|
|
Console.WriteLine("Live ACS + scheduled historical fetch (if enabled) running. Tail today's attendance log for details.");
|
|
Console.WriteLine("Optional: --test --fetch DeviceId --from yyyy-MM-dd HH:mm:ss --to yyyy-MM-dd HH:mm:ss");
|
|
Console.WriteLine("Optional: --test --export-templates DeviceId --card CardOrEmployeeNo [--out path.json]");
|
|
Console.WriteLine("Optional: --test --export-all-templates DeviceId [--outDir path] [--pageSize 30] [--maxUsers 5000]");
|
|
Console.WriteLine("Optional: --test --export-all-templates-isapi DeviceId [--outDir path] [--pageSize 30] [--maxUsers 5000]");
|
|
Console.WriteLine("Waiting... (Ctrl+C to stop)");
|
|
}
|
|
|
|
try
|
|
{
|
|
await run.ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// expected
|
|
}
|
|
}
|
|
|
|
private static string QuoteForLog(string a)
|
|
{
|
|
if (string.IsNullOrEmpty(a))
|
|
return a;
|
|
return a.Any(char.IsWhiteSpace) ? "\"" + a.Replace("\"", "\\\"") + "\"" : a;
|
|
}
|
|
|
|
private static bool TryParseExportTemplateArgs(string[] args, out string deviceId, out string cardNo, out string outPath, out string failureReason)
|
|
{
|
|
deviceId = "";
|
|
cardNo = "";
|
|
outPath = "";
|
|
failureReason = "";
|
|
|
|
if (!args.Any(a => string.Equals(a, "--export-templates", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
failureReason = "no --export-templates in args";
|
|
return false;
|
|
}
|
|
|
|
deviceId = (GetArgAfterSwitch(args, "--export-templates") ?? "").Trim().Trim('"');
|
|
cardNo = (GetArgAfterSwitch(args, "--card") ?? "").Trim().Trim('"');
|
|
var outVal = GetArgAfterSwitch(args, "--out");
|
|
outPath = string.IsNullOrWhiteSpace(outVal) ? "" : outVal.Trim().Trim('"');
|
|
|
|
if (string.IsNullOrWhiteSpace(deviceId))
|
|
{
|
|
failureReason = "missing value after --export-templates";
|
|
return false;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(cardNo))
|
|
{
|
|
failureReason = "missing --card <CardNoOrEmployeeId> (SDK enroll key on device)";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryParseExportAllTemplatesArgs(
|
|
string[] args,
|
|
out string deviceId,
|
|
out string outDir,
|
|
out int pageSize,
|
|
out int maxUsers,
|
|
out string failureReason)
|
|
{
|
|
deviceId = "";
|
|
outDir = "";
|
|
pageSize = 30;
|
|
maxUsers = 10000;
|
|
failureReason = "";
|
|
|
|
if (!args.Any(a => string.Equals(a, "--export-all-templates", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
failureReason = "no --export-all-templates in args";
|
|
return false;
|
|
}
|
|
|
|
deviceId = (GetArgAfterSwitch(args, "--export-all-templates") ?? "").Trim().Trim('"');
|
|
var outVal = GetArgAfterSwitch(args, "--outDir");
|
|
outDir = string.IsNullOrWhiteSpace(outVal) ? "" : outVal.Trim().Trim('"');
|
|
|
|
var pageVal = GetArgAfterSwitch(args, "--pageSize");
|
|
if (!string.IsNullOrWhiteSpace(pageVal) && int.TryParse(pageVal, out var ps))
|
|
pageSize = Math.Max(1, ps);
|
|
|
|
var maxVal = GetArgAfterSwitch(args, "--maxUsers");
|
|
if (!string.IsNullOrWhiteSpace(maxVal) && int.TryParse(maxVal, out var mu))
|
|
maxUsers = Math.Max(1, mu);
|
|
|
|
if (string.IsNullOrWhiteSpace(deviceId))
|
|
{
|
|
failureReason = "missing value after --export-all-templates";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryParseExportAllIsapiTemplatesArgs(
|
|
string[] args,
|
|
out string deviceId,
|
|
out string outDir,
|
|
out int pageSize,
|
|
out int maxUsers,
|
|
out string failureReason)
|
|
{
|
|
deviceId = "";
|
|
outDir = "";
|
|
pageSize = 30;
|
|
maxUsers = 10000;
|
|
failureReason = "";
|
|
|
|
if (!args.Any(a => string.Equals(a, "--export-all-templates-isapi", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
failureReason = "no --export-all-templates-isapi in args";
|
|
return false;
|
|
}
|
|
|
|
deviceId = (GetArgAfterSwitch(args, "--export-all-templates-isapi") ?? "").Trim().Trim('\"');
|
|
var outVal = GetArgAfterSwitch(args, "--outDir");
|
|
outDir = string.IsNullOrWhiteSpace(outVal) ? "" : outVal.Trim().Trim('\"');
|
|
|
|
var pageVal = GetArgAfterSwitch(args, "--pageSize");
|
|
if (!string.IsNullOrWhiteSpace(pageVal) && int.TryParse(pageVal, out var ps))
|
|
pageSize = Math.Max(1, ps);
|
|
|
|
var maxVal = GetArgAfterSwitch(args, "--maxUsers");
|
|
if (!string.IsNullOrWhiteSpace(maxVal) && int.TryParse(maxVal, out var mu))
|
|
maxUsers = Math.Max(1, mu);
|
|
|
|
if (string.IsNullOrWhiteSpace(deviceId))
|
|
{
|
|
failureReason = "missing value after --export-all-templates-isapi";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryParseFetchArgs(string[] args, out string deviceId, out DateTime from, out DateTime to, out string failureReason)
|
|
{
|
|
deviceId = "";
|
|
from = default;
|
|
to = default;
|
|
failureReason = "";
|
|
|
|
bool wantFetch = false;
|
|
for (int i = 0; i < args.Length; i++)
|
|
{
|
|
if (string.Equals(args[i], "--fetch", StringComparison.OrdinalIgnoreCase))
|
|
wantFetch = true;
|
|
}
|
|
|
|
if (!wantFetch)
|
|
{
|
|
failureReason = "no --fetch in args";
|
|
return false;
|
|
}
|
|
|
|
var fetchVal = GetArgAfterSwitch(args, "--fetch");
|
|
var fromVal = GetArgAfterSwitch(args, "--from");
|
|
var toVal = GetArgAfterSwitch(args, "--to");
|
|
|
|
deviceId = (fetchVal ?? "").Trim().Trim('"');
|
|
bool fromOk = TryParseCliDateTime(fromVal, out from);
|
|
bool toOk = TryParseCliDateTime(toVal, out to);
|
|
|
|
if (string.IsNullOrWhiteSpace(deviceId))
|
|
{
|
|
failureReason = "missing value after --fetch (need --fetch <DeviceId>)";
|
|
return false;
|
|
}
|
|
|
|
if (!fromOk)
|
|
{
|
|
failureReason = "invalid or missing --from (got \"" + (fromVal ?? "(missing)") + "\"; use yyyy-MM-dd HH:mm:ss or yyyy-MM-dd)";
|
|
return false;
|
|
}
|
|
|
|
if (!toOk)
|
|
{
|
|
failureReason = "invalid or missing --to (got \"" + (toVal ?? "(missing)") + "\"; use yyyy-MM-dd HH:mm:ss or yyyy-MM-dd)";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static string? GetArgAfterSwitch(string[] args, string switchName)
|
|
{
|
|
for (int i = 0; i < args.Length - 1; i++)
|
|
{
|
|
if (string.Equals(args[i], switchName, StringComparison.OrdinalIgnoreCase))
|
|
return args[i + 1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool TryParseCliDateTime(string? raw, out DateTime dt)
|
|
{
|
|
dt = default;
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
return false;
|
|
|
|
var s = raw.Trim().Trim('"');
|
|
string[] formats =
|
|
{
|
|
"yyyy-MM-dd HH:mm:ss",
|
|
"yyyy-MM-ddTHH:mm:ss",
|
|
"yyyy-MM-ddTHH:mm:ss.fff",
|
|
"yyyy-MM-dd"
|
|
};
|
|
|
|
if (DateTime.TryParseExact(s, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out dt))
|
|
return true;
|
|
|
|
return DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out dt);
|
|
}
|
|
}
|