Extract a per-user background host so indexing can later run outside the window.

Keep the GUI as the index writer for now; mutex, named pipe, and opt-in logon autostart prepare Explorer.Host.exe without two SQLite writers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 17:10:49 +02:00
parent 9efb306979
commit 7c23bc2474
40 changed files with 1586 additions and 140 deletions

View File

@@ -1,21 +1,11 @@
using Explorer.Analysis;
using Explorer.Application;
using Explorer.Domain;
using Explorer.Domain.Abstractions;
using Explorer.FileOperations;
using Explorer.Indexing;
using Explorer.Plugin.Abstractions;
using Explorer.Plugin.GoogleDrive;
using Explorer.Plugin.Nextcloud;
using Explorer.Plugin.OneDrive;
using Explorer.Hosting;
using Explorer.Presentation;
using Explorer.Presentation.ViewModels;
using Explorer.Search;
using Explorer.Storage.Sqlite;
using Explorer.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Explorer.App;
@@ -23,105 +13,20 @@ public static class AppServices
{
public static IServiceCollection AddExplorer(this IServiceCollection services)
{
services.AddSingleton<IClock, SystemClock>();
services.AddSingleton<IAppEnvironment, WindowsAppEnvironment>();
services.AddSingleton<IVolumeService, WindowsVolumeService>();
services.AddSingleton<IFileSystemEnumerator, WindowsFileSystemEnumerator>();
services.AddSingleton<IUsnJournal, WindowsUsnJournal>();
services.AddSingleton<IShellFileOperations, WindowsShellFileOperations>();
services.AddExplorerCore();
services.AddExplorerUi();
return services;
}
public static IServiceCollection AddExplorerUi(this IServiceCollection services)
{
services.AddSingleton<IOsClipboard, Services.WpfClipboard>();
services.AddSingleton<IIndexStore>(sp =>
{
var env = sp.GetRequiredService<IAppEnvironment>();
var logger = sp.GetRequiredService<ILogger<SqliteIndexStore>>();
return new SqliteIndexStore(env.DatabasePath, logger);
});
services.AddSingleton<IStorageProvider, OneDriveStorageProvider>();
services.AddSingleton<IStorageProvider, GoogleDriveStorageProvider>();
services.AddSingleton<IStorageProvider, NextcloudStorageProvider>();
services.AddSingleton<StorageProviderRegistry>();
services.AddSingleton<IHydrationGuard, HydrationGuard>();
services.AddSingleton<IArchiveExecutor, SevenZipArchiveExecutor>();
services.AddSingleton<WindowsGitStatusProvider>();
services.AddSingleton<IGitStatusProvider>(sp => sp.GetRequiredService<WindowsGitStatusProvider>());
services.AddSingleton<IGitCommandProvider>(sp => sp.GetRequiredService<WindowsGitStatusProvider>());
services.AddSingleton<IWorkspaceLauncher, WindowsWorkspaceLauncher>();
services.AddSingleton<IElevatedScanService, WindowsElevatedScanService>();
services.AddSingleton<IRecycleBinCatalog, WindowsRecycleBinCatalog>();
services.AddSingleton<SourceManager>();
services.AddSingleton<PathHistoryStore>();
services.AddSingleton<CloudPlaceStore>();
services.AddSingleton<UiPreferencesStore>();
services.AddSingleton<IArchiveCatalog, ArchiveCatalog>();
services.AddSingleton<ArchiveContentsIndexer>();
services.AddSingleton<BrowseService>();
services.AddSingleton<ThumbnailService>();
services.AddSingleton<IThumbnailService>(sp => sp.GetRequiredService<ThumbnailService>());
services.AddSingleton<FilesystemScanner>();
services.AddSingleton<FolderReconciler>();
services.AddSingleton<UsnChangeApplier>();
services.AddSingleton<IndexingCoordinator>();
services.AddSingleton<DirectoryWatcherHub>();
services.AddSingleton<SearchService>();
services.AddSingleton<AnalysisService>();
services.AddSingleton<IOperationExecutor, NativeFileOperationExecutor>();
services.AddSingleton<TransferQueue>();
services.AddSingleton<FileOperationService>();
services.AddSingleton<RenamePlanner>();
services.AddSingleton<RenameBatchService>();
services.AddSingleton<FolderSyncPlanner>();
services.AddSingleton<FolderSyncService>();
services.AddSingleton<FileOperationProfilePlanner>();
services.AddSingleton<OperationProfileService>();
services.AddSingleton<ReorganizePlanner>();
services.AddSingleton<ReorganizeService>();
services.AddSingleton<DuplicateHashWorker>();
services.AddSingleton<HistoryRollupService>();
services.AddSingleton<MainViewModel>();
services.AddSingleton<MainWindow>();
services.AddHostedService(sp => sp.GetRequiredService<IndexingCoordinator>());
services.AddHostedService(sp => sp.GetRequiredService<TransferQueue>());
services.AddHostedService(sp => sp.GetRequiredService<DuplicateHashWorker>());
services.AddHostedService(sp => sp.GetRequiredService<HistoryRollupService>());
services.AddHostedService(sp => sp.GetRequiredService<ThumbnailService>());
services.AddHostedService<WatcherHostedService>();
return services;
}
}
public sealed class WatcherHostedService : BackgroundService
{
private readonly DirectoryWatcherHub _hub;
private readonly SourceManager _sources;
private readonly TransferQueue _transfers;
private readonly FolderSyncService _sync;
private readonly OperationProfileService _profiles;
public WatcherHostedService(
DirectoryWatcherHub hub,
SourceManager sources,
TransferQueue transfers,
FolderSyncService sync,
OperationProfileService profiles)
{
_hub = hub;
_sources = sources;
_transfers = transfers;
_sync = sync;
_profiles = profiles;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(20));
await _hub.RefreshAsync(stoppingToken).ConfigureAwait(false);
while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false))
{
await _sources.RefreshOnlineStateAsync(forceRefresh: true, stoppingToken).ConfigureAwait(false);
_transfers.NotifyAvailability();
await _sync.TryAutoRunAsync(stoppingToken).ConfigureAwait(false);
await _profiles.TryAutoRunAsync(stoppingToken).ConfigureAwait(false);
await _hub.RefreshAsync(stoppingToken).ConfigureAwait(false);
}
}
}

View File

@@ -29,6 +29,10 @@
<ProjectReference Include="..\Explorer.Application\Explorer.Application.csproj" />
<ProjectReference Include="..\Explorer.Domain\Explorer.Domain.csproj" />
<ProjectReference Include="..\Explorer.FileOperations\Explorer.FileOperations.csproj" />
<ProjectReference Include="..\Explorer.Host\Explorer.Host.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
<ProjectReference Include="..\Explorer.Hosting\Explorer.Hosting.csproj" />
<ProjectReference Include="..\Explorer.Indexing\Explorer.Indexing.csproj" />
<ProjectReference Include="..\Explorer.Plugin.Abstractions\Explorer.Plugin.Abstractions.csproj" />
<ProjectReference Include="..\Explorer.Plugin.GoogleDrive\Explorer.Plugin.GoogleDrive.csproj" />
@@ -39,4 +43,17 @@
<ProjectReference Include="..\Explorer.Storage.Sqlite\Explorer.Storage.Sqlite.csproj" />
<ProjectReference Include="..\Explorer.Windows\Explorer.Windows.csproj" />
</ItemGroup>
<Target Name="CopyExplorerHost" AfterTargets="Build">
<PropertyGroup>
<_HostDir>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\Explorer.Host\bin\$(Configuration)\net10.0-windows\'))</_HostDir>
</PropertyGroup>
<ItemGroup>
<_HostFiles Include="$(_HostDir)Explorer.Host.exe" />
<_HostFiles Include="$(_HostDir)Explorer.Host.dll" />
<_HostFiles Include="$(_HostDir)Explorer.Host.deps.json" />
<_HostFiles Include="$(_HostDir)Explorer.Host.runtimeconfig.json" />
<_HostFiles Include="$(_HostDir)Explorer.Host.pdb" />
</ItemGroup>
<Copy SourceFiles="@(_HostFiles)" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" Condition="Exists('$(_HostDir)Explorer.Host.exe')" />
</Target>
</Project>

View File

@@ -61,6 +61,16 @@
Content="Include archive contents in the index"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="24,0,0,18" FontSize="12"
Text="When enabled, a scan lists files inside ZIP, RAR, 7z, TAR, and similar archives from the archive catalog — files are not extracted. Individual uncompressed sizes are stored. Folder totals still use the archives size on disk. Online-only cloud archives are skipped."/>
<CheckBox x:Name="AutoIndexRemovable" Margin="0,0,0,6"
Content="Automatically index removable drives when they appear"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="24,0,0,18" FontSize="12"
Text="USB and other removable volumes are queued for a full scan when they come online and are not indexed yet. Cloud locations are never auto-indexed. Online-only files are not hydrated."/>
<TextBlock Text="Background host" FontSize="16" FontWeight="SemiBold" Margin="0,8,0,10"/>
<CheckBox x:Name="BackgroundHostAtLogon" Margin="0,0,0,6"
Content="Start Explorer.Host.exe at Windows sign-in"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="24,0,0,18" FontSize="12"
Text="Registers a per-user logon task. The window still owns indexing in this version. If the host starts while this window is open, it exits because the index is already in use."/>
<TextBlock Text="7-Zip" FontSize="16" FontWeight="SemiBold" Margin="0,8,0,10"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="0,0,0,8" FontSize="12"

View File

@@ -1,5 +1,6 @@
using System.Windows;
using Explorer.Application;
using Explorer.Hosting;
using Explorer.Presentation.ViewModels;
namespace Explorer.App;
@@ -20,6 +21,8 @@ public partial class SettingsWindow : Window
GroupNetwork.IsChecked = prefs.GroupNetworkPlaces;
GroupCloud.IsChecked = prefs.GroupCloudPlaces;
IndexArchives.IsChecked = prefs.IndexArchiveContents;
AutoIndexRemovable.IsChecked = prefs.AutoIndexRemovable;
BackgroundHostAtLogon.IsChecked = prefs.BackgroundHostAtLogon;
ShowHidden.IsChecked = prefs.ShowHiddenFiles;
ShowProtected.IsChecked = prefs.ShowProtectedSystemLocations;
AutoClearQueue.IsChecked = prefs.AutoClearQueueWhenDone;
@@ -45,6 +48,8 @@ public partial class SettingsWindow : Window
GroupNetworkPlaces = GroupNetwork.IsChecked == true,
GroupCloudPlaces = GroupCloud.IsChecked == true,
IndexArchiveContents = IndexArchives.IsChecked == true,
AutoIndexRemovable = AutoIndexRemovable.IsChecked == true,
BackgroundHostAtLogon = BackgroundHostAtLogon.IsChecked == true,
ShowHiddenFiles = ShowHidden.IsChecked == true,
ShowProtectedSystemLocations = ShowProtected.IsChecked == true,
AutoClearQueueWhenDone = AutoClearQueue.IsChecked == true,
@@ -52,10 +57,41 @@ public partial class SettingsWindow : Window
GitPath = string.IsNullOrWhiteSpace(GitPath.Text) ? null : GitPath.Text.Trim()
};
await _vm.ApplyPreferencesAsync(prefs).ConfigureAwait(true);
ApplyBackgroundHostAutostart(prefs.BackgroundHostAtLogon);
DialogResult = true;
Close();
}
private void ApplyBackgroundHostAutostart(bool enabled)
{
if (enabled)
{
var exe = HostLogonAutostart.FindHostExecutable();
if (exe is null)
{
MessageBox.Show(
this,
"Explorer.Host.exe was not found next to Explorer Workbench, so the sign-in task was not registered.",
"Background host",
MessageBoxButton.OK,
MessageBoxImage.Warning);
return;
}
if (!HostLogonAutostart.TryRegister(exe, out var error))
{
MessageBox.Show(this, error, "Background host", MessageBoxButton.OK, MessageBoxImage.Warning);
}
return;
}
if (!HostLogonAutostart.TryUnregister(out var unregisterError))
{
MessageBox.Show(this, unregisterError, "Background host", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private void OnBrowseSevenZip(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog