diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..c3ddbd2 --- /dev/null +++ b/App.xaml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..d7cc151 --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,153 @@ +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Markup; +using System.Windows.Threading; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace CRAWLER; + +public partial class App : Application +{ + private IHost _host; + + public App() + { + ApplyRussianCulture(); + RegisterGlobalExceptionHandlers(); + } + + protected override async void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + try + { + _host = AppHost.Create(); + await _host.StartAsync().ConfigureAwait(true); + + MainWindow = _host.Services.GetRequiredService(); + MainWindow.Show(); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "CRAWLER", MessageBoxButton.OK, MessageBoxImage.Error); + Shutdown(-1); + } + } + + protected override async void OnExit(ExitEventArgs e) + { + try + { + if (_host != null) + { + try + { + await _host.StopAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(true); + } + finally + { + _host.Dispose(); + } + } + } + catch (Exception ex) + { + ShowUnhandledException(ex, true); + } + finally + { + UnregisterGlobalExceptionHandlers(); + base.OnExit(e); + } + } + + private static void ApplyRussianCulture() + { + var culture = new CultureInfo("ru-RU"); + + CultureInfo.DefaultThreadCurrentCulture = culture; + CultureInfo.DefaultThreadCurrentUICulture = culture; + Thread.CurrentThread.CurrentCulture = culture; + Thread.CurrentThread.CurrentUICulture = culture; + + FrameworkElement.LanguageProperty.OverrideMetadata( + typeof(FrameworkElement), + new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(culture.IetfLanguageTag))); + } + + private void RegisterGlobalExceptionHandlers() + { + DispatcherUnhandledException += OnDispatcherUnhandledException; + AppDomain.CurrentDomain.UnhandledException += OnCurrentDomainUnhandledException; + TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; + } + + private void UnregisterGlobalExceptionHandlers() + { + DispatcherUnhandledException -= OnDispatcherUnhandledException; + AppDomain.CurrentDomain.UnhandledException -= OnCurrentDomainUnhandledException; + TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException; + } + + private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + ShowUnhandledException(e.Exception, false); + e.Handled = true; + } + + private void OnCurrentDomainUnhandledException(object sender, UnhandledExceptionEventArgs e) + { + if (e.ExceptionObject is Exception exception) + { + ShowUnhandledException(exception, e.IsTerminating); + return; + } + + MessageBox.Show( + e.ExceptionObject == null ? "Произошла необработанная ошибка." : e.ExceptionObject.ToString(), + e.IsTerminating ? "CRAWLER - критическая ошибка" : "CRAWLER", + MessageBoxButton.OK, + MessageBoxImage.Error); + } + + private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + ShowUnhandledException(e.Exception, false); + e.SetObserved(); + } + + private static void ShowUnhandledException(Exception exception, bool isCritical) + { + var actualException = UnwrapException(exception); + var message = string.IsNullOrWhiteSpace(actualException.Message) + ? actualException.ToString() + : actualException.Message; + + MessageBox.Show( + message, + isCritical ? "CRAWLER - критическая ошибка" : "CRAWLER", + MessageBoxButton.OK, + MessageBoxImage.Error); + } + + private static Exception UnwrapException(Exception exception) + { + if (exception is AggregateException aggregateException) + { + var flattened = aggregateException.Flatten(); + if (flattened.InnerExceptions.Count == 1) + { + return UnwrapException(flattened.InnerExceptions[0]); + } + + return flattened; + } + + return exception; + } +} diff --git a/AppHost.cs b/AppHost.cs new file mode 100644 index 0000000..8b23428 --- /dev/null +++ b/AppHost.cs @@ -0,0 +1,47 @@ +using System; +using CRAWLER.Parsing; +using CRAWLER.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace CRAWLER; + +internal static class AppHost +{ + public static IHost Create() + { + return Host.CreateDefaultBuilder() + .UseContentRoot(AppContext.BaseDirectory) + .ConfigureAppConfiguration((_, config) => + { + config.Sources.Clear(); + config.SetBasePath(AppContext.BaseDirectory); + config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); + config.AddEnvironmentVariables(); + }) + .ConfigureServices((context, services) => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(provider => new MainWindow( + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService())); + }) + .UseDefaultServiceProvider((_, options) => + { + options.ValidateOnBuild = true; + options.ValidateScopes = true; + }) + .Build(); + } +} diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..cc29e7f --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/CRAWLER.csproj b/CRAWLER.csproj new file mode 100644 index 0000000..9aacd80 --- /dev/null +++ b/CRAWLER.csproj @@ -0,0 +1,37 @@ + + + + WinExe + net10.0-windows + disable + enable + true + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/CRAWLER.sln b/CRAWLER.sln new file mode 100644 index 0000000..c2e5296 --- /dev/null +++ b/CRAWLER.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.4.11626.88 stable +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CRAWLER", "CRAWLER.csproj", "{99A6FD50-529F-431C-8F74-7F37BBA2948E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {99A6FD50-529F-431C-8F74-7F37BBA2948E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99A6FD50-529F-431C-8F74-7F37BBA2948E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99A6FD50-529F-431C-8F74-7F37BBA2948E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99A6FD50-529F-431C-8F74-7F37BBA2948E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {607CB39E-759D-4950-B3DE-F3008151509A} + EndGlobalSection +EndGlobal diff --git a/Configuration/CrawlerOptions.cs b/Configuration/CrawlerOptions.cs new file mode 100644 index 0000000..39f17e0 --- /dev/null +++ b/Configuration/CrawlerOptions.cs @@ -0,0 +1,12 @@ +namespace CRAWLER.Configuration; + +internal sealed class CrawlerOptions +{ + public string BaseUrl { get; set; } = "https://www.ktopoverit.ru"; + public string CatalogPathFormat { get; set; } = "/poverka/gosreestr_sredstv_izmereniy?page={0}"; + public int RequestDelayMilliseconds { get; set; } = 350; + public int DefaultPagesToScan { get; set; } = 1; + public string PdfStoragePath { get; set; } = "%LOCALAPPDATA%\\CRAWLER\\PdfStore"; + public int TimeoutSeconds { get; set; } = 30; + public string UserAgent { get; set; } = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) CRAWLER/1.0"; +} diff --git a/Configuration/DatabaseOptions.cs b/Configuration/DatabaseOptions.cs new file mode 100644 index 0000000..ab69dd9 --- /dev/null +++ b/Configuration/DatabaseOptions.cs @@ -0,0 +1,19 @@ +namespace CRAWLER.Configuration; + +internal sealed class DatabaseOptions +{ + public string ApplicationName { get; set; } = "CRAWLER"; + public int CommandTimeoutSeconds { get; set; } = 60; + public int ConnectRetryCount { get; set; } = 3; + public int ConnectRetryIntervalSeconds { get; set; } = 5; + public int ConnectTimeoutSeconds { get; set; } = 15; + public string Database { get; set; } = "CRAWLER"; + public bool Encrypt { get; set; } + public bool IntegratedSecurity { get; set; } = true; + public bool MultipleActiveResultSets { get; set; } = true; + public bool Pooling { get; set; } = true; + public int MaxPoolSize { get; set; } = 100; + public int MinPoolSize { get; set; } + public string Server { get; set; } = @".\SQLEXPRESS"; + public bool TrustServerCertificate { get; set; } = true; +} diff --git a/Dialogs/EditInstrumentWindow.xaml b/Dialogs/EditInstrumentWindow.xaml new file mode 100644 index 0000000..3b489ec --- /dev/null +++ b/Dialogs/EditInstrumentWindow.xaml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +