diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..4b6fa0c --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2026-06-18 - [Optimization & Reliability] +**Инсайт:** Синхронные вызовы `Process.Kill()` и `WaitForExit()` в UI-потоке приводили к заметным «зависаниям» интерфейса при остановке нескольких процессов (Zapret + ByeDPI/Warp). Менеджер доменов игнорировал список исключений при синхронизации с движком. +**Действие:** Перевести все команды управления процессами в `async Task`. Использовать `WaitForExitAsync()` для неблокирующего ожидания завершения. Реализовать полную синхронизацию списков (цели + исключения) в `SyncCustomHostlist`. Внедрить debounced-сохранение настроек для предотвращения избыточной дисковой активности. diff --git a/FluxRoute.AI/Services/BanditSelector.cs b/FluxRoute.AI/Services/BanditSelector.cs index 0b3a1f7..54ddb4c 100644 --- a/FluxRoute.AI/Services/BanditSelector.cs +++ b/FluxRoute.AI/Services/BanditSelector.cs @@ -105,7 +105,8 @@ public BanditSelector(AiStrategyRegistry registry, Func? aiSettings return pareto[_rng.Next(pareto.Count)].g; } - return scoredUsable.OrderByDescending(x => x.score).First().g; + // BOLT ⚡: Replacing O(N log N) OrderBy with O(N) MaxBy for better performance. + return scoredUsable.MaxBy(x => x.score).g; } public StrategyGenome? BestKnownForNetwork(IReadOnlyList candidates, string networkHash) diff --git a/FluxRoute/ViewModels/MainViewModel.Ai.cs b/FluxRoute/ViewModels/MainViewModel.Ai.cs index a69c173..7becd15 100644 --- a/FluxRoute/ViewModels/MainViewModel.Ai.cs +++ b/FluxRoute/ViewModels/MainViewModel.Ai.cs @@ -15,27 +15,27 @@ public partial class MainViewModel [ObservableProperty] private bool aiEnabled; partial void OnAiEnabledChanged(bool value) { - SaveSettings(); + SaveSettingsDebounced(); if (value) RebuildAiStrategyRows(); } [ObservableProperty] private bool useUcb1; - partial void OnUseUcb1Changed(bool value) => SaveSettings(); + partial void OnUseUcb1Changed(bool value) => SaveSettingsDebounced(); [ObservableProperty] private int aiExplorationPermil = 100; - partial void OnAiExplorationPermilChanged(int value) => SaveSettings(); + partial void OnAiExplorationPermilChanged(int value) => SaveSettingsDebounced(); [ObservableProperty] private int aiAutoDeleteBelowScore = 60; - partial void OnAiAutoDeleteBelowScoreChanged(int value) => SaveSettings(); + partial void OnAiAutoDeleteBelowScoreChanged(int value) => SaveSettingsDebounced(); [ObservableProperty] private bool useHybridMode; - partial void OnUseHybridModeChanged(bool value) => SaveSettings(); + partial void OnUseHybridModeChanged(bool value) => SaveSettingsDebounced(); [ObservableProperty] private int engineMode; partial void OnEngineModeChanged(int value) { - SaveSettings(); + SaveSettingsDebounced(); var runMode = value switch { (int)DpiEngineMode.Zapret => DpiRunMode.Standalone, @@ -50,58 +50,58 @@ partial void OnEngineModeChanged(int value) } [ObservableProperty] private int byeDpiSocksPort = 1080; - partial void OnByeDpiSocksPortChanged(int value) => SaveSettings(); + partial void OnByeDpiSocksPortChanged(int value) => SaveSettingsDebounced(); [ObservableProperty] private string? byeDpiSplitPos; - partial void OnByeDpiSplitPosChanged(string? value) => SaveSettings(); + partial void OnByeDpiSplitPosChanged(string? value) => SaveSettingsDebounced(); [ObservableProperty] private string? byeDpiDisorderPos = "1"; - partial void OnByeDpiDisorderPosChanged(string? value) => SaveSettings(); + partial void OnByeDpiDisorderPosChanged(string? value) => SaveSettingsDebounced(); [ObservableProperty] private string? byeDpiFakePos; - partial void OnByeDpiFakePosChanged(string? value) => SaveSettings(); + partial void OnByeDpiFakePosChanged(string? value) => SaveSettingsDebounced(); [ObservableProperty] private string? byeDpiOobPos; - partial void OnByeDpiOobPosChanged(string? value) => SaveSettings(); + partial void OnByeDpiOobPosChanged(string? value) => SaveSettingsDebounced(); [ObservableProperty] private string? byeDpiTlsrecPos; - partial void OnByeDpiTlsrecPosChanged(string? value) => SaveSettings(); + partial void OnByeDpiTlsrecPosChanged(string? value) => SaveSettingsDebounced(); [ObservableProperty] private int? byeDpiFakeTtl; - partial void OnByeDpiFakeTtlChanged(int? value) => SaveSettings(); + partial void OnByeDpiFakeTtlChanged(int? value) => SaveSettingsDebounced(); [ObservableProperty] private bool byeDpiAutoTtl; - partial void OnByeDpiAutoTtlChanged(bool value) => SaveSettings(); + partial void OnByeDpiAutoTtlChanged(bool value) => SaveSettingsDebounced(); [ObservableProperty] private string? byeDpiAutoMode = "torst"; - partial void OnByeDpiAutoModeChanged(string? value) => SaveSettings(); + partial void OnByeDpiAutoModeChanged(string? value) => SaveSettingsDebounced(); [ObservableProperty] private int? byeDpiTimeout; - partial void OnByeDpiTimeoutChanged(int? value) => SaveSettings(); + partial void OnByeDpiTimeoutChanged(int? value) => SaveSettingsDebounced(); [ObservableProperty] private string? byeDpiHosts; - partial void OnByeDpiHostsChanged(string? value) => SaveSettings(); + partial void OnByeDpiHostsChanged(string? value) => SaveSettingsDebounced(); [ObservableProperty] private string? byeDpiHostlist; - partial void OnByeDpiHostlistChanged(string? value) => SaveSettings(); + partial void OnByeDpiHostlistChanged(string? value) => SaveSettingsDebounced(); [ObservableProperty] private string? byeDpiFakeTlsMod; - partial void OnByeDpiFakeTlsModChanged(string? value) => SaveSettings(); + partial void OnByeDpiFakeTlsModChanged(string? value) => SaveSettingsDebounced(); [ObservableProperty] private string? byeDpiFakeSni; - partial void OnByeDpiFakeSniChanged(string? value) => SaveSettings(); + partial void OnByeDpiFakeSniChanged(string? value) => SaveSettingsDebounced(); [ObservableProperty] private string? byeDpiFakeData; - partial void OnByeDpiFakeDataChanged(string? value) => SaveSettings(); + partial void OnByeDpiFakeDataChanged(string? value) => SaveSettingsDebounced(); [ObservableProperty] private string? byeDpiModHttp; - partial void OnByeDpiModHttpChanged(string? value) => SaveSettings(); + partial void OnByeDpiModHttpChanged(string? value) => SaveSettingsDebounced(); [ObservableProperty] private int? byeDpiTlsminor; - partial void OnByeDpiTlsminorChanged(int? value) => SaveSettings(); + partial void OnByeDpiTlsminorChanged(int? value) => SaveSettingsDebounced(); [ObservableProperty] private bool byeDpiMd5sig; - partial void OnByeDpiMd5sigChanged(bool value) => SaveSettings(); + partial void OnByeDpiMd5sigChanged(bool value) => SaveSettingsDebounced(); [ObservableProperty] private string aiNetworkLabel = "—"; [ObservableProperty] private string aiGenerationText = "—"; diff --git a/FluxRoute/ViewModels/MainViewModel.Orchestrator.cs b/FluxRoute/ViewModels/MainViewModel.Orchestrator.cs index 89fba0f..ebd3e8a 100644 --- a/FluxRoute/ViewModels/MainViewModel.Orchestrator.cs +++ b/FluxRoute/ViewModels/MainViewModel.Orchestrator.cs @@ -121,7 +121,7 @@ private void OnOrchestratorStatus(object? sender, OrchestratorEventArgs e) if (e.Message.Contains("Сканирование завершено", StringComparison.OrdinalIgnoreCase)) { SortProfileScores(); - SaveSettings(); + SaveSettingsDebounced(); } if (e.IsSwitched && e.NewProfile is not null) @@ -176,10 +176,10 @@ private async Task SwitchProfileAsync(ProfileItem? profile) if (dispatcher is null || dispatcher.HasShutdownStarted || dispatcher.HasShutdownFinished) return; - void SwitchOnUi() + async Task SwitchOnUi() { _suppressOrchestratorStop = true; - Stop(); + await Stop(); _suppressOrchestratorStop = false; if (profile is not null) @@ -191,23 +191,23 @@ void SwitchOnUi() } if (dispatcher.CheckAccess()) - SwitchOnUi(); + await SwitchOnUi(); else - await dispatcher.InvokeAsync(SwitchOnUi).Task.ConfigureAwait(false); + await dispatcher.InvokeAsync(SwitchOnUi).Task.Unwrap().ConfigureAwait(false); // Даём время на завершение WinDivert и winws.exe await Task.Delay(1500).ConfigureAwait(false); - void StartOnUi() + async Task StartOnUi() { if (profile is not null) - Start(); + await Start(); } if (dispatcher.CheckAccess()) - StartOnUi(); + await StartOnUi(); else - await dispatcher.InvokeAsync(StartOnUi).Task.ConfigureAwait(false); + await dispatcher.InvokeAsync(StartOnUi).Task.Unwrap().ConfigureAwait(false); } private Task UpdateProfileScoreAsync(string fileName, int score) @@ -317,19 +317,18 @@ private Task EnsureProtectionRunningAsync() if (dispatcher is null || dispatcher.HasShutdownStarted || dispatcher.HasShutdownFinished) return Task.CompletedTask; - void EnsureOnUi() + async Task EnsureOnUi() { if (SelectedProfile is not null && !IsTrackedProcessRunning()) - Start(); + await Start(); } if (dispatcher.CheckAccess()) { - EnsureOnUi(); - return Task.CompletedTask; + return EnsureOnUi(); } - return dispatcher.InvokeAsync(EnsureOnUi).Task; + return dispatcher.InvokeAsync(EnsureOnUi).Task.Unwrap(); } @@ -375,7 +374,7 @@ private void DeleteAiStrategy(AiStrategyRowVm? row) if (wasActive) { if (IsRunning) - Stop(); + _ = Stop(); SelectedProfile = Profiles.FirstOrDefault(); } @@ -465,7 +464,7 @@ private async Task ScanProfiles() await _orchestrator.ScanAllProfilesAsync(); SortProfileScores(); ScanProgressText = "Сканирование завершено"; - SaveSettings(); + SaveSettingsDebounced(); var top = ProfileScores.FirstOrDefault(s => s.Score > 0); if (top is not null) @@ -561,9 +560,11 @@ internal void ApplyOrchestratorEnabledState() { Logs.Add("[Оркестратор] Переход в ручной режим — перезапуск Zapret..."); _suppressOrchestratorStop = true; - Stop(); - _suppressOrchestratorStop = false; - Start(); + _ = Stop().ContinueWith(_ => + { + _suppressOrchestratorStop = false; + return Start(); + }, TaskScheduler.FromCurrentSynchronizationContext()); } } } diff --git a/FluxRoute/ViewModels/MainViewModel.Process.cs b/FluxRoute/ViewModels/MainViewModel.Process.cs index fa6cc75..39099df 100644 --- a/FluxRoute/ViewModels/MainViewModel.Process.cs +++ b/FluxRoute/ViewModels/MainViewModel.Process.cs @@ -73,16 +73,16 @@ private void RefreshProfiles() } [RelayCommand] - private void ToggleStartStop() + private async Task ToggleStartStop() { if (IsRunning) - Stop(); + await Stop(); else - Start(); + await Start(); } [RelayCommand] - private void Start() + private async Task Start() { if (IsRunning) { @@ -111,13 +111,13 @@ private void Start() if (ProfileBatLauncher.TryCreateLaunchPlan(SelectedProfile.FullPath, EngineDir, out var plan, out var parseError) && plan is not null) { - StartWinwsDirect(plan); + await StartWinwsDirect(plan); return; } Logs.Add($"⚠️ Прямой запуск winws.exe недоступен: {parseError}"); Logs.Add("⚠️ Использую совместимый запуск через BAT/cmd.exe."); - StartViaBatFallback(); + await StartViaBatFallback(); } catch (Exception ex) { @@ -126,7 +126,7 @@ private void Start() } } - private void StartWinwsDirect(WinwsLaunchPlan plan) + private async Task StartWinwsDirect(WinwsLaunchPlan plan) { var winws = ProfileBatLauncher.StartWinws(plan); _startedViaBatFallback = false; @@ -142,7 +142,7 @@ private void StartWinwsDirect(WinwsLaunchPlan plan) TryStartOrchestratorIfEnabled(); if (UseHybridMode) - _ = StartByeDpiAsync(); + await StartByeDpiAsync(); Application.Current?.Dispatcher.BeginInvoke(() => { @@ -182,7 +182,7 @@ private async Task StartByeDpiAsync() } } - private void StartViaBatFallback() + private async Task StartViaBatFallback() { var psi = new ProcessStartInfo { @@ -223,7 +223,7 @@ private void StartViaBatFallback() }, System.Windows.Threading.DispatcherPriority.Background); if (UseHybridMode) - _ = StartByeDpiAsync(); + await StartByeDpiAsync(); } private async Task TrackDirectWinwsAsync(Process winws) @@ -377,7 +377,7 @@ await RunOnUiThreadAsync(() => } [RelayCommand] - private void Stop() + private async Task Stop() { _hideWindowsCts?.Cancel(); @@ -425,7 +425,9 @@ private void Stop() { using var p = Process.GetProcessById((int)pid); p.Kill(entireProcessTree: true); - p.WaitForExit(1500); + + using var cts = new CancellationTokenSource(2000); + await p.WaitForExitAsync(cts.Token); killed++; } catch @@ -448,7 +450,7 @@ private void Stop() IsRunning = false; _runStartedAt = null; - try { _ = _engineManager.StopAllAsync(); } + try { await _engineManager.StopAllAsync(); } catch { } if (!_suppressOrchestratorStop) diff --git a/FluxRoute/ViewModels/MainViewModel.cs b/FluxRoute/ViewModels/MainViewModel.cs index 8e052d6..638e20c 100644 --- a/FluxRoute/ViewModels/MainViewModel.cs +++ b/FluxRoute/ViewModels/MainViewModel.cs @@ -133,9 +133,9 @@ private async Task ApplyPreset(ConfigPreset preset) // 3. Перезапустить защиту if (IsRunning) { - Stop(); + await Stop(); await Task.Delay(1200).ConfigureAwait(false); - Application.Current.Dispatcher.BeginInvoke(() => Start()); + await Start(); } AddToRecentLogs($"✅ Пресет «{preset.Name}» применён"); @@ -255,8 +255,7 @@ partial void OnSelectedProfileChanged(ProfileItem? oldValue, ProfileItem? newVal if (!_suppressProfileWarning && _settingsLoaded && IsRunning && newValue is not null) { - Stop(); - Start(); + _ = Stop().ContinueWith(_ => Start(), TaskScheduler.FromCurrentSynchronizationContext()); } } @@ -341,14 +340,14 @@ partial void OnSelectedTabIndexChanged(int value) [ObservableProperty] private bool orchestratorEnabled; partial void OnOrchestratorEnabledChanged(bool value) { - SaveSettings(); + SaveSettingsDebounced(); if (_settingsLoaded) ApplyOrchestratorEnabledState(); } [ObservableProperty] private string orchestratorStatus = "Не запущен"; [ObservableProperty] private string orchestratorNextCheck = "—"; [ObservableProperty] private string orchestratorInterval = "1"; - partial void OnOrchestratorIntervalChanged(string value) => SaveSettings(); + partial void OnOrchestratorIntervalChanged(string value) => SaveSettingsDebounced(); [ObservableProperty] private bool isScanning; [ObservableProperty] private string scanProgressText = ""; public string OrchestratorToggleLabel => OrchestratorRunning ? "Остановить оркестратор" : "Запустить оркестратор"; @@ -356,21 +355,21 @@ partial void OnOrchestratorEnabledChanged(bool value) // ── Настройки сайтов ── [ObservableProperty] private bool siteYouTube = true; - partial void OnSiteYouTubeChanged(bool value) => SaveSettings(); + partial void OnSiteYouTubeChanged(bool value) => SaveSettingsDebounced(); [ObservableProperty] private bool siteDiscord = true; - partial void OnSiteDiscordChanged(bool value) => SaveSettings(); + partial void OnSiteDiscordChanged(bool value) => SaveSettingsDebounced(); [ObservableProperty] private bool siteGoogle = true; - partial void OnSiteGoogleChanged(bool value) => SaveSettings(); + partial void OnSiteGoogleChanged(bool value) => SaveSettingsDebounced(); [ObservableProperty] private bool siteTwitch = true; - partial void OnSiteTwitchChanged(bool value) => SaveSettings(); + partial void OnSiteTwitchChanged(bool value) => SaveSettingsDebounced(); [ObservableProperty] private bool siteInstagram = true; - partial void OnSiteInstagramChanged(bool value) => SaveSettings(); + partial void OnSiteInstagramChanged(bool value) => SaveSettingsDebounced(); [ObservableProperty] private bool siteTelegram = true; - partial void OnSiteTelegramChanged(bool value) => SaveSettings(); + partial void OnSiteTelegramChanged(bool value) => SaveSettingsDebounced(); // ── Свои сайты ── [ObservableProperty] private string userCustomSitesText = ""; - partial void OnUserCustomSitesTextChanged(string value) => SaveSettings(); + partial void OnUserCustomSitesTextChanged(string value) => SaveSettingsDebounced(); private readonly OrchestratorService _orchestrator; private readonly AiOrchestratorService _aiOrchestrator; @@ -424,11 +423,11 @@ public string GameFilterProtocol // ── Обновления ── [ObservableProperty] private bool autoUpdateEnabled = false; - partial void OnAutoUpdateEnabledChanged(bool value) => SaveSettings(); + partial void OnAutoUpdateEnabledChanged(bool value) => SaveSettingsDebounced(); // ── Предупреждение при смене профиля ── [ObservableProperty] private bool showProfileSwitchWarning = true; - partial void OnShowProfileSwitchWarningChanged(bool value) => SaveSettings(); + partial void OnShowProfileSwitchWarningChanged(bool value) => SaveSettingsDebounced(); private bool _suppressProfileWarning; // ── Системные ── @@ -436,10 +435,10 @@ public string GameFilterProtocol partial void OnAutoStartEnabledChanged(bool value) { AutoStartService.SetEnabled(value); - SaveSettings(); + SaveSettingsDebounced(); } [ObservableProperty] private bool minimizeToTray = true; - partial void OnMinimizeToTrayChanged(bool value) => SaveSettings(); + partial void OnMinimizeToTrayChanged(bool value) => SaveSettingsDebounced(); // ── Обновления (wrappers → UpdatesViewModel) ── public string UpdateStatus => Updates.UpdateStatus; @@ -616,6 +615,13 @@ public MainViewModel( _orchestratorUiTimer.Tick += (_, _) => UpdateOrchestratorNextCheck(); _orchestratorUiTimer.Start(); + + _saveSettingsDebounce.Tick += (s, e) => + { + _saveSettingsDebounce.Stop(); + SaveSettings(); + }; + _aiOrchestrator.SyncRegistryFromEngine(); RebuildAiStrategyRows(); InitializeTgProxyOnStartup(); @@ -725,6 +731,13 @@ private void ApplySettings(AppSettings settings) Presets.Add(p); } + public void SaveSettingsDebounced() + { + if (!_settingsLoaded) return; + _saveSettingsDebounce.Stop(); + _saveSettingsDebounce.Start(); + } + public void SaveSettings() { if (!_settingsLoaded) return; @@ -813,10 +826,10 @@ private void OpenEngineFolder() private void ToggleLogs() => IsLogsVisible = !IsLogsVisible; [RelayCommand] - private void MainAction() + private async Task MainAction() { - if (IsRunning) Stop(); - else Start(); + if (IsRunning) await Stop(); + else await Start(); } @@ -828,20 +841,25 @@ private void AddToRecentLogs(string message) LastStatusMessage = message; } - // ── Синхронизация пользовательских доменов с движком (winws.exe) ── // ── Синхронизация пользовательских доменов с движком (winws.exe) ── private void SyncCustomHostlist() + { + SyncHostlistFile("list-general-user.txt", CustomTargetDomains, false); + SyncHostlistFile("list-exclude-user.txt", CustomExcludeDomains, true); + } + + private void SyncHostlistFile(string fileName, ObservableCollection sourceList, bool isExclusion) { try { var listsDir = Path.Combine(EngineDir, "lists"); Directory.CreateDirectory(listsDir); - var userHostlistPath = Path.Combine(listsDir, "list-general-user.txt"); + var filePath = Path.Combine(listsDir, fileName); var domains = new HashSet(StringComparer.OrdinalIgnoreCase); - // 1. Берем домены из нового Менеджера доменов (вкладка "Домены") - foreach (var d in CustomTargetDomains) + // 1. Берем из переданного списка + foreach (var d in sourceList) { if (!string.IsNullOrWhiteSpace(d)) domains.Add(d.Trim()); @@ -852,36 +870,38 @@ private void SyncCustomHostlist() { var legacy = UserCustomSitesText .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Where(s => !s.StartsWith("!")); + .Where(s => isExclusion ? s.StartsWith("!") : !s.StartsWith("!")); + foreach (var d in legacy) - domains.Add(d.Trim()); + { + var domain = isExclusion ? d.TrimStart('!').Trim() : d.Trim(); + if (!string.IsNullOrWhiteSpace(domain)) + domains.Add(domain); + } } // Записываем в файл if (domains.Count > 0) { - // Используем явное удаление + запись, чтобы избежать кэширования - if (File.Exists(userHostlistPath)) + if (File.Exists(filePath)) { - try { File.SetAttributes(userHostlistPath, FileAttributes.Normal); } catch { } + try { File.SetAttributes(filePath, FileAttributes.Normal); } catch { } } - File.WriteAllLines(userHostlistPath, domains.OrderBy(x => x), new UTF8Encoding(false)); - Logs.Add($"[Sync] Записано {domains.Count} доменов в list-general-user.txt"); + File.WriteAllLines(filePath, domains.OrderBy(x => x), new UTF8Encoding(false)); + Logs.Add($"[Sync] Записано {domains.Count} доменов в {fileName}"); } else { - // Пустой список — удаляем файл или пишем комментарий - if (File.Exists(userHostlistPath)) - File.Delete(userHostlistPath); + if (File.Exists(filePath)) + File.Delete(filePath); else - File.WriteAllText(userHostlistPath, "# custom domains empty\n", new UTF8Encoding(false)); - Logs.Add("[Sync] list-general-user.txt очищен"); + File.WriteAllText(filePath, "# custom domains empty\n", new UTF8Encoding(false)); + Logs.Add($"[Sync] {fileName} очищен"); } } catch (Exception ex) { - Logs.Add($"❌ Ошибка записи list-general-user.txt: {ex.Message}"); - Logs.Add($" Stack: {ex.StackTrace?.Split('\n')[0]}"); + Logs.Add($"❌ Ошибка записи {fileName}: {ex.Message}"); } } diff --git a/FluxRoute/ViewModels/UpdatesViewModel.cs b/FluxRoute/ViewModels/UpdatesViewModel.cs index 9ecdff4..8c93275 100644 --- a/FluxRoute/ViewModels/UpdatesViewModel.cs +++ b/FluxRoute/ViewModels/UpdatesViewModel.cs @@ -20,7 +20,7 @@ public sealed partial class UpdatesViewModel : ObservableObject private readonly Func _getAutoUpdateEnabled; private readonly Func _getCurrentEngineVersion; private readonly Action _setCurrentEngineVersion; - private readonly Action _stopEngine; + private readonly Func _stopEngine; private readonly Action _loadProfiles; private readonly Action _refreshDiagnostics; private readonly Action _addAppLog; @@ -69,7 +69,7 @@ public UpdatesViewModel( Func getAutoUpdateEnabled, Func getCurrentEngineVersion, Action setCurrentEngineVersion, - Action stopEngine, + Func stopEngine, Action loadProfiles, Action refreshDiagnostics, Action addAppLog, @@ -218,7 +218,7 @@ internal async Task InstallUpdateAsync() { if (_pendingUpdate is null) return; IsUpdating = true; - _stopEngine(); + await _stopEngine(); var success = await _updater.InstallUpdateAsync(EngineDir, _pendingUpdate, msg =>