mirror of
https://github.com/8damon/Blackbird-Platform
synced 2026-06-21 13:41:12 +00:00
5506 lines
232 KiB
C#
5506 lines
232 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
using System.Windows.Input;
|
|
using System.Windows.Threading;
|
|
|
|
namespace BlackbirdInterface
|
|
{
|
|
public partial class MainWindow : Window, IIntelDetailsProvider
|
|
{
|
|
private readonly TelemetryEventStore _allEvents = new();
|
|
private readonly BulkObservableCollection<TelemetryEvent> _focusedEvents = new();
|
|
private readonly Dictionary<EventSelectionKey, IReadOnlyList<TelemetryEvent>> _focusedClusterMembers = new();
|
|
private readonly BoundedStringPool _telemetryTextPool = new(8192);
|
|
private readonly HashSet<string> _knownLaneKeys = new(StringComparer.Ordinal);
|
|
private const double DefaultTimelineViewDurationSeconds = 120;
|
|
private const int MaxControllerLaunchArgumentChars = 2048;
|
|
private const int HookControllerCallTimeoutSeconds = 30;
|
|
private static readonly TimeSpan HookControllerCallTimeout =
|
|
TimeSpan.FromSeconds(HookControllerCallTimeoutSeconds);
|
|
private bool _viewportRefreshPending;
|
|
private DateTime _latestEventTimestampUtc;
|
|
|
|
private DateTime _captureStartUtc;
|
|
private EventsFloatWindow? _eventsFloatWindow;
|
|
private EventLogWindow? _eventLogWindow;
|
|
private PerformanceFloatWindow? _performanceFloatWindow;
|
|
private EtwFloatWindow? _etwFloatWindow;
|
|
private HeuristicsFloatWindow? _heuristicsFloatWindow;
|
|
private ChildProcessGraphWindow? _childProcessGraphWindow;
|
|
private bool _eventsPaneVisible = true;
|
|
private bool _performancePaneVisible = true;
|
|
private bool _eventsPaneFloating;
|
|
private bool _performancePaneFloating;
|
|
private bool _etwPaneFloating;
|
|
private bool _heuristicsPaneFloating;
|
|
private bool _heuristicsOnTop;
|
|
|
|
private string? _laneFocusKey;
|
|
|
|
private PerformanceSampler? _perf;
|
|
|
|
private readonly ObservableCollection<GraphExplorerItem> _explorer = new();
|
|
private readonly ObservableCollection<ProcessSessionTab> _processTabs = new();
|
|
private readonly int _defaultPid = Process.GetCurrentProcess().Id;
|
|
private bool _suppressTabSelectionChange;
|
|
private ProcessSessionTab? _currentSession;
|
|
private int _samplerPid;
|
|
private bool _performanceOnTop;
|
|
private bool _hasPerformanceData;
|
|
private bool _connectivityHealthy = true;
|
|
private bool _draggingEventsPaneHeader;
|
|
private bool _draggingPerformancePaneHeader;
|
|
private bool _openingProcessPicker;
|
|
private bool _connectInProgress;
|
|
private bool _pendingLaunchOptions;
|
|
private bool _pendingUseUsermodeHooks;
|
|
private bool _pendingAutoOpenApiGraph;
|
|
private bool _pendingHookPreconfigured;
|
|
private bool _pendingLaunchStartsSuspended;
|
|
private bool _pendingLeaveSuspendedAfterReady;
|
|
private bool _pendingLaunchOwnedByInterface;
|
|
private bool _hookControllerCallInFlight;
|
|
private AnalysisOperationGuard? _activeAnalysisOperation;
|
|
private LiveAnalysisSessionLease? _liveAnalysisSession;
|
|
private ProcessSessionTab? _queuedSessionSwitchTab;
|
|
private bool _sessionSwitchQueued;
|
|
private LaunchTargetKind _pendingAnalysisSubjectKind = LaunchTargetKind.Executable;
|
|
private string _pendingAnalysisSubjectPath = string.Empty;
|
|
private string _pendingAnalysisHostPath = string.Empty;
|
|
private bool _isMainWindowShuttingDown;
|
|
private BlackbirdBackendSession? _preparedLaunchBackendSession;
|
|
private int _preparedLaunchBackendPid;
|
|
private MainInterfaceViewMode _mainViewMode = MainInterfaceViewMode.Telemetry;
|
|
private readonly BulkObservableCollection<ApiCallGraphMainRowView> _apiViewRows = new();
|
|
private readonly BulkObservableCollection<ExtendedActivityRowSnapshot> _extendedViewRows = new();
|
|
private readonly BulkObservableCollection<ExtendedActivityRowSnapshot> _extendedComRows = new();
|
|
private readonly BulkObservableCollection<ExtendedActivityRowSnapshot> _extendedEtwRows = new();
|
|
private readonly BulkObservableCollection<ExtendedActivityRowSnapshot> _extendedJobRows = new();
|
|
private readonly BulkObservableCollection<ExtendedActivityRowSnapshot> _extendedYaraRows = new();
|
|
private Process? _targetExitWatchProcess;
|
|
private int _targetExitWatchPid;
|
|
private Vector _floatingPaneDragOffset;
|
|
private readonly DispatcherTimer _detachedEventLogRefreshTimer;
|
|
private readonly DispatcherTimer _processStateRefreshTimer;
|
|
private readonly DispatcherTimer _apiGraphRefreshTimer;
|
|
private readonly DispatcherTimer _extendedViewRefreshTimer;
|
|
private readonly DispatcherTimer _childProcessGraphRefreshTimer;
|
|
private readonly DispatcherTimer _timelineLiveTickTimer;
|
|
private bool _scrollSyncPending;
|
|
private bool _topTimeTravelSyncing;
|
|
private bool _toolbarViewMenuSyncing;
|
|
private bool _followLiveTimeline = true;
|
|
private bool _eventSelectionSyncing;
|
|
private EventSelectionKey? _selectedEventAnchor;
|
|
private double _pendingScrollStartSeconds;
|
|
private bool _targetExecutionSuspended;
|
|
private bool _signatureIntelEnabled = true;
|
|
private bool _signatureIntelMemoryScanEnabled;
|
|
private bool _signatureIntelPageScanEnabled;
|
|
private readonly List<ApiCallGraphMainRowView> _apiViewSnapshotRows = new();
|
|
private readonly List<ApiCallGraphRowSnapshot> _apiGraphSnapshotRows = new();
|
|
private ApiViewPresentationMode _apiViewPresentationMode;
|
|
private DateTime _scopeStatusCacheUtc;
|
|
private int _scopeStatusCachePid;
|
|
private IntelScopeStatus _scopeStatusCache = IntelScopeStatus.Unknown;
|
|
private static readonly Brush ProcessStateRunningBackground =
|
|
new SolidColorBrush(Color.FromRgb(0x14, 0x3A, 0x1E));
|
|
private static readonly Brush ProcessStateRunningBorder = new SolidColorBrush(Color.FromRgb(0x49, 0xC1, 0x66));
|
|
private static readonly Brush ProcessStateRunningForeground =
|
|
new SolidColorBrush(Color.FromRgb(0xA9, 0xF5, 0xB8));
|
|
private static readonly Brush ProcessStateWaitingBackground =
|
|
new SolidColorBrush(Color.FromRgb(0x3D, 0x34, 0x16));
|
|
private static readonly Brush ProcessStateWaitingBorder = new SolidColorBrush(Color.FromRgb(0xDE, 0xC2, 0x62));
|
|
private static readonly Brush ProcessStateWaitingForeground =
|
|
new SolidColorBrush(Color.FromRgb(0xF8, 0xE9, 0xAF));
|
|
private static readonly Brush ProcessStateExitedBackground =
|
|
new SolidColorBrush(Color.FromRgb(0x28, 0x1A, 0x1A));
|
|
private static readonly Brush ProcessStateExitedBorder = new SolidColorBrush(Color.FromRgb(0x73, 0x4A, 0x4A));
|
|
private static readonly Brush ProcessStateExitedForeground =
|
|
new SolidColorBrush(Color.FromRgb(0xE1, 0xB4, 0xB4));
|
|
private static readonly Brush ProcessStateUnknownBackground =
|
|
new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x1E));
|
|
private static readonly Brush ProcessStateUnknownBorder = new SolidColorBrush(Color.FromRgb(0x4A, 0x4A, 0x4A));
|
|
private static readonly Brush ProcessStateUnknownForeground =
|
|
new SolidColorBrush(Color.FromRgb(0xB8, 0xB8, 0xB8));
|
|
private static readonly Brush ToolbarInactiveBackground = new SolidColorBrush(Color.FromRgb(0x18, 0x18, 0x18));
|
|
private static readonly Brush ToolbarInactiveBorder = new SolidColorBrush(Color.FromRgb(0x46, 0x46, 0x46));
|
|
private static readonly Brush ToolbarInactiveForeground = new SolidColorBrush(Color.FromRgb(0x7A, 0x7A, 0x7A));
|
|
private static readonly Brush ToolbarTargetBackground = new SolidColorBrush(Color.FromRgb(0x4C, 0x14, 0x18));
|
|
private static readonly Brush ToolbarTargetBorder = new SolidColorBrush(Color.FromRgb(0xE0, 0x52, 0x5A));
|
|
private static readonly Brush ToolbarTargetForeground = new SolidColorBrush(Color.FromRgb(0xFF, 0xC6, 0xCB));
|
|
private static readonly Brush ToolbarPauseBackground = new SolidColorBrush(Color.FromRgb(0x3B, 0x24, 0x11));
|
|
private static readonly Brush ToolbarPauseBorder = new SolidColorBrush(Color.FromRgb(0xE1, 0x92, 0x2C));
|
|
private static readonly Brush ToolbarPauseForeground = new SolidColorBrush(Color.FromRgb(0xF0, 0xAB, 0x4B));
|
|
private static readonly Brush ToolbarResumeBackground = new SolidColorBrush(Color.FromRgb(0x14, 0x35, 0x1C));
|
|
private static readonly Brush ToolbarResumeBorder = new SolidColorBrush(Color.FromRgb(0x4C, 0xC5, 0x68));
|
|
private static readonly Brush ToolbarResumeForeground = new SolidColorBrush(Color.FromRgb(0x98, 0xEF, 0xAD));
|
|
private static readonly Brush ToolbarStopBackground = new SolidColorBrush(Color.FromRgb(0x3D, 0x15, 0x1A));
|
|
private static readonly Brush ToolbarStopBorder = new SolidColorBrush(Color.FromRgb(0xD6, 0x43, 0x43));
|
|
private static readonly Brush ToolbarStopForeground = new SolidColorBrush(Color.FromRgb(0xF0, 0x74, 0x74));
|
|
private const string WindowTitleBase = "Blackbird";
|
|
|
|
private const uint ProcessTerminate = 0x0001;
|
|
private const uint ProcessSynchronize = 0x00100000;
|
|
private const uint ProcessSuspendResume = 0x0800;
|
|
private const uint ProcessVmRead = 0x0010;
|
|
private const uint ProcessQueryLimitedInformation = 0x1000;
|
|
|
|
private enum MainInterfaceViewMode
|
|
{
|
|
Telemetry = 0,
|
|
Api = 1,
|
|
Extended = 2
|
|
}
|
|
|
|
private enum ApiViewPresentationMode
|
|
{
|
|
CallGraph = 0,
|
|
ThreadTimeline = 1
|
|
}
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
if (Kernel32Native.EnableDebugPrivilege(out int debugPrivilegeError))
|
|
{
|
|
DiagnosticsState.SetValue("Target Handle Access", "SeDebugPrivilege enabled");
|
|
}
|
|
else
|
|
{
|
|
DiagnosticsState.SetValue("Target Handle Access",
|
|
$"SeDebugPrivilege unavailable win32={debugPrivilegeError}");
|
|
}
|
|
if (ApiViewModeBox != null)
|
|
{
|
|
ApiViewModeBox.SelectedIndex = 0;
|
|
}
|
|
InitializeSignatureIntelSubsystem();
|
|
InitializeInterfaceSettings();
|
|
SetResourceReference(BackgroundProperty, "WinBgBrush");
|
|
RootGrid.SetResourceReference(Panel.BackgroundProperty, "WinBgBrush");
|
|
WindowThemeHelper.WireThemeAwareTitleBar(this);
|
|
_detachedEventLogRefreshTimer =
|
|
new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(110) };
|
|
_detachedEventLogRefreshTimer.Tick += DetachedEventLogRefreshTimer_Tick;
|
|
_processStateRefreshTimer =
|
|
new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(900) };
|
|
_processStateRefreshTimer.Tick += (_, __) =>
|
|
{
|
|
ValidateCurrentSessionState();
|
|
RefreshProcessStateBadge();
|
|
};
|
|
_apiGraphRefreshTimer =
|
|
new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(180) };
|
|
_apiGraphRefreshTimer.Tick += ApiGraphRefreshTimer_Tick;
|
|
_extendedViewRefreshTimer =
|
|
new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(220) };
|
|
_extendedViewRefreshTimer.Tick += (_, __) => FlushExtendedActivitySnapshot();
|
|
_childProcessGraphRefreshTimer =
|
|
new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(140) };
|
|
_childProcessGraphRefreshTimer.Tick += ChildProcessGraphRefreshTimer_Tick;
|
|
_timelineLiveTickTimer =
|
|
new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(250) };
|
|
_timelineLiveTickTimer.Tick += (_, __) =>
|
|
{
|
|
if (_followLiveTimeline && _allEvents.Count > 0 && !_viewportRefreshPending)
|
|
{
|
|
UpdateScrollBar();
|
|
double viewport = Math.Max(1, EventsPaneHost?.Scroll?.ViewportSize ?? 1);
|
|
ApplyTimelineViewport(ComputeTimelineMaxStart(viewport), syncScroll: true,
|
|
updateFollowState: false);
|
|
}
|
|
};
|
|
Loaded += OnLoaded;
|
|
Closing += OnClosing;
|
|
Closed += OnClosed;
|
|
RefreshToolbarCommandState();
|
|
}
|
|
|
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
|
{
|
|
PidBox.Text = string.Empty;
|
|
_samplerPid = 0;
|
|
|
|
_captureStartUtc = AnchorCaptureStartUtc(DefaultTimelineViewDurationSeconds);
|
|
_latestEventTimestampUtc = _captureStartUtc;
|
|
|
|
EventsPaneHost.Grid.ItemsSource = _focusedEvents;
|
|
EventsPaneHost.SetHasData(false);
|
|
EventsPaneHost.SetEventLogDetached(false);
|
|
|
|
EventsPaneHost.Timeline.CaptureStartUtc = _captureStartUtc;
|
|
EventsPaneHost.Timeline.ViewDurationSeconds = DefaultTimelineViewDurationSeconds;
|
|
EventsPaneHost.Timeline.ViewStartSeconds = 0;
|
|
|
|
EventsPaneHost.Timeline.LaneInteraction += Timeline_LaneInteraction;
|
|
EventsPaneHost.Timeline.SelectedEventChanged += Timeline_SelectedEventChanged;
|
|
EventsPaneHost.Timeline.EventDoubleClicked += Timeline_EventDoubleClicked;
|
|
|
|
EventsPaneHost.Grid.SelectionChanged += Grid_SelectionChanged;
|
|
|
|
EventsPaneHost.Scroll.ValueChanged += Scroll_ValueChanged;
|
|
|
|
EventsPaneHost.CloseRequested += (_, __) => HideEventsPane();
|
|
EventsPaneHost.FloatRequested += (_, __) => ToggleFloatDock();
|
|
EventsPaneHost.LogPopoutRequested += (_, __) => ToggleEventLogDock();
|
|
EventsPaneHost.SettingsRequested += (_, __) => OpenLaneSettings();
|
|
EventsPaneHost.LaneFilterSelectionChanged += (_, key) => SetLaneFocus(key);
|
|
EventsPaneHost.EventLogEntryOpenRequested += EventsPaneHost_EventLogEntryOpenRequested;
|
|
EventsPaneHost.HeaderDragStarted += (_, a) => BeginPaneHeaderDrag(isEventsPane: true, a.ScreenPosition);
|
|
EventsPaneHost.HeaderDragDelta += (_, a) => ContinuePaneHeaderDrag(isEventsPane: true, a.ScreenPosition);
|
|
EventsPaneHost.HeaderDragCompleted += (_, a) => EndPaneHeaderDrag(isEventsPane: true, a.ScreenPosition);
|
|
PerformancePaneHost.CloseRequested += (_, __) => HidePerformancePane();
|
|
PerformancePaneHost.ReorderRequested += (_, __) => TogglePaneOrder();
|
|
PerformancePaneHost.FloatRequested += (_, __) => TogglePerformanceFloatDock();
|
|
PerformancePaneHost.ThreadDoubleClicked += PerformancePaneHost_ThreadDoubleClicked;
|
|
PerformancePaneHost.DisassemblyRequested += PerformancePaneHost_DisassemblyRequested;
|
|
PerformancePaneHost.ParallelStacksRequested += (_, __) => OpenParallelStacksWindow();
|
|
PerformancePaneHost.HeaderDragStarted += (_, a) =>
|
|
BeginPaneHeaderDrag(isEventsPane: false, a.ScreenPosition);
|
|
PerformancePaneHost.HeaderDragDelta += (_, a) =>
|
|
ContinuePaneHeaderDrag(isEventsPane: false, a.ScreenPosition);
|
|
PerformancePaneHost.HeaderDragCompleted += (_, a) =>
|
|
EndPaneHeaderDrag(isEventsPane: false, a.ScreenPosition);
|
|
EtwPaneHost.ReorderRequested += (_, __) => ToggleIntelPaneOrder();
|
|
EtwPaneHost.FloatRequested += (_, __) => ToggleEtwFloatDock();
|
|
EtwPaneHost.CloseRequested += (_, __) => HideEtwPane();
|
|
EtwPaneHost.InspectRequested += (_, __) => OpenEtwInspector();
|
|
HeuristicsPaneHost.ReorderRequested += (_, __) => ToggleIntelPaneOrder();
|
|
HeuristicsPaneHost.FloatRequested += (_, __) => ToggleHeuristicsFloatDock();
|
|
HeuristicsPaneHost.CloseRequested += (_, __) => HideHeuristicsPane();
|
|
HeuristicsPaneHost.InspectRequested += (_, __) => OpenHeuristicsInspector();
|
|
FilesystemPaneHost.CloseRequested += (_, __) => HideFilesystemPane();
|
|
FilesystemPaneHost.InspectRequested += (_, __) => OpenFilesystemInspector();
|
|
RegistryPaneHost.CloseRequested += (_, __) => HideRegistryPane();
|
|
RegistryPaneHost.InspectRequested += (_, __) => OpenRegistryInspector();
|
|
ProcessRelationsPaneHost.CloseRequested += (_, __) => HideProcessRelationsPane();
|
|
ProcessRelationsPaneHost.InspectRequested += (_, __) => OpenProcessRelationsInspector();
|
|
ProcessRelationsPaneHost.GraphStateChanged += ProcessRelationsPaneHost_GraphStateChanged;
|
|
SetupExplorer();
|
|
SetupProcessTabs();
|
|
InitializeBackendUi();
|
|
InitializeRuntimeConfigDefaults();
|
|
ApiViewDataGrid.ItemsSource = _apiViewRows;
|
|
if (ExtendedViewDataGrid != null)
|
|
{
|
|
ExtendedViewDataGrid.ItemsSource = _extendedViewRows;
|
|
}
|
|
if (ExtendedComDataGrid != null)
|
|
{
|
|
ExtendedComDataGrid.ItemsSource = _extendedComRows;
|
|
}
|
|
if (ExtendedEtwDataGrid != null)
|
|
{
|
|
ExtendedEtwDataGrid.ItemsSource = _extendedEtwRows;
|
|
}
|
|
if (ExtendedJobDataGrid != null)
|
|
{
|
|
ExtendedJobDataGrid.ItemsSource = _extendedJobRows;
|
|
}
|
|
if (ExtendedYaraDataGrid != null)
|
|
{
|
|
ExtendedYaraDataGrid.ItemsSource = _extendedYaraRows;
|
|
}
|
|
ApiViewDataGrid.SelectedIndex = -1;
|
|
UpdateApiViewSelection(null);
|
|
SetMainInterfaceViewMode(MainInterfaceViewMode.Telemetry);
|
|
ApplyUplinkStatusVisual(healthy: null);
|
|
|
|
UpdateScrollBar();
|
|
FocusViewport();
|
|
UpdateTopTimeTravelBar();
|
|
RebuildToolbarViewMenuOptions();
|
|
|
|
_perf = new PerformanceSampler();
|
|
_perf.SampleArrived += Perf_SampleArrived;
|
|
|
|
PerformancePaneHost.SetCaptureStart(_captureStartUtc);
|
|
PerformancePaneHost.SetPid(TryGetPid());
|
|
PerformancePaneHost.SetProcessLiveDataAvailable(false);
|
|
PerformancePaneHost.SetTargetSuspended(false);
|
|
SyncPerformanceViewToTimeline();
|
|
StatusBlock.Text = "NO TARGET SELECTED";
|
|
ApplyPaneOrder();
|
|
ApplyIntelPaneOrder();
|
|
_processStateRefreshTimer.Start();
|
|
_apiGraphRefreshTimer.Start();
|
|
_timelineLiveTickTimer.Start();
|
|
RefreshProcessStateBadge();
|
|
}
|
|
|
|
private void SetupProcessTabs()
|
|
{
|
|
ProcessTabs.ItemsSource = _processTabs;
|
|
_currentSession = null;
|
|
}
|
|
|
|
private void OnClosing(object? sender, CancelEventArgs e)
|
|
{
|
|
_ = sender;
|
|
if (!PrepareSessionShutdown())
|
|
{
|
|
e.Cancel = true;
|
|
return;
|
|
}
|
|
|
|
_isMainWindowShuttingDown = true;
|
|
CancelActiveAnalysisOperationForShutdown();
|
|
}
|
|
|
|
private void OnClosed(object? sender, EventArgs e)
|
|
{
|
|
_isMainWindowShuttingDown = true;
|
|
CancelActiveAnalysisOperationForShutdown();
|
|
DisposeSignatureIntelSubsystem();
|
|
_detachedEventLogRefreshTimer.Stop();
|
|
_processStateRefreshTimer.Stop();
|
|
_apiGraphRefreshTimer.Stop();
|
|
_timelineLiveTickTimer.Stop();
|
|
|
|
if (_perf != null)
|
|
{
|
|
_perf.SampleArrived -= Perf_SampleArrived;
|
|
_perf.Stop();
|
|
_perf = null;
|
|
}
|
|
|
|
if (_eventsFloatWindow != null)
|
|
{
|
|
_eventsFloatWindow.Closing -= EventsFloatWindow_Closing;
|
|
_eventsFloatWindow.Closed -= EventsFloatWindow_Closed;
|
|
if (_eventsFloatWindow.Content == EventsPaneHost)
|
|
{
|
|
_eventsFloatWindow.Content = null;
|
|
}
|
|
_eventsFloatWindow.Close();
|
|
_eventsFloatWindow = null;
|
|
}
|
|
if (_eventLogWindow != null)
|
|
{
|
|
_eventLogWindow.Closed -= EventLogWindow_Closed;
|
|
_eventLogWindow.Close();
|
|
_eventLogWindow = null;
|
|
}
|
|
if (_performanceFloatWindow != null)
|
|
{
|
|
_performanceFloatWindow.Closing -= PerformanceFloatWindow_Closing;
|
|
_performanceFloatWindow.Closed -= PerformanceFloatWindow_Closed;
|
|
if (_performanceFloatWindow.Content == PerformancePaneHost)
|
|
{
|
|
_performanceFloatWindow.Content = null;
|
|
}
|
|
_performanceFloatWindow.Close();
|
|
_performanceFloatWindow = null;
|
|
}
|
|
if (_etwFloatWindow != null)
|
|
{
|
|
_etwFloatWindow.Closing -= EtwFloatWindow_Closing;
|
|
_etwFloatWindow.Closed -= EtwFloatWindow_Closed;
|
|
if (_etwFloatWindow.Content == EtwPaneHost)
|
|
{
|
|
_etwFloatWindow.Content = null;
|
|
}
|
|
_etwFloatWindow.Close();
|
|
_etwFloatWindow = null;
|
|
}
|
|
if (_heuristicsFloatWindow != null)
|
|
{
|
|
_heuristicsFloatWindow.Closing -= HeuristicsFloatWindow_Closing;
|
|
_heuristicsFloatWindow.Closed -= HeuristicsFloatWindow_Closed;
|
|
if (_heuristicsFloatWindow.Content == HeuristicsPaneHost)
|
|
{
|
|
_heuristicsFloatWindow.Content = null;
|
|
}
|
|
_heuristicsFloatWindow.Close();
|
|
_heuristicsFloatWindow = null;
|
|
}
|
|
if (_childProcessGraphWindow != null)
|
|
{
|
|
SaveChildProcessGraphStateToSession();
|
|
_childProcessGraphWindow.Closed -= ChildProcessGraphWindow_Closed;
|
|
_childProcessGraphWindow.Close();
|
|
_childProcessGraphWindow = null;
|
|
}
|
|
ProcessRelationsPaneHost.GraphStateChanged -= ProcessRelationsPaneHost_GraphStateChanged;
|
|
_childProcessGraphRefreshTimer.Stop();
|
|
SaveIntelSessionState(_currentSession?.Pid ?? 0);
|
|
StopLiveAnalysisSession(LiveAnalysisStopReason.Shutdown, fastTeardown: true, stopPerformance: true);
|
|
DisposePreparedLaunchBackendSession();
|
|
TerminateLaunchOwnedTargetsOnShutdown();
|
|
HideDockPreview();
|
|
CleanupTemporarySessionBackingStores();
|
|
}
|
|
|
|
private void SetupExplorer()
|
|
{
|
|
_explorer.Clear();
|
|
|
|
_explorer.Add(new GraphExplorerItem(
|
|
"Events", new SolidColorBrush(Color.FromRgb(0xAA, 0x7D, 0x4A))) { IsEnabled = true });
|
|
_explorer.Add(new GraphExplorerItem(
|
|
"Performance", new SolidColorBrush(Color.FromRgb(0x58, 0xB6, 0x58))) { IsEnabled = true });
|
|
_explorer.Add(new GraphExplorerItem(
|
|
"ETW", new SolidColorBrush(Color.FromRgb(0xD2, 0x89, 0x34))) { IsEnabled = true });
|
|
_explorer.Add(new GraphExplorerItem(
|
|
"Heuristics", new SolidColorBrush(Color.FromRgb(0xD2, 0x55, 0x55))) { IsEnabled = true });
|
|
_explorer.Add(new GraphExplorerItem(
|
|
"Filesystem", new SolidColorBrush(Color.FromRgb(0x45, 0x8E, 0x7A))) { IsEnabled = true });
|
|
_explorer.Add(new GraphExplorerItem(
|
|
"Registry", new SolidColorBrush(Color.FromRgb(0x7A, 0x5E, 0xA8))) { IsEnabled = true });
|
|
_explorer.Add(new GraphExplorerItem(
|
|
"Process Relations", new SolidColorBrush(Color.FromRgb(0xD2, 0xB8, 0x55))) { IsEnabled = true });
|
|
|
|
GraphExplorer.ItemsSource = _explorer;
|
|
|
|
foreach (var item in _explorer)
|
|
{
|
|
item.PropertyChanged += (_, args) =>
|
|
{
|
|
if (string.Equals(args.PropertyName, nameof(GraphExplorerItem.IsEnabled), StringComparison.Ordinal))
|
|
{
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
};
|
|
}
|
|
|
|
RefreshExplorerDataBadges();
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private GraphExplorerItem? FindExplorerItem(string name) =>
|
|
_explorer.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
|
|
|
|
private void SetExplorerHasData(string name, bool hasData)
|
|
{
|
|
var item = FindExplorerItem(name);
|
|
if (item != null)
|
|
{
|
|
item.HasData = hasData;
|
|
if (!hasData)
|
|
{
|
|
item.ClearPreviewValues();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RefreshExplorerDataBadges()
|
|
{
|
|
SetExplorerHasData("Events", _allEvents.Count > 0);
|
|
SetExplorerHasData("Performance",
|
|
_hasPerformanceData || (_currentSession?.PerformanceHistory.Count ?? 0) > 0);
|
|
SetExplorerHasData("ETW", EtwPaneHost.ItemCount > 0);
|
|
SetExplorerHasData("Heuristics", HeuristicsPaneHost.ItemCount > 0);
|
|
SetExplorerHasData("Filesystem", FilesystemPaneHost.ItemCount > 0);
|
|
SetExplorerHasData("Process Relations", ProcessRelationsPaneHost.ItemCount > 0);
|
|
}
|
|
|
|
private void ApplyDockVisibilityFromExplorer()
|
|
{
|
|
bool showEvents = _explorer.FirstOrDefault(x => x.Name == "Events")?.IsEnabled ?? true;
|
|
bool showPerf = _explorer.FirstOrDefault(x => x.Name == "Performance")?.IsEnabled ?? true;
|
|
bool showEtw = _explorer.FirstOrDefault(x => x.Name == "ETW")?.IsEnabled ?? true;
|
|
bool showHeuristics = _explorer.FirstOrDefault(x => x.Name == "Heuristics")?.IsEnabled ?? true;
|
|
bool showFilesystem = _explorer.FirstOrDefault(x => x.Name == "Filesystem")?.IsEnabled ?? true;
|
|
bool showRegistry = _explorer.FirstOrDefault(x => x.Name == "Registry")?.IsEnabled ?? true;
|
|
bool showRelations = _explorer.FirstOrDefault(x => x.Name == "Process Relations")?.IsEnabled ?? true;
|
|
|
|
if (!showEvents && _eventsFloatWindow != null)
|
|
_eventsFloatWindow.Close();
|
|
if (!showEvents && _eventLogWindow != null)
|
|
_eventLogWindow.Close();
|
|
if (!showPerf && _performanceFloatWindow != null)
|
|
_performanceFloatWindow.Close();
|
|
if (!showEtw && _etwFloatWindow != null)
|
|
_etwFloatWindow.Close();
|
|
if (!showHeuristics && _heuristicsFloatWindow != null)
|
|
_heuristicsFloatWindow.Close();
|
|
|
|
bool showEventsContent = showEvents && _eventsPaneVisible && !_eventsPaneFloating;
|
|
bool showPerformanceContent = showPerf && _performancePaneVisible && !_performancePaneFloating;
|
|
bool showEtwContent = showEtw && !_etwPaneFloating;
|
|
bool showHeuristicsContent = showHeuristics && !_heuristicsPaneFloating;
|
|
bool showFilesystemContent = showFilesystem;
|
|
bool showRegistryContent = showRegistry;
|
|
bool showRelationsContent = showRelations;
|
|
|
|
EventsDockBorder.Visibility = showEventsContent ? Visibility.Visible : Visibility.Collapsed;
|
|
PerformanceDockBorder.Visibility = showPerformanceContent ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
CollapsedEventsBar.Visibility =
|
|
(showEvents && !_eventsPaneVisible && !_eventsPaneFloating) ? Visibility.Visible : Visibility.Collapsed;
|
|
CollapsedPerformanceBar.Visibility = (showPerf && !_performancePaneVisible && !_performancePaneFloating)
|
|
? Visibility.Visible
|
|
: Visibility.Collapsed;
|
|
|
|
DockGrid.RowDefinitions[0].Height =
|
|
showEvents ? (_eventsPaneFloating
|
|
? new GridLength(0)
|
|
: (_eventsPaneVisible ? new GridLength(4, GridUnitType.Star) : new GridLength(28)))
|
|
: new GridLength(0);
|
|
|
|
DockGrid.RowDefinitions[2].Height =
|
|
showPerf ? (_performancePaneFloating
|
|
? new GridLength(0)
|
|
: (_performancePaneVisible ? new GridLength(5, GridUnitType.Star) : new GridLength(28)))
|
|
: new GridLength(0);
|
|
|
|
DockGrid.RowDefinitions[1].Height =
|
|
(showEventsContent && showPerformanceContent) ? new GridLength(2) : new GridLength(0);
|
|
|
|
EtwDockBorder.Visibility = showEtwContent ? Visibility.Visible : Visibility.Collapsed;
|
|
HeuristicsDockBorder.Visibility = showHeuristicsContent ? Visibility.Visible : Visibility.Collapsed;
|
|
FilesystemDockBorder.Visibility = showFilesystemContent ? Visibility.Visible : Visibility.Collapsed;
|
|
RegistryDockBorder.Visibility = showRegistryContent ? Visibility.Visible : Visibility.Collapsed;
|
|
ProcessRelationsDockBorder.Visibility = showRelationsContent ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
bool row0Visible = (Grid.GetRow(EtwDockBorder) == 0 && showEtwContent) ||
|
|
(Grid.GetRow(HeuristicsDockBorder) == 0 && showHeuristicsContent);
|
|
bool row2Visible = (Grid.GetRow(EtwDockBorder) == 2 && showEtwContent) ||
|
|
(Grid.GetRow(HeuristicsDockBorder) == 2 && showHeuristicsContent);
|
|
bool row4Visible = showFilesystemContent;
|
|
bool row6Visible = showRegistryContent;
|
|
bool row8Visible = showRelationsContent;
|
|
IntelligenceDock.RowDefinitions[0].Height =
|
|
row0Visible ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
|
|
IntelligenceDock.RowDefinitions[2].Height =
|
|
row2Visible ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
|
|
IntelligenceDock.RowDefinitions[4].Height =
|
|
row4Visible ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
|
|
IntelligenceDock.RowDefinitions[6].Height =
|
|
row6Visible ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
|
|
IntelligenceDock.RowDefinitions[8].Height =
|
|
row8Visible ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
|
|
IntelligenceDock.RowDefinitions[1].Height =
|
|
(row0Visible && row2Visible) ? new GridLength(2) : new GridLength(0);
|
|
IntelligenceDock.RowDefinitions[3].Height =
|
|
(row2Visible && row4Visible) ? new GridLength(2) : new GridLength(0);
|
|
IntelligenceDock.RowDefinitions[5].Height =
|
|
(row4Visible && row6Visible) ? new GridLength(2) : new GridLength(0);
|
|
IntelligenceDock.RowDefinitions[7].Height =
|
|
(row6Visible && row8Visible) ? new GridLength(2) : new GridLength(0);
|
|
|
|
bool showIntel = row0Visible || row2Visible || row4Visible || row6Visible || row8Visible;
|
|
if (_mainViewMode != MainInterfaceViewMode.Telemetry)
|
|
{
|
|
showIntel = false;
|
|
}
|
|
IntelligenceColumn.Width = showIntel ? new GridLength(560) : new GridLength(0);
|
|
IntelligenceSplitterColumn.Width = showIntel ? new GridLength(2) : new GridLength(0);
|
|
IntelligenceSplitter.Visibility = showIntel ? Visibility.Visible : Visibility.Collapsed;
|
|
IntelligenceDock.Visibility = showIntel ? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
|
|
private void GraphExplorer_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
var element = e.OriginalSource as DependencyObject;
|
|
if (element == null)
|
|
return;
|
|
|
|
var container = ItemsControl.ContainerFromElement(GraphExplorer, element) as ListBoxItem;
|
|
if (container?.DataContext is not GraphExplorerItem item)
|
|
return;
|
|
|
|
item.IsEnabled = !item.IsEnabled;
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void Perf_SampleArrived(object? sender, PerformanceSample e)
|
|
{
|
|
if (!Dispatcher.CheckAccess())
|
|
{
|
|
Dispatcher.BeginInvoke(new Action(() => Perf_SampleArrived(sender, e)));
|
|
return;
|
|
}
|
|
|
|
if (_currentSession != null)
|
|
{
|
|
_captureProjection?.ObservePerformance(e);
|
|
_currentSession.PerformanceHistory.Add(ClonePerformanceSample(e));
|
|
if (_currentSession.PerformanceHistory.Count > 4000)
|
|
_currentSession.PerformanceHistory.RemoveRange(0, _currentSession.PerformanceHistory.Count - 4000);
|
|
}
|
|
|
|
var evCount = _focusedEvents.Count;
|
|
var eventsItem = _explorer.FirstOrDefault(x => x.Name == "Events");
|
|
eventsItem?.PushPreviewValue(evCount);
|
|
|
|
var perfItem = _explorer.FirstOrDefault(x => x.Name == "Performance");
|
|
perfItem?.PushPreviewValue(e.CpuPercent);
|
|
_hasPerformanceData = true;
|
|
SetExplorerHasData("Performance", true);
|
|
|
|
PerformancePaneHost.PushSample(e);
|
|
|
|
SyncPerformanceViewToTimeline();
|
|
}
|
|
|
|
private void StartLiveCaptureForPid(int pid, bool useUsermodeHooks, bool launchStartsSuspended = false,
|
|
bool fastBackendRestart = false)
|
|
{
|
|
if (pid <= 0 || _perf == null)
|
|
return;
|
|
|
|
_targetExecutionSuspended = launchStartsSuspended;
|
|
|
|
if (_currentSession != null && _currentSession.Pid == pid && _currentSession.Events.Count == 0 &&
|
|
_currentSession.PerformanceHistory.Count == 0 && _currentSession.ThreadLifecycleHistory.Count == 0)
|
|
{
|
|
double viewDuration = Math.Max(1, EventsPaneHost.Timeline.ViewDurationSeconds);
|
|
_captureStartUtc = AnchorCaptureStartUtc(viewDuration);
|
|
_latestEventTimestampUtc = _captureStartUtc;
|
|
_currentSession.CaptureStartUtc = _captureStartUtc;
|
|
_currentSession.ViewDurationSeconds = viewDuration;
|
|
_currentSession.ViewStartSeconds = 0;
|
|
|
|
EventsPaneHost.Timeline.CaptureStartUtc = _captureStartUtc;
|
|
EventsPaneHost.Timeline.ViewDurationSeconds = viewDuration;
|
|
EventsPaneHost.Timeline.ViewStartSeconds = 0;
|
|
EventsPaneHost.Scroll.Value = 0;
|
|
UpdateScrollBar();
|
|
FocusViewport();
|
|
PerformancePaneHost.SetCaptureStart(_captureStartUtc);
|
|
SyncPerformanceViewToTimeline();
|
|
}
|
|
|
|
_perf.SetTargetPid(pid);
|
|
_perf.Start();
|
|
_samplerPid = pid;
|
|
PerformancePaneHost.SetProcessLiveDataAvailable(true);
|
|
PerformancePaneHost.SetTargetSuspended(launchStartsSuspended);
|
|
PerformancePaneHost.RefreshLiveProcessDetails();
|
|
StartTargetExitWatcher(pid);
|
|
EnsureLiveCaptureStoreForCurrentSession(pid);
|
|
StartBackendForPid(pid, useUsermodeHooks, fastStopExistingSession: fastBackendRestart);
|
|
if (_backendSession != null)
|
|
{
|
|
TrackLiveAnalysisSession(pid, _currentSession);
|
|
}
|
|
bool autoOpenApiGraph = _currentSession?.AutoOpenApiGraphOnNextStart == true;
|
|
if (_currentSession != null)
|
|
{
|
|
_currentSession.AutoOpenApiGraphOnNextStart = false;
|
|
}
|
|
SetMainInterfaceViewMode(useUsermodeHooks && autoOpenApiGraph ? MainInterfaceViewMode.Api
|
|
: MainInterfaceViewMode.Telemetry);
|
|
StatusBlock.Text = $"LIVE CAPTURE: {NormalizeSessionTitle(_currentSession?.Title ?? $"PID {pid}")}";
|
|
RefreshProcessStateBadge();
|
|
}
|
|
|
|
private void StartTargetExitWatcher(int pid)
|
|
{
|
|
StopTargetExitWatcher();
|
|
if (pid <= 0)
|
|
return;
|
|
|
|
Process? process = null;
|
|
try
|
|
{
|
|
process = Process.GetProcessById(pid);
|
|
process.EnableRaisingEvents = true;
|
|
process.Exited += TargetExitWatchProcess_Exited;
|
|
_targetExitWatchProcess = process;
|
|
_targetExitWatchPid = pid;
|
|
process = null; // ownership transferred to _targetExitWatchProcess
|
|
if (_targetExitWatchProcess.HasExited)
|
|
{
|
|
HandleTargetProcessExit(pid, BuildProcessExitReason(_targetExitWatchProcess));
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
process?.Dispose();
|
|
_targetExitWatchProcess?.Dispose();
|
|
_targetExitWatchProcess = null;
|
|
_targetExitWatchPid = 0;
|
|
if (!Dispatcher.HasShutdownStarted)
|
|
Dispatcher.BeginInvoke(new Action(ValidateCurrentSessionState));
|
|
}
|
|
}
|
|
|
|
private void StopTargetExitWatcher()
|
|
{
|
|
if (_targetExitWatchProcess != null)
|
|
{
|
|
try
|
|
{
|
|
_targetExitWatchProcess.Exited -= TargetExitWatchProcess_Exited;
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
try
|
|
{
|
|
_targetExitWatchProcess.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
_targetExitWatchProcess = null;
|
|
_targetExitWatchPid = 0;
|
|
}
|
|
|
|
private void TargetExitWatchProcess_Exited(object? sender, EventArgs e)
|
|
{
|
|
int pid = 0;
|
|
string exitReason = string.Empty;
|
|
if (sender is Process p)
|
|
{
|
|
try
|
|
{
|
|
pid = p.Id;
|
|
exitReason = BuildProcessExitReason(p);
|
|
}
|
|
catch
|
|
{
|
|
pid = 0;
|
|
}
|
|
}
|
|
|
|
if (pid <= 0)
|
|
pid = _targetExitWatchPid;
|
|
|
|
Dispatcher.BeginInvoke(new Action(() => HandleTargetProcessExit(pid, exitReason)));
|
|
}
|
|
|
|
private static string BuildProcessExitReason(Process process)
|
|
{
|
|
try
|
|
{
|
|
return $"process exited exitCode=0x{unchecked((uint)process.ExitCode):X8}";
|
|
}
|
|
catch
|
|
{
|
|
return "process exited; exit code unavailable";
|
|
}
|
|
}
|
|
|
|
private void HandleTargetProcessExit(int pid, string? observedReason = null)
|
|
{
|
|
if (pid <= 0)
|
|
return;
|
|
|
|
ProcessSessionTab? exitedTab = _processTabs.FirstOrDefault(x => x.Pid == pid);
|
|
if (exitedTab == null || exitedTab.TargetExited)
|
|
return;
|
|
|
|
exitedTab.TargetExited = true;
|
|
_targetExitReasonByPid.TryGetValue(unchecked((uint)pid), out string? remembered);
|
|
string exitReason = !string.IsNullOrWhiteSpace(remembered)
|
|
? remembered
|
|
: (!string.IsNullOrWhiteSpace(observedReason)
|
|
? observedReason.Trim()
|
|
: "process exited; cause not observed before teardown");
|
|
exitedTab.TargetExitReason = exitReason;
|
|
MarkSessionExited(exitedTab);
|
|
|
|
if (!ReferenceEquals(_currentSession, exitedTab))
|
|
{
|
|
if (exitedTab.ActivePauseStartUtc.HasValue)
|
|
{
|
|
exitedTab.PausedTimelineSpans.Add(
|
|
new PausedTimelineSpan { StartUtc = exitedTab.ActivePauseStartUtc.Value,
|
|
EndUtc = DateTime.UtcNow });
|
|
exitedTab.ActivePauseStartUtc = null;
|
|
}
|
|
SaveTabToBackingStore(exitedTab);
|
|
RefreshProcessStateBadge();
|
|
return;
|
|
}
|
|
|
|
AppendEvent(new TelemetryEvent { TimestampUtc = DateTime.UtcNow, PID = pid, TID = 0, Group = "Session",
|
|
SubType = "ProcessExit", Summary = "TARGET PROCESS EXITED",
|
|
Details = $"{exitReason}. Data capture stopped for this tab." });
|
|
|
|
StopLiveAnalysisSession(LiveAnalysisStopReason.TargetExited, preserveApiGraphSnapshot: true,
|
|
fastTeardown: true, stopPerformance: true);
|
|
if (exitedTab.ActivePauseStartUtc.HasValue)
|
|
{
|
|
exitedTab.PausedTimelineSpans.Add(
|
|
new PausedTimelineSpan { StartUtc = exitedTab.ActivePauseStartUtc.Value,
|
|
EndUtc = DateTime.UtcNow });
|
|
exitedTab.ActivePauseStartUtc = null;
|
|
}
|
|
SyncCurrentSessionStateToMemory();
|
|
_ = TryPersistCurrentSessionAfterStop();
|
|
|
|
StatusBlock.Text = $"Capture stopped; target exited: {exitReason}";
|
|
RefreshProcessStateBadge();
|
|
ThemedMessageBox.ShowToast(this, $"Target process {pid} exited: {exitReason}", "Target Exited",
|
|
MessageBoxImage.Warning, durationMs: 5000);
|
|
}
|
|
|
|
private void ValidateCurrentSessionState()
|
|
{
|
|
if (_currentSession == null || _currentSession.Pid <= 0 || _currentSession.OfflineSnapshot ||
|
|
_currentSession.TargetExited)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (TryOpenTargetProcess(_currentSession.Pid, out _, out _, out bool accessDenied))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!accessDenied)
|
|
{
|
|
HandleTargetProcessExit(_currentSession.Pid);
|
|
}
|
|
}
|
|
|
|
private static void MarkSessionExited(ProcessSessionTab tab)
|
|
{
|
|
tab.Title = NormalizeSessionTitle(tab.Title);
|
|
}
|
|
|
|
private bool TryOpenTargetProcess(int pid, out string processName, out string failure, out bool accessDenied)
|
|
{
|
|
processName = string.Empty;
|
|
failure = string.Empty;
|
|
accessDenied = false;
|
|
|
|
Process? process = null;
|
|
try
|
|
{
|
|
process = Process.GetProcessById(pid);
|
|
processName = process.ProcessName;
|
|
if (process.HasExited)
|
|
{
|
|
failure = $"TARGET PID {pid} HAS EXITED";
|
|
return false;
|
|
}
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
failure = $"PID {pid} NOT FOUND";
|
|
return false;
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
failure = $"PID {pid} IS NOT AVAILABLE";
|
|
return false;
|
|
}
|
|
catch (Win32Exception ex)
|
|
{
|
|
if (ex.NativeErrorCode != 5)
|
|
{
|
|
failure = $"FAILED TO OPEN PID {pid} (WIN32 {ex.NativeErrorCode})";
|
|
return false;
|
|
}
|
|
|
|
processName = $"PID {pid}";
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
process?.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
IntPtr handle = Kernel32Native.OpenProcess(ProcessQueryLimitedInformation | ProcessSynchronize, false,
|
|
unchecked((uint)pid));
|
|
if (handle == IntPtr.Zero)
|
|
{
|
|
int err = Marshal.GetLastWin32Error();
|
|
accessDenied = err == 5;
|
|
failure = accessDenied ? $"ACCESS DENIED TO PID {pid}" : $"FAILED TO OPEN PID {pid} (WIN32 {err})";
|
|
return false;
|
|
}
|
|
|
|
_ = Kernel32Native.CloseHandle(handle);
|
|
return true;
|
|
}
|
|
|
|
private int TryGetPid()
|
|
{
|
|
if (PidBox == null)
|
|
return 0;
|
|
var s = PidBox.Text?.Trim();
|
|
if (string.IsNullOrWhiteSpace(s))
|
|
return _currentSession?.Pid ?? 0;
|
|
if (int.TryParse(s, out int pid) && pid > 0)
|
|
return pid;
|
|
return 0;
|
|
}
|
|
|
|
private string GetProcessTabTitle(int pid)
|
|
{
|
|
try
|
|
{
|
|
using var p = Process.GetProcessById(pid);
|
|
return $"{p.ProcessName} (Silo)";
|
|
}
|
|
catch
|
|
{
|
|
return $"PID {pid} (Silo)";
|
|
}
|
|
}
|
|
|
|
private void FlashSwitchToast(string label)
|
|
{
|
|
SwitchToastText.Text = label;
|
|
SwitchToastBorder.Visibility = Visibility.Visible;
|
|
|
|
var fadeIn = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(120));
|
|
var fadeOut = new DoubleAnimation(
|
|
1, 0, TimeSpan.FromMilliseconds(300)) { BeginTime = TimeSpan.FromMilliseconds(900) };
|
|
fadeOut.Completed += (_, __) => SwitchToastBorder.Visibility = Visibility.Collapsed;
|
|
|
|
var storyboard = new Storyboard();
|
|
Storyboard.SetTarget(fadeIn, SwitchToastBorder);
|
|
Storyboard.SetTargetProperty(fadeIn, new PropertyPath(OpacityProperty));
|
|
Storyboard.SetTarget(fadeOut, SwitchToastBorder);
|
|
Storyboard.SetTargetProperty(fadeOut, new PropertyPath(OpacityProperty));
|
|
storyboard.Children.Add(fadeIn);
|
|
storyboard.Children.Add(fadeOut);
|
|
storyboard.Begin();
|
|
}
|
|
|
|
private ProcessSessionTab AddOrSelectProcessTab(int pid, string title, bool select)
|
|
{
|
|
var existing = _processTabs.FirstOrDefault(t => t.Pid == pid);
|
|
if (existing == null)
|
|
{
|
|
double initialDuration = EventsPaneHost?.Timeline != null
|
|
? Math.Clamp(EventsPaneHost.Timeline.ViewDurationSeconds, 1, 120)
|
|
: DefaultTimelineViewDurationSeconds;
|
|
existing = new ProcessSessionTab { Pid = pid,
|
|
Title = NormalizeSessionTitle(title),
|
|
CaptureStartUtc = AnchorCaptureStartUtc(initialDuration),
|
|
ViewDurationSeconds = initialDuration,
|
|
ViewStartSeconds = 0,
|
|
KernelHooksEnabled = _kernelHooksArmed,
|
|
SignatureIntelEnabled = _signatureIntelEnabled,
|
|
SignatureIntelMemoryScanEnabled = _signatureIntelMemoryScanEnabled,
|
|
SignatureIntelPageScanEnabled = _signatureIntelPageScanEnabled };
|
|
_processTabs.Add(existing);
|
|
}
|
|
else
|
|
{
|
|
existing.Title = NormalizeSessionTitle(title);
|
|
}
|
|
|
|
if (select)
|
|
{
|
|
_suppressTabSelectionChange = true;
|
|
ProcessTabs.SelectedItem = existing;
|
|
_suppressTabSelectionChange = false;
|
|
}
|
|
|
|
return existing;
|
|
}
|
|
|
|
private static string NormalizeSessionTitle(string? title)
|
|
{
|
|
string value = string.IsNullOrWhiteSpace(title) ? "PID" : title.Trim();
|
|
while (value.EndsWith("[EXITED]", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
value = value[..^ "[EXITED]".Length].TrimEnd();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
private void SyncCurrentSessionViewportStateToMemory()
|
|
{
|
|
if (_currentSession == null)
|
|
return;
|
|
|
|
_currentSession.CaptureStartUtc = _captureStartUtc;
|
|
_currentSession.LaneFocusKey = _laneFocusKey;
|
|
_currentSession.ViewDurationSeconds = EventsPaneHost.Timeline.ViewDurationSeconds;
|
|
_currentSession.ViewStartSeconds = EventsPaneHost.Timeline.ViewStartSeconds;
|
|
SaveIntelSessionState(_currentSession.Pid);
|
|
}
|
|
|
|
private void SyncCurrentSessionStateToMemory()
|
|
{
|
|
if (_currentSession == null)
|
|
return;
|
|
|
|
SyncCurrentSessionViewportStateToMemory();
|
|
if (!CurrentSessionEventMirrorLooksCurrent())
|
|
{
|
|
_currentSession.Events.Clear();
|
|
_currentSession.Events.AddRange(_allEvents.Select(CloneTelemetryEvent));
|
|
}
|
|
}
|
|
|
|
private bool CurrentSessionEventMirrorLooksCurrent()
|
|
{
|
|
if (_currentSession == null || _currentSession.Events.Count != _allEvents.Count)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_allEvents.Count == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
TelemetryEvent mirrored = _currentSession.Events[^1];
|
|
TelemetryEvent live = _allEvents[^1];
|
|
return mirrored.TimestampUtc == live.TimestampUtc && mirrored.PID == live.PID &&
|
|
mirrored.TID == live.TID && string.Equals(mirrored.Group, live.Group, StringComparison.Ordinal) &&
|
|
string.Equals(mirrored.SubType, live.SubType, StringComparison.Ordinal);
|
|
}
|
|
|
|
private void SaveCurrentSessionState()
|
|
{
|
|
if (_currentSession == null)
|
|
return;
|
|
|
|
SyncCurrentSessionStateToMemory();
|
|
if (_currentSession.OfflineSnapshot || _liveCaptureStore == null)
|
|
{
|
|
SaveTabToBackingStore(_currentSession);
|
|
}
|
|
}
|
|
|
|
private bool TryPersistCurrentSessionAfterStop()
|
|
{
|
|
if (_currentSession == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
SyncCurrentSessionStateToMemory();
|
|
SaveTabToBackingStore(_currentSession);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void RestoreSessionState(ProcessSessionTab tab)
|
|
{
|
|
EnsureSessionMaterialized(tab);
|
|
double restoredDuration = Math.Clamp(
|
|
tab.ViewDurationSeconds <= 0 ? DefaultTimelineViewDurationSeconds : tab.ViewDurationSeconds, 1, 120);
|
|
bool freshSession = !tab.OfflineSnapshot && tab.Events.Count == 0 && tab.PerformanceHistory.Count == 0 &&
|
|
tab.ThreadLifecycleHistory.Count == 0;
|
|
if (freshSession)
|
|
{
|
|
tab.CaptureStartUtc = AnchorCaptureStartUtc(restoredDuration);
|
|
tab.ViewStartSeconds = 0;
|
|
}
|
|
|
|
_captureStartUtc =
|
|
tab.CaptureStartUtc == default ? AnchorCaptureStartUtc(restoredDuration) : tab.CaptureStartUtc;
|
|
_laneFocusKey = tab.LaneFocusKey;
|
|
|
|
_allEvents.Clear();
|
|
_knownLaneKeys.Clear();
|
|
_telemetryTextPool.Clear();
|
|
_focusedEvents.Clear();
|
|
EventsPaneHost.Timeline.Items.Clear();
|
|
EventsPaneHost.Timeline.ClearAllLaneFilters();
|
|
ClearSelectedEvent();
|
|
|
|
IEnumerable<TelemetryEvent> restoreEvents =
|
|
TelemetryEventsAreTimestampSorted(tab.Events) ? tab.Events : tab.Events.OrderBy(x => x.TimestampUtc);
|
|
foreach (var ev in restoreEvents)
|
|
{
|
|
_allEvents.Add(NormalizeTelemetryEventForStore(ev));
|
|
}
|
|
_latestEventTimestampUtc = _allEvents.Count > 0 ? _allEvents[^1].TimestampUtc : _captureStartUtc;
|
|
|
|
EventsPaneHost.Timeline.CaptureStartUtc = _captureStartUtc;
|
|
EventsPaneHost.Timeline.ViewDurationSeconds = restoredDuration;
|
|
EventsPaneHost.Timeline.ViewStartSeconds = Math.Max(0, tab.ViewStartSeconds);
|
|
ApplyPausedTimelineRanges();
|
|
UpdateScrollBar();
|
|
EventsPaneHost.Scroll.Value =
|
|
Math.Min(EventsPaneHost.Scroll.Maximum, EventsPaneHost.Timeline.ViewStartSeconds);
|
|
FocusViewport();
|
|
|
|
PerformancePaneHost.SetCaptureStart(_captureStartUtc);
|
|
PerformancePaneHost.SetPid(tab.Pid);
|
|
PerformancePaneHost.SetAnalysisSubject(tab.AnalysisSubjectPath, tab.AnalysisHostPath);
|
|
DiagnosticsState.SetValue("Analysis Subject", tab.AnalysisSubjectKind == LaunchTargetKind.Dll &&
|
|
!string.IsNullOrWhiteSpace(tab.AnalysisSubjectPath)
|
|
? tab.AnalysisSubjectPath
|
|
: "Process image");
|
|
PerformancePaneHost.LoadHistory(tab.PerformanceHistory);
|
|
PerformancePaneHost.LoadMemoryRegionAttributionHistory(tab.MemoryRegionAttributionHistory);
|
|
PerformancePaneHost.LoadThreadLifecycleHistory(tab.ThreadLifecycleHistory);
|
|
PerformancePaneHost.LoadThreadStackHistory(tab.ThreadStackHistories);
|
|
RestoreIntelSessionState(tab.Pid);
|
|
SyncPerformanceViewToTimeline();
|
|
_hasPerformanceData = tab.PerformanceHistory.Count > 0;
|
|
SetExplorerHasData("Performance", tab.PerformanceHistory.Count > 0);
|
|
EventsPaneHost.SetHasData(_allEvents.Count > 0);
|
|
RefreshExplorerDataBadges();
|
|
RefreshChildProcessGraphWindowIfOpen();
|
|
}
|
|
|
|
private void SwitchToSession(ProcessSessionTab tab)
|
|
{
|
|
if (ReferenceEquals(_currentSession, tab))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_currentSession != null && !ReferenceEquals(_currentSession, tab))
|
|
{
|
|
SaveChildProcessGraphStateToSession(_currentSession);
|
|
SyncCurrentSessionViewportStateToMemory();
|
|
FlashSwitchToast($"Switched to {tab.Title}");
|
|
}
|
|
|
|
_currentSession = tab;
|
|
PidBox.Text = tab.Pid.ToString();
|
|
RefreshSubsystemSegmentationDiagnostics();
|
|
RestoreChildProcessGraphStateFromSession(tab);
|
|
|
|
RestoreSessionState(tab);
|
|
|
|
if (tab.OfflineSnapshot)
|
|
{
|
|
StopLiveAnalysisSession(LiveAnalysisStopReason.SessionSwitch, preserveApiGraphSnapshot: true,
|
|
fastTeardown: true, stopPerformance: true);
|
|
StatusBlock.Text = $"OFFLINE SESSION: {tab.Title}";
|
|
RefreshProcessStateBadge();
|
|
return;
|
|
}
|
|
|
|
if (tab.TargetExited)
|
|
{
|
|
StopLiveAnalysisSession(LiveAnalysisStopReason.SessionSwitch, preserveApiGraphSnapshot: true,
|
|
fastTeardown: true, stopPerformance: true);
|
|
StatusBlock.Text = "Capture stopped; historical data remains available.";
|
|
RefreshProcessStateBadge();
|
|
return;
|
|
}
|
|
|
|
bool allowPreparedProtectedAttach =
|
|
_preparedLaunchBackendSession != null && _preparedLaunchBackendPid == tab.Pid && tab.UseUsermodeHooks;
|
|
|
|
if (!TryOpenTargetProcess(tab.Pid, out _, out var failure, out var accessDenied))
|
|
{
|
|
if (!allowPreparedProtectedAttach)
|
|
{
|
|
StopLiveAnalysisSession(LiveAnalysisStopReason.SessionSwitch, preserveApiGraphSnapshot: true,
|
|
fastTeardown: true, stopPerformance: true);
|
|
if (!accessDenied)
|
|
{
|
|
tab.TargetExited = true;
|
|
MarkSessionExited(tab);
|
|
StatusBlock.Text = "Capture stopped; historical data remains available.";
|
|
}
|
|
else
|
|
{
|
|
StatusBlock.Text = $"Access denied to PID {tab.Pid}. Capture stopped.";
|
|
}
|
|
RefreshProcessStateBadge();
|
|
return;
|
|
}
|
|
|
|
OutputCapture.AppendLine(
|
|
$"Protected launch session accepted without direct process-open access: PID {tab.Pid}");
|
|
}
|
|
|
|
bool launchStartsSuspended = tab.LaunchStartsSuspendedPending;
|
|
tab.LaunchStartsSuspendedPending = false;
|
|
StartLiveCaptureForPid(tab.Pid, tab.UseUsermodeHooks, launchStartsSuspended, fastBackendRestart: true);
|
|
}
|
|
|
|
private static bool TelemetryEventsAreTimestampSorted(IReadOnlyList<TelemetryEvent> events)
|
|
{
|
|
for (int i = 1; i < events.Count; i += 1)
|
|
{
|
|
if (events[i].TimestampUtc < events[i - 1].TimestampUtc)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void SyncPerformanceViewToTimeline()
|
|
{
|
|
var viewStart = _captureStartUtc + TimeSpan.FromSeconds(EventsPaneHost.Timeline.ViewStartSeconds);
|
|
var viewEnd = viewStart + TimeSpan.FromSeconds(EventsPaneHost.Timeline.ViewDurationSeconds);
|
|
|
|
PerformancePaneHost.SetViewWindow(viewStart, viewEnd);
|
|
}
|
|
|
|
private void ApplyTimelineViewport(double targetStart, bool syncScroll, bool updateFollowState)
|
|
{
|
|
if (EventsPaneHost?.Scroll == null || EventsPaneHost?.Timeline == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
double viewport = Math.Max(1, EventsPaneHost.Scroll.ViewportSize);
|
|
double maxStart = ComputeTimelineMaxStart(viewport);
|
|
double clamped = Math.Max(0, Math.Min(maxStart, targetStart));
|
|
if (updateFollowState)
|
|
{
|
|
_followLiveTimeline = maxStart <= 0.001 || Math.Abs(clamped - maxStart) < 0.25;
|
|
}
|
|
|
|
_pendingScrollStartSeconds = clamped;
|
|
EventsPaneHost.Timeline.ViewStartSeconds = clamped;
|
|
if (syncScroll && Math.Abs(EventsPaneHost.Scroll.Value - clamped) > 0.001)
|
|
{
|
|
EventsPaneHost.Scroll.Value = clamped;
|
|
}
|
|
|
|
FocusViewport();
|
|
UpdateTopTimeTravelBar();
|
|
SyncPerformanceViewToTimeline();
|
|
}
|
|
|
|
private void AppendEvent(TelemetryEvent ev)
|
|
{
|
|
AppendEvents(new[] { ev });
|
|
}
|
|
|
|
private void AppendEvents(IReadOnlyList<TelemetryEvent> events)
|
|
{
|
|
if (events.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool laneKeysChanged = false;
|
|
for (int i = 0; i < events.Count; i += 1)
|
|
{
|
|
TelemetryEvent ev = NormalizeTelemetryEventForStore(events[i]);
|
|
_allEvents.Add(ev);
|
|
_currentSession?.Events.Add(ev);
|
|
if (ev.TimestampUtc > _latestEventTimestampUtc)
|
|
{
|
|
_latestEventTimestampUtc = ev.TimestampUtc;
|
|
}
|
|
if (!string.IsNullOrWhiteSpace(ev.Group) && _knownLaneKeys.Add(ev.Group))
|
|
{
|
|
laneKeysChanged = true;
|
|
}
|
|
}
|
|
|
|
SetExplorerHasData("Events", _allEvents.Count > 0);
|
|
EventsPaneHost.SetHasData(true);
|
|
if (laneKeysChanged)
|
|
{
|
|
EventsPaneHost.SetLaneFilterOptions(_knownLaneKeys.OrderBy(k => k));
|
|
}
|
|
ScheduleViewportRefresh();
|
|
}
|
|
|
|
internal void SetBackendConnectivity(bool healthy)
|
|
{
|
|
_connectivityHealthy = healthy;
|
|
ApplyUplinkStatusVisual(healthy);
|
|
EventsPaneHost.SetConnectivityHealthy(healthy);
|
|
EventsPaneHost.SetHasData(_allEvents.Count > 0);
|
|
RefreshExplorerDataBadges();
|
|
}
|
|
|
|
private void ApplyUplinkStatusVisual(bool? healthy)
|
|
{
|
|
if (StatusBlock == null)
|
|
return;
|
|
|
|
if (healthy == true)
|
|
{
|
|
StatusBlock.SetResourceReference(TextBlock.ForegroundProperty, "WinMutedTextBrush");
|
|
return;
|
|
}
|
|
|
|
if (healthy == false)
|
|
{
|
|
StatusBlock.SetResourceReference(TextBlock.ForegroundProperty, "StatusFailedBrush");
|
|
return;
|
|
}
|
|
|
|
StatusBlock.SetResourceReference(TextBlock.ForegroundProperty, "WinMutedTextBrush");
|
|
}
|
|
|
|
private void UpdateWindowTitle()
|
|
{
|
|
string sessionLabel;
|
|
if (_currentSession == null || _currentSession.Pid <= 0)
|
|
{
|
|
sessionLabel = "IDLE";
|
|
}
|
|
else if (_currentSession.OfflineSnapshot)
|
|
{
|
|
sessionLabel = $"OFFLINE SESSION: {NormalizeSessionTitle(_currentSession.Title)}";
|
|
}
|
|
else if (_currentSession.TargetExited)
|
|
{
|
|
sessionLabel = $"[CAPTURE {_currentSession.Pid} — STOPPED]";
|
|
}
|
|
else
|
|
{
|
|
string captureTitle = NormalizeSessionTitle(_currentSession.Title ?? $"PID {_currentSession.Pid}");
|
|
sessionLabel = $"[CAPTURE {_currentSession.Pid} {captureTitle}]";
|
|
}
|
|
|
|
string newTitle = $"{WindowTitleBase} {sessionLabel}";
|
|
if (!string.Equals(Title, newTitle, StringComparison.Ordinal))
|
|
{
|
|
Title = newTitle;
|
|
}
|
|
}
|
|
|
|
private void RefreshProcessStateBadge()
|
|
{
|
|
UpdateWindowTitle();
|
|
RefreshToolbarCommandState();
|
|
|
|
if (ProcessStateBadge == null || ProcessStateBlock == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_currentSession == null || _currentSession.Pid <= 0)
|
|
{
|
|
SetProcessStateVisual("Disconnected", ProcessStateUnknownBackground, ProcessStateUnknownBorder,
|
|
ProcessStateUnknownForeground);
|
|
return;
|
|
}
|
|
|
|
string labelPrefix = $"PID {_currentSession.Pid}";
|
|
if (_currentSession.OfflineSnapshot)
|
|
{
|
|
SetProcessStateVisual($"{labelPrefix} • Exited (offline capture)", ProcessStateExitedBackground,
|
|
ProcessStateExitedBorder, ProcessStateExitedForeground);
|
|
return;
|
|
}
|
|
|
|
if (_currentSession.TargetExited)
|
|
{
|
|
string reason = string.IsNullOrWhiteSpace(_currentSession.TargetExitReason)
|
|
? "capture available"
|
|
: _currentSession.TargetExitReason;
|
|
SetProcessStateVisual($"{labelPrefix} • Exited ({reason})", ProcessStateExitedBackground,
|
|
ProcessStateExitedBorder, ProcessStateExitedForeground);
|
|
return;
|
|
}
|
|
|
|
IntelScopeStatus scope = ((IIntelDetailsProvider)this).GetIntelScopeStatus();
|
|
switch (scope)
|
|
{
|
|
case IntelScopeStatus.Running:
|
|
SetProcessStateVisual($"{labelPrefix} • Connected / Running", ProcessStateRunningBackground,
|
|
ProcessStateRunningBorder, ProcessStateRunningForeground);
|
|
break;
|
|
case IntelScopeStatus.Waiting:
|
|
SetProcessStateVisual($"{labelPrefix} • Suspended / Waiting", ProcessStateWaitingBackground,
|
|
ProcessStateWaitingBorder, ProcessStateWaitingForeground);
|
|
break;
|
|
case IntelScopeStatus.Exited:
|
|
SetProcessStateVisual($"{labelPrefix} • Exited (capture available)", ProcessStateExitedBackground,
|
|
ProcessStateExitedBorder, ProcessStateExitedForeground);
|
|
break;
|
|
default:
|
|
SetProcessStateVisual($"{labelPrefix} • Connected / Unknown", ProcessStateUnknownBackground,
|
|
ProcessStateUnknownBorder, ProcessStateUnknownForeground);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void SetProcessStateVisual(string text, Brush background, Brush border, Brush foreground)
|
|
{
|
|
ProcessStateBlock.Text = text;
|
|
ProcessStateBadge.Background = background;
|
|
ProcessStateBadge.BorderBrush = border;
|
|
ProcessStateBlock.Foreground = foreground;
|
|
}
|
|
|
|
private void RefreshToolbarCommandState()
|
|
{
|
|
bool hasAttachedTarget = _currentSession != null && _currentSession.Pid > 0 &&
|
|
!_currentSession.OfflineSnapshot && !_currentSession.TargetExited;
|
|
|
|
bool canPause = hasAttachedTarget;
|
|
bool canResume = hasAttachedTarget && _targetExecutionSuspended;
|
|
if (canResume)
|
|
{
|
|
canPause = false;
|
|
}
|
|
|
|
SetToolbarCommandButtonState(TargetCommandButton, TargetCommandGlyph, true, ToolbarTargetBackground,
|
|
ToolbarTargetBorder, ToolbarTargetForeground);
|
|
SetToolbarCommandButtonState(PauseCommandButton, PauseCommandGlyph, canPause, ToolbarPauseBackground,
|
|
ToolbarPauseBorder, ToolbarPauseForeground);
|
|
SetToolbarCommandButtonState(ResumeCommandButton, ResumeCommandGlyph, canResume, ToolbarResumeBackground,
|
|
ToolbarResumeBorder, ToolbarResumeForeground);
|
|
SetToolbarCommandButtonState(TerminateCommandButton, TerminateCommandGlyph, hasAttachedTarget,
|
|
ToolbarStopBackground, ToolbarStopBorder, ToolbarStopForeground);
|
|
RefreshHooksButtonState();
|
|
|
|
if (TimeTravelLiveNotice != null)
|
|
{
|
|
bool targetStopped = _currentSession != null && _currentSession.TargetExited;
|
|
TimeTravelLiveNotice.Visibility = targetStopped ? Visibility.Collapsed : Visibility.Visible;
|
|
}
|
|
}
|
|
|
|
private static void SetToolbarCommandButtonState(Button? button, Border? glyph, bool enabled,
|
|
Brush activeBackground, Brush activeBorder,
|
|
Brush activeForeground)
|
|
{
|
|
if (button == null || glyph == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
button.IsEnabled = enabled;
|
|
button.Background = enabled ? activeBackground : ToolbarInactiveBackground;
|
|
button.BorderBrush = enabled ? activeBorder : ToolbarInactiveBorder;
|
|
button.Foreground = enabled ? activeForeground : ToolbarInactiveForeground;
|
|
glyph.Background = enabled ? activeForeground : ToolbarInactiveForeground;
|
|
}
|
|
|
|
private void Scroll_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
|
{
|
|
double viewport = Math.Max(1, EventsPaneHost.Scroll.ViewportSize);
|
|
double maxStart = ComputeTimelineMaxStart(viewport);
|
|
double clamped = Math.Max(0, Math.Min(maxStart, EventsPaneHost.Scroll.Value));
|
|
_pendingScrollStartSeconds = clamped;
|
|
EventsPaneHost.Timeline.ViewStartSeconds = clamped;
|
|
_followLiveTimeline = maxStart <= 0.001 || Math.Abs(clamped - maxStart) < 0.25;
|
|
|
|
if (_scrollSyncPending)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_scrollSyncPending = true;
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
_scrollSyncPending = false;
|
|
ApplyTimelineViewport(_pendingScrollStartSeconds, syncScroll: false,
|
|
updateFollowState: false);
|
|
}),
|
|
DispatcherPriority.Render);
|
|
}
|
|
|
|
private void UpdateScrollBar()
|
|
{
|
|
if (_allEvents.Count == 0)
|
|
{
|
|
EventsPaneHost.Scroll.Maximum = 0;
|
|
EventsPaneHost.Scroll.ViewportSize = 1;
|
|
EventsPaneHost.Scroll.SmallChange = 1;
|
|
EventsPaneHost.Scroll.LargeChange = 1;
|
|
EventsPaneHost.Scroll.Value = 0;
|
|
EventsPaneHost.Timeline.ViewStartSeconds = 0;
|
|
_latestEventTimestampUtc = _captureStartUtc;
|
|
UpdateTopTimeTravelBar();
|
|
return;
|
|
}
|
|
|
|
DateTime horizonUtc = GetTimelineHorizonUtc();
|
|
var totalSeconds = (horizonUtc - _captureStartUtc).TotalSeconds;
|
|
if (totalSeconds < 0)
|
|
{
|
|
totalSeconds = 0;
|
|
}
|
|
|
|
double duration = Math.Max(1, EventsPaneHost.Timeline.ViewDurationSeconds);
|
|
double maxStart = Math.Max(0, totalSeconds - duration);
|
|
EventsPaneHost.Scroll.ViewportSize = duration;
|
|
EventsPaneHost.Scroll.Maximum = maxStart;
|
|
EventsPaneHost.Scroll.SmallChange = Math.Max(1, duration / 20.0);
|
|
EventsPaneHost.Scroll.LargeChange = Math.Max(1, duration * 0.8);
|
|
|
|
if (EventsPaneHost.Timeline.ViewStartSeconds > maxStart)
|
|
{
|
|
EventsPaneHost.Timeline.ViewStartSeconds = maxStart;
|
|
}
|
|
|
|
if (EventsPaneHost.Scroll.Value > maxStart)
|
|
{
|
|
EventsPaneHost.Scroll.Value = maxStart;
|
|
}
|
|
|
|
UpdateTopTimeTravelBar();
|
|
}
|
|
|
|
private void FocusViewport()
|
|
{
|
|
var viewStart = _captureStartUtc + TimeSpan.FromSeconds(EventsPaneHost.Timeline.ViewStartSeconds);
|
|
var viewEnd = viewStart + TimeSpan.FromSeconds(EventsPaneHost.Timeline.ViewDurationSeconds);
|
|
var selectedAnchor = CaptureSelectedEventAnchor();
|
|
|
|
double durationSeconds = Math.Max(1, (viewEnd - viewStart).TotalSeconds);
|
|
RangeBlock.Text = $"Window {viewStart:HH:mm:ss}-{viewEnd:HH:mm:ss} {durationSeconds:0}s";
|
|
|
|
if (_allEvents.Count == 0)
|
|
{
|
|
_focusedEvents.ReplaceAll(Array.Empty<TelemetryEvent>());
|
|
EventsPaneHost.Timeline.ReplaceItems(_focusedEvents);
|
|
SetExplorerHasData("Events", false);
|
|
EventsPaneHost.SetHeaderStats("View 0 | Total 0 | 0.0/s");
|
|
ClearSelectedEvent();
|
|
UpdateDetachedEventLogWindow();
|
|
return;
|
|
}
|
|
|
|
int start = LowerBoundEventIndex(viewStart);
|
|
int endExclusive = UpperBoundEventIndex(viewEnd);
|
|
var rawVisibleEvents = new List<TelemetryEvent>(Math.Max(16, endExclusive - start));
|
|
for (int i = start; i < endExclusive; i += 1)
|
|
{
|
|
var ev = _allEvents[i];
|
|
if (!PassLaneFocus(ev))
|
|
{
|
|
continue;
|
|
}
|
|
rawVisibleEvents.Add(ev);
|
|
}
|
|
|
|
var visibleEvents =
|
|
ClusterViewportEvents(rawVisibleEvents, Math.Max(1.0, EventsPaneHost.Timeline.ViewDurationSeconds));
|
|
_focusedEvents.ReplaceAll(visibleEvents);
|
|
EventsPaneHost.Timeline.ReplaceItems(_focusedEvents);
|
|
|
|
RestoreSelectedEventInFocusedView(selectedAnchor);
|
|
FindExplorerItem("Events")?.PushPreviewValue(_focusedEvents.Count);
|
|
SetExplorerHasData("Events", _allEvents.Count > 0);
|
|
double viewSeconds = Math.Max(1.0, EventsPaneHost.Timeline.ViewDurationSeconds);
|
|
double rate = rawVisibleEvents.Count / viewSeconds;
|
|
EventsPaneHost.SetHeaderStats(
|
|
$"View {_focusedEvents.Count} clustered | Raw {rawVisibleEvents.Count} | Total {_allEvents.Count} | {rate:0.0}/s");
|
|
UpdateDetachedEventLogWindow();
|
|
UpdateTopTimeTravelBar();
|
|
}
|
|
|
|
private IReadOnlyList<TelemetryEvent> ClusterViewportEvents(IReadOnlyList<TelemetryEvent> rawVisibleEvents,
|
|
double viewDurationSeconds)
|
|
{
|
|
_focusedClusterMembers.Clear();
|
|
if (rawVisibleEvents.Count == 0)
|
|
{
|
|
return Array.Empty<TelemetryEvent>();
|
|
}
|
|
|
|
if (rawVisibleEvents.Count <= 180)
|
|
{
|
|
for (int i = 0; i < rawVisibleEvents.Count; i += 1)
|
|
{
|
|
TelemetryEvent raw = rawVisibleEvents[i];
|
|
_focusedClusterMembers[new EventSelectionKey(raw)] = new[] { raw };
|
|
}
|
|
|
|
return rawVisibleEvents;
|
|
}
|
|
|
|
int targetVisibleCount =
|
|
Math.Clamp((int)Math.Round((EventsPaneHost?.Timeline?.ActualWidth ?? 1280.0) / 9.0), 80, 160);
|
|
bool includeThreadId = rawVisibleEvents.Count <= 800;
|
|
double bucketSeconds = Math.Max(0.12, viewDurationSeconds / Math.Max(60, targetVisibleCount));
|
|
List<TelemetryEvent> clustered = new();
|
|
|
|
for (int iteration = 0; iteration < 5; iteration += 1)
|
|
{
|
|
clustered = BuildClusteredViewportEvents(rawVisibleEvents, bucketSeconds, includeThreadId);
|
|
if (clustered.Count <= targetVisibleCount)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (includeThreadId && rawVisibleEvents.Count > targetVisibleCount * 2)
|
|
{
|
|
includeThreadId = false;
|
|
continue;
|
|
}
|
|
|
|
bucketSeconds *= 1.7;
|
|
}
|
|
|
|
return clustered;
|
|
}
|
|
|
|
private List<TelemetryEvent> BuildClusteredViewportEvents(IReadOnlyList<TelemetryEvent> rawVisibleEvents,
|
|
double bucketSeconds, bool includeThreadId)
|
|
{
|
|
var buckets = new Dictionary<string, List<TelemetryEvent>>(StringComparer.Ordinal);
|
|
for (int i = 0; i < rawVisibleEvents.Count; i += 1)
|
|
{
|
|
TelemetryEvent ev = rawVisibleEvents[i];
|
|
long bucketIndex = (long)Math.Floor(Math.Max(0, (ev.TimestampUtc - _captureStartUtc).TotalSeconds) /
|
|
Math.Max(0.01, bucketSeconds));
|
|
string normalizedSummary = NormalizeClusterSummary(ev.Summary);
|
|
int clusterTid = includeThreadId ? ev.TID : 0;
|
|
string key =
|
|
$"{bucketIndex}|{ev.PID}|{clusterTid}|{ev.Group}|{ev.SubType}|{normalizedSummary}|{ev.ProcessName}";
|
|
if (!buckets.TryGetValue(key, out List<TelemetryEvent>? members))
|
|
{
|
|
members = new List<TelemetryEvent>(4);
|
|
buckets[key] = members;
|
|
}
|
|
|
|
members.Add(ev);
|
|
}
|
|
|
|
var displayEvents = new List<TelemetryEvent>(buckets.Count);
|
|
foreach (List<TelemetryEvent> members in buckets.Values.OrderBy(x => x[^1].TimestampUtc))
|
|
{
|
|
if (members.Count == 1)
|
|
{
|
|
TelemetryEvent raw = members[0];
|
|
_focusedClusterMembers[new EventSelectionKey(raw)] = members;
|
|
displayEvents.Add(raw);
|
|
continue;
|
|
}
|
|
|
|
TelemetryEvent first = members[0];
|
|
TelemetryEvent last = members[^1];
|
|
bool mixedThreads = members.Any(x => x.TID != first.TID);
|
|
bool mixedProcesses = members.Any(x => x.PID != first.PID);
|
|
string baseSummary = string.IsNullOrWhiteSpace(last.Summary) ? last.SubType : last.Summary;
|
|
string summary = $"[{members.Count}x] {baseSummary}";
|
|
string details =
|
|
$"cluster-count={members.Count}; first={first.TimestampUtc:O}; last={last.TimestampUtc:O}; " +
|
|
$"group={first.Group}; subtype={first.SubType}; mixedThreads={(mixedThreads ? "yes" : "no")}; mixedProcesses={(mixedProcesses ? "yes" : "no")}";
|
|
|
|
var clustered = new TelemetryEvent { TimestampUtc = last.TimestampUtc,
|
|
PID = mixedProcesses ? 0 : first.PID,
|
|
TID = mixedThreads ? 0 : first.TID,
|
|
Group = first.Group,
|
|
SubType = first.SubType,
|
|
ProcessName = first.ProcessName,
|
|
Summary = summary,
|
|
Details = details };
|
|
|
|
_focusedClusterMembers[new EventSelectionKey(clustered)] = members;
|
|
displayEvents.Add(clustered);
|
|
}
|
|
|
|
return displayEvents;
|
|
}
|
|
|
|
private static string NormalizeClusterSummary(string? summary)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(summary))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
string normalized = summary.Trim();
|
|
if (normalized.Length > 160)
|
|
{
|
|
normalized = normalized[..160];
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
private IReadOnlyList<TelemetryEvent> GetFocusedEventMembers(TelemetryEvent? displayEvent)
|
|
{
|
|
if (displayEvent == null)
|
|
{
|
|
return Array.Empty<TelemetryEvent>();
|
|
}
|
|
|
|
if (_focusedClusterMembers.TryGetValue(new EventSelectionKey(displayEvent), out IReadOnlyList<TelemetryEvent>? members))
|
|
{
|
|
return members;
|
|
}
|
|
|
|
return new[] { displayEvent };
|
|
}
|
|
|
|
private IReadOnlyList<TelemetryEvent> ExpandFocusedEventMembers(IEnumerable<TelemetryEvent> displayEvents)
|
|
{
|
|
var expanded = new List<TelemetryEvent>();
|
|
foreach (TelemetryEvent ev in displayEvents)
|
|
{
|
|
expanded.AddRange(GetFocusedEventMembers(ev));
|
|
}
|
|
|
|
return expanded;
|
|
}
|
|
|
|
private void UpdateTopTimeTravelBar()
|
|
{
|
|
if (TopTimeTravelSlider == null || TopTimeTravelRangeBlock == null || EventsPaneHost?.Scroll == null ||
|
|
EventsPaneHost?.Timeline == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
double viewport = Math.Max(1, EventsPaneHost.Scroll.ViewportSize);
|
|
double maxStart = ComputeTimelineMaxStart(viewport);
|
|
double start = Math.Max(0, Math.Min(maxStart, EventsPaneHost.Timeline.ViewStartSeconds));
|
|
|
|
_topTimeTravelSyncing = true;
|
|
TopTimeTravelSlider.Minimum = 0;
|
|
TopTimeTravelSlider.Maximum = Math.Max(0, maxStart);
|
|
TopTimeTravelSlider.Value = start;
|
|
TopTimeTravelSlider.SmallChange = Math.Max(1, viewport / 30.0);
|
|
TopTimeTravelSlider.LargeChange = Math.Max(1, viewport * 0.5);
|
|
TopTimeTravelSlider.IsEnabled = maxStart > 0.001;
|
|
_topTimeTravelSyncing = false;
|
|
|
|
DateTime viewStart = _captureStartUtc + TimeSpan.FromSeconds(start);
|
|
DateTime viewEnd = viewStart + TimeSpan.FromSeconds(viewport);
|
|
TopTimeTravelRangeBlock.Text = $"{viewStart:HH:mm:ss}-{viewEnd:HH:mm:ss}";
|
|
}
|
|
|
|
private void TopTimeTravelSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
|
{
|
|
if (_topTimeTravelSyncing || EventsPaneHost?.Scroll == null || EventsPaneHost?.Timeline == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ApplyTimelineViewport(e.NewValue, syncScroll: true, updateFollowState: true);
|
|
}
|
|
|
|
private void NudgeTopTimeTravel(double secondsDelta)
|
|
{
|
|
if (EventsPaneHost?.Scroll == null || EventsPaneHost?.Timeline == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ApplyTimelineViewport(EventsPaneHost.Scroll.Value + secondsDelta, syncScroll: true,
|
|
updateFollowState: true);
|
|
}
|
|
|
|
private void TopTimeTravelBack10_Click(object sender, RoutedEventArgs e) => NudgeTopTimeTravel(-10);
|
|
private void TopTimeTravelBack1_Click(object sender, RoutedEventArgs e) => NudgeTopTimeTravel(-1);
|
|
private void TopTimeTravelForward1_Click(object sender, RoutedEventArgs e) => NudgeTopTimeTravel(1);
|
|
private void TopTimeTravelForward10_Click(object sender, RoutedEventArgs e) => NudgeTopTimeTravel(10);
|
|
|
|
private void ScheduleViewportRefresh()
|
|
{
|
|
if (_viewportRefreshPending)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_viewportRefreshPending = true;
|
|
Dispatcher.BeginInvoke(
|
|
new Action(
|
|
() =>
|
|
{
|
|
_viewportRefreshPending = false;
|
|
UpdateScrollBar();
|
|
if (_followLiveTimeline && EventsPaneHost?.Scroll != null)
|
|
{
|
|
ApplyTimelineViewport(
|
|
ComputeTimelineMaxStart(Math.Max(1, EventsPaneHost.Scroll.ViewportSize)),
|
|
syncScroll: true, updateFollowState: false);
|
|
}
|
|
else
|
|
{
|
|
FocusViewport();
|
|
UpdateTopTimeTravelBar();
|
|
SyncPerformanceViewToTimeline();
|
|
}
|
|
double viewStartSeconds = EventsPaneHost?.Timeline?.ViewStartSeconds ?? 0;
|
|
DiagnosticsState.SetValue(
|
|
"UI Viewport",
|
|
$"view={_focusedEvents.Count} total={_allEvents.Count} start={viewStartSeconds:0.0}s");
|
|
}),
|
|
System.Windows.Threading.DispatcherPriority.Background);
|
|
}
|
|
|
|
private double ComputeTimelineMaxStart(double viewportSeconds)
|
|
{
|
|
double viewport = Math.Max(1, viewportSeconds);
|
|
double totalSeconds = Math.Max(0, (GetTimelineHorizonUtc() - _captureStartUtc).TotalSeconds);
|
|
return Math.Max(0, totalSeconds - viewport);
|
|
}
|
|
|
|
private DateTime GetTimelineHorizonUtc()
|
|
{
|
|
DateTime eventHorizon =
|
|
_latestEventTimestampUtc > _captureStartUtc ? _latestEventTimestampUtc : _captureStartUtc;
|
|
return eventHorizon;
|
|
}
|
|
|
|
private static DateTime AnchorCaptureStartUtc(double viewDurationSeconds)
|
|
{
|
|
double seconds = Math.Max(1, viewDurationSeconds);
|
|
return DateTime.UtcNow - TimeSpan.FromSeconds(seconds);
|
|
}
|
|
|
|
private TelemetryEvent NormalizeTelemetryEventForStore(TelemetryEvent source)
|
|
{
|
|
string group = _telemetryTextPool.Intern(source.Group, 48);
|
|
string subType = _telemetryTextPool.Intern(source.SubType, 96);
|
|
string processName = _telemetryTextPool.Intern(source.ProcessName, 96);
|
|
string summary = _telemetryTextPool.Intern(source.Summary, 160);
|
|
string details = _telemetryTextPool.Intern(source.Details, 384);
|
|
|
|
if (ReferenceEquals(group, source.Group) && ReferenceEquals(subType, source.SubType) &&
|
|
ReferenceEquals(processName, source.ProcessName) && ReferenceEquals(summary, source.Summary) &&
|
|
ReferenceEquals(details, source.Details))
|
|
{
|
|
return source;
|
|
}
|
|
|
|
return new TelemetryEvent { TimestampUtc = source.TimestampUtc,
|
|
PID = source.PID,
|
|
TID = source.TID,
|
|
Group = group,
|
|
SubType = subType,
|
|
ProcessName = processName,
|
|
Summary = summary,
|
|
Details = details };
|
|
}
|
|
|
|
private int LowerBoundEventIndex(DateTime timestampUtc)
|
|
{
|
|
int lo = 0;
|
|
int hi = _allEvents.Count;
|
|
while (lo < hi)
|
|
{
|
|
int mid = lo + ((hi - lo) / 2);
|
|
if (_allEvents[mid].TimestampUtc < timestampUtc)
|
|
{
|
|
lo = mid + 1;
|
|
}
|
|
else
|
|
{
|
|
hi = mid;
|
|
}
|
|
}
|
|
|
|
return lo;
|
|
}
|
|
|
|
private int UpperBoundEventIndex(DateTime timestampUtc)
|
|
{
|
|
int lo = 0;
|
|
int hi = _allEvents.Count;
|
|
while (lo < hi)
|
|
{
|
|
int mid = lo + ((hi - lo) / 2);
|
|
if (_allEvents[mid].TimestampUtc <= timestampUtc)
|
|
{
|
|
lo = mid + 1;
|
|
}
|
|
else
|
|
{
|
|
hi = mid;
|
|
}
|
|
}
|
|
|
|
return lo;
|
|
}
|
|
|
|
private bool PassLaneFocus(TelemetryEvent ev)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_laneFocusKey))
|
|
return true;
|
|
|
|
string key = string.IsNullOrWhiteSpace(ev.SubType) ? ev.Group : $"{ev.Group}/{ev.SubType}";
|
|
return string.Equals(key, _laneFocusKey, StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(ev.Group, _laneFocusKey, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private void Timeline_LaneInteraction(object? sender, LaneInteractionEventArgs e)
|
|
{
|
|
if (e.Button == System.Windows.Input.MouseButton.Left && !e.IsArrow)
|
|
{
|
|
_laneFocusKey = e.LaneKey;
|
|
FocusViewport();
|
|
return;
|
|
}
|
|
|
|
if (e.Button != System.Windows.Input.MouseButton.Right)
|
|
return;
|
|
|
|
var menu = new ContextMenu();
|
|
|
|
var miEnableDisable =
|
|
new MenuItem { Header = EventsPaneHost.Timeline.IsLaneVisible(e.LaneKey) ? "Disable" : "Enable" };
|
|
miEnableDisable.Click += (_, __) =>
|
|
{
|
|
bool nowVisible = !EventsPaneHost.Timeline.IsLaneVisible(e.LaneKey);
|
|
EventsPaneHost.Timeline.SetLaneVisible(e.LaneKey, nowVisible);
|
|
FocusViewport();
|
|
};
|
|
menu.Items.Add(miEnableDisable);
|
|
|
|
var miFilter = new MenuItem { Header = "Filter to selection" };
|
|
miFilter.Click += (_, __) =>
|
|
{
|
|
_laneFocusKey = e.LaneKey;
|
|
FocusViewport();
|
|
};
|
|
menu.Items.Add(miFilter);
|
|
|
|
var miClear = new MenuItem { Header = "Clear filter" };
|
|
miClear.Click += (_, __) =>
|
|
{
|
|
_laneFocusKey = null;
|
|
EventsPaneHost.Timeline.ClearAllLaneFilters();
|
|
FocusViewport();
|
|
};
|
|
menu.Items.Add(miClear);
|
|
|
|
menu.Items.Add(new Separator());
|
|
|
|
var miColor = new MenuItem { Header = "Set colour (cycle)" };
|
|
miColor.Click += (_, __) =>
|
|
{
|
|
Color[] options = {
|
|
Color.FromRgb(0x4C, 0x8F, 0xD2), Color.FromRgb(0x6C, 0xA4, 0xDE), Color.FromRgb(0x58, 0xB6, 0x58),
|
|
Color.FromRgb(0x7B, 0xC7, 0x7B), Color.FromRgb(0x8D, 0x97, 0xA3),
|
|
};
|
|
|
|
int idx = (Math.Abs(e.LaneKey.GetHashCode()) % options.Length);
|
|
var next = options[(idx + DateTime.UtcNow.Second) % options.Length];
|
|
EventsPaneHost.Timeline.SetLaneColor(e.LaneKey, next);
|
|
};
|
|
menu.Items.Add(miColor);
|
|
|
|
menu.IsOpen = true;
|
|
}
|
|
|
|
private void Timeline_EventDoubleClicked(object? sender, TelemetryEventSelectedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
}
|
|
|
|
private EventSelectionKey? CaptureSelectedEventAnchor()
|
|
{
|
|
if (_eventSelectionSyncing)
|
|
{
|
|
return _selectedEventAnchor;
|
|
}
|
|
|
|
if (EventsPaneHost.Grid.SelectedItem is TelemetryEvent selectedInGrid)
|
|
{
|
|
return new EventSelectionKey(selectedInGrid);
|
|
}
|
|
|
|
if (EventsPaneHost.Timeline.SelectedEvent is TelemetryEvent selectedInTimeline)
|
|
{
|
|
return new EventSelectionKey(selectedInTimeline);
|
|
}
|
|
|
|
return _selectedEventAnchor;
|
|
}
|
|
|
|
private void RestoreSelectedEventInFocusedView(EventSelectionKey? preferred)
|
|
{
|
|
if (preferred is not EventSelectionKey key || _focusedEvents.Count == 0)
|
|
{
|
|
if (preferred == null)
|
|
{
|
|
ClearSelectedEvent();
|
|
}
|
|
return;
|
|
}
|
|
|
|
TelemetryEvent? matched = _focusedEvents.FirstOrDefault(ev => key.Matches(ev));
|
|
if (matched != null)
|
|
{
|
|
ApplySelectedEvent(matched, scrollIntoView: false);
|
|
return;
|
|
}
|
|
|
|
TelemetryEvent? clusteredMatch =
|
|
_focusedEvents.FirstOrDefault(ev => GetFocusedEventMembers(ev).Any(raw => key.Matches(raw)));
|
|
if (clusteredMatch != null)
|
|
{
|
|
ApplySelectedEvent(clusteredMatch, scrollIntoView: false);
|
|
return;
|
|
}
|
|
|
|
if (_selectedEventAnchor.HasValue && !_selectedEventAnchor.Value.Equals(key))
|
|
{
|
|
TelemetryEvent? fallback = _focusedEvents.FirstOrDefault(ev => _selectedEventAnchor.Value.Matches(ev));
|
|
if (fallback != null)
|
|
{
|
|
ApplySelectedEvent(fallback, scrollIntoView: false);
|
|
return;
|
|
}
|
|
|
|
TelemetryEvent? clusteredFallback = _focusedEvents.FirstOrDefault(
|
|
ev => GetFocusedEventMembers(ev).Any(raw => _selectedEventAnchor.Value.Matches(raw)));
|
|
if (clusteredFallback != null)
|
|
{
|
|
ApplySelectedEvent(clusteredFallback, scrollIntoView: false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplySelectedEvent(TelemetryEvent? selected, bool scrollIntoView)
|
|
{
|
|
if (EventsPaneHost?.Grid == null || EventsPaneHost?.Timeline == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_eventSelectionSyncing = true;
|
|
try
|
|
{
|
|
EventsPaneHost.Timeline.SelectedEvent = selected;
|
|
EventsPaneHost.Grid.SelectedItem = selected;
|
|
if (selected != null)
|
|
{
|
|
UpdateSelectedEventAnchor(selected);
|
|
if (scrollIntoView)
|
|
{
|
|
EventsPaneHost.Grid.ScrollIntoView(selected);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_eventSelectionSyncing = false;
|
|
}
|
|
}
|
|
|
|
private void UpdateSelectedEventAnchor(TelemetryEvent selected)
|
|
{
|
|
_selectedEventAnchor = new EventSelectionKey(selected);
|
|
}
|
|
|
|
private void ClearSelectedEvent()
|
|
{
|
|
if (EventsPaneHost?.Grid == null || EventsPaneHost?.Timeline == null)
|
|
{
|
|
_selectedEventAnchor = null;
|
|
return;
|
|
}
|
|
|
|
_eventSelectionSyncing = true;
|
|
try
|
|
{
|
|
EventsPaneHost.Grid.SelectedItem = null;
|
|
EventsPaneHost.Timeline.SelectedEvent = null;
|
|
_selectedEventAnchor = null;
|
|
}
|
|
finally
|
|
{
|
|
_eventSelectionSyncing = false;
|
|
}
|
|
}
|
|
|
|
private void Timeline_SelectedEventChanged(object? sender, TelemetryEventSelectedEventArgs e)
|
|
{
|
|
if (_eventSelectionSyncing)
|
|
return;
|
|
|
|
if (e.Selected == null)
|
|
return;
|
|
|
|
ApplySelectedEvent(e.Selected, scrollIntoView: true);
|
|
}
|
|
|
|
private void Grid_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_eventSelectionSyncing)
|
|
return;
|
|
|
|
if (EventsPaneHost.Grid.SelectedItem is TelemetryEvent te)
|
|
{
|
|
ApplySelectedEvent(te, scrollIntoView: false);
|
|
}
|
|
}
|
|
|
|
private void HideEventsPane()
|
|
{
|
|
_eventsPaneVisible = false;
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void ShowEventsPane()
|
|
{
|
|
_eventsPaneVisible = true;
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void HidePerformancePane()
|
|
{
|
|
_performancePaneVisible = false;
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void HideEtwPane()
|
|
{
|
|
var sw = FindExplorerItem("ETW");
|
|
if (sw != null)
|
|
sw.IsEnabled = false;
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void HideHeuristicsPane()
|
|
{
|
|
var heur = FindExplorerItem("Heuristics");
|
|
if (heur != null)
|
|
heur.IsEnabled = false;
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void HideFilesystemPane()
|
|
{
|
|
var fs = FindExplorerItem("Filesystem");
|
|
if (fs != null)
|
|
fs.IsEnabled = false;
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void HideRegistryPane()
|
|
{
|
|
var reg = FindExplorerItem("Registry");
|
|
if (reg != null)
|
|
reg.IsEnabled = false;
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void HideProcessRelationsPane()
|
|
{
|
|
var rel = FindExplorerItem("Process Relations");
|
|
if (rel != null)
|
|
rel.IsEnabled = false;
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void SetExplorerPaneEnabled(string name, bool enabled)
|
|
{
|
|
var item = FindExplorerItem(name);
|
|
if (item != null)
|
|
{
|
|
item.IsEnabled = enabled;
|
|
}
|
|
}
|
|
|
|
private void ShowEtwPane()
|
|
{
|
|
SetExplorerPaneEnabled("ETW", true);
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void ShowHeuristicsPane()
|
|
{
|
|
SetExplorerPaneEnabled("Heuristics", true);
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void ShowFilesystemPane()
|
|
{
|
|
SetExplorerPaneEnabled("Filesystem", true);
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void ShowRegistryPane()
|
|
{
|
|
SetExplorerPaneEnabled("Registry", true);
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void ShowProcessRelationsPane()
|
|
{
|
|
SetExplorerPaneEnabled("Process Relations", true);
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void ShowPerformancePane()
|
|
{
|
|
_performancePaneVisible = true;
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void CollapsedEventsBar_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
ShowEventsPane();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void CollapsedPerformanceBar_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
ShowPerformancePane();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void CollapseOrExpandPane()
|
|
{
|
|
if (_eventsPaneVisible)
|
|
HideEventsPane();
|
|
else
|
|
ShowEventsPane();
|
|
}
|
|
|
|
private void CollapseOrExpandPerformancePane()
|
|
{
|
|
if (_performancePaneVisible)
|
|
HidePerformancePane();
|
|
else
|
|
ShowPerformancePane();
|
|
}
|
|
|
|
private void OpenOrActivateEventLogWindow()
|
|
{
|
|
if (_eventLogWindow == null)
|
|
{
|
|
UndockEventLog();
|
|
return;
|
|
}
|
|
|
|
if (_eventLogWindow.WindowState == WindowState.Minimized)
|
|
{
|
|
_eventLogWindow.WindowState = WindowState.Normal;
|
|
}
|
|
_eventLogWindow.Activate();
|
|
}
|
|
|
|
private void SetMainInterfaceViewMode(MainInterfaceViewMode mode)
|
|
{
|
|
_mainViewMode = mode;
|
|
if (DockGrid != null)
|
|
{
|
|
DockGrid.Visibility =
|
|
mode == MainInterfaceViewMode.Telemetry ? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
if (ApiViewBorder != null)
|
|
{
|
|
ApiViewBorder.Visibility =
|
|
mode == MainInterfaceViewMode.Api ? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
if (ExtendedViewBorder != null)
|
|
{
|
|
ExtendedViewBorder.Visibility =
|
|
mode == MainInterfaceViewMode.Extended ? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
ApplyDockVisibilityFromExplorer();
|
|
UpdateMainViewButtons(mode);
|
|
|
|
if (IsLoaded)
|
|
{
|
|
RebuildToolbarViewMenuOptions();
|
|
}
|
|
}
|
|
|
|
private void UpdateMainViewButtons(MainInterfaceViewMode mode)
|
|
{
|
|
SetMainViewButtonSelected(MainViewButton, mode == MainInterfaceViewMode.Telemetry);
|
|
SetMainViewButtonSelected(ApiViewButton, mode == MainInterfaceViewMode.Api);
|
|
SetMainViewButtonSelected(ExtendedViewButton, mode == MainInterfaceViewMode.Extended);
|
|
}
|
|
|
|
private static void SetMainViewButtonSelected(Button? button, bool selected)
|
|
{
|
|
if (button == null)
|
|
{
|
|
return;
|
|
}
|
|
button.IsEnabled = !selected;
|
|
button.Opacity = selected ? 0.72 : 1.0;
|
|
}
|
|
|
|
private void MainView_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
SetMainInterfaceViewMode(MainInterfaceViewMode.Telemetry);
|
|
}
|
|
|
|
private void ApiView_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
SetMainInterfaceViewMode(MainInterfaceViewMode.Api);
|
|
}
|
|
|
|
private void ExtendedView_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
SetMainInterfaceViewMode(MainInterfaceViewMode.Extended);
|
|
}
|
|
|
|
private void OpenDiagnosticsWindow()
|
|
{
|
|
var w = new DiagnosticsWindow(TryGetPid()) { Owner = this };
|
|
w.Show();
|
|
}
|
|
|
|
private void SignatureIntelRules_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
OpenSignatureIntelRulesWindow();
|
|
}
|
|
|
|
private void ProcessRelationsPaneHost_GraphStateChanged(object? sender, EventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
ScheduleChildProcessGraphWindowRefresh();
|
|
}
|
|
|
|
private void OpenOrActivateChildProcessGraphWindow()
|
|
{
|
|
if (_childProcessGraphWindow == null)
|
|
{
|
|
_childProcessGraphWindow =
|
|
new ChildProcessGraphWindow(ProcessRelationsPaneHost.CurrentRootPid) { Owner = this };
|
|
_childProcessGraphWindow.Closed += ChildProcessGraphWindow_Closed;
|
|
RestoreChildProcessGraphStateFromSession();
|
|
RefreshChildProcessGraphWindow();
|
|
_childProcessGraphWindow.Show();
|
|
RebuildToolbarViewMenuOptions();
|
|
return;
|
|
}
|
|
|
|
RefreshChildProcessGraphWindow();
|
|
if (_childProcessGraphWindow.WindowState == WindowState.Minimized)
|
|
{
|
|
_childProcessGraphWindow.WindowState = WindowState.Normal;
|
|
}
|
|
|
|
_childProcessGraphWindow.Activate();
|
|
}
|
|
|
|
private void RefreshChildProcessGraphWindowIfOpen()
|
|
{
|
|
if (_childProcessGraphWindow == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RefreshChildProcessGraphWindow();
|
|
}
|
|
|
|
private void ScheduleChildProcessGraphWindowRefresh()
|
|
{
|
|
if (_childProcessGraphWindow == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_childProcessGraphRefreshTimer.Stop();
|
|
_childProcessGraphRefreshTimer.Start();
|
|
}
|
|
|
|
private void ChildProcessGraphRefreshTimer_Tick(object? sender, EventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
_childProcessGraphRefreshTimer.Stop();
|
|
RefreshChildProcessGraphWindowIfOpen();
|
|
}
|
|
|
|
private void RefreshChildProcessGraphWindow()
|
|
{
|
|
_childProcessGraphWindow?.UpdateGraph(ProcessRelationsPaneHost.SnapshotItems(),
|
|
ProcessRelationsPaneHost.CurrentRootPid);
|
|
}
|
|
|
|
private void ChildProcessGraphWindow_Closed(object? sender, EventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
SaveChildProcessGraphStateToSession();
|
|
if (_childProcessGraphWindow != null)
|
|
{
|
|
_childProcessGraphWindow.Closed -= ChildProcessGraphWindow_Closed;
|
|
}
|
|
|
|
_childProcessGraphWindow = null;
|
|
RebuildToolbarViewMenuOptions();
|
|
}
|
|
|
|
private void SaveChildProcessGraphStateToSession(ProcessSessionTab? tab = null)
|
|
{
|
|
ProcessSessionTab? session = tab ?? _currentSession;
|
|
if (session == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
session.ChildProcessExpandedKeys.Clear();
|
|
if (_childProcessGraphWindow == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (string key in _childProcessGraphWindow.GetExpandedKeysSnapshot())
|
|
{
|
|
session.ChildProcessExpandedKeys.Add(key);
|
|
}
|
|
}
|
|
|
|
private void RestoreChildProcessGraphStateFromSession(ProcessSessionTab? tab = null)
|
|
{
|
|
if (_childProcessGraphWindow == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_childProcessGraphWindow.SetExpandedKeys((tab ?? _currentSession)?.ChildProcessExpandedKeys);
|
|
}
|
|
|
|
private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
_ = e;
|
|
if (!IsLoaded || sender is not ComboBox combo)
|
|
return;
|
|
|
|
string? tag = (combo.SelectedItem as System.Windows.Controls.ContentControl)?.Tag as string;
|
|
if (string.IsNullOrEmpty(tag))
|
|
return;
|
|
|
|
combo.SelectedIndex = 0;
|
|
|
|
if (tag == "layout-simple")
|
|
{
|
|
ApplySimpleLayout();
|
|
}
|
|
else if (tag == "layout-advanced")
|
|
{
|
|
ApplyAdvancedLayout();
|
|
}
|
|
}
|
|
|
|
private void ApplySimpleLayout()
|
|
{
|
|
if (EtwPaneHost != null)
|
|
EtwPaneHost.Visibility = Visibility.Collapsed;
|
|
if (HeuristicsPaneHost != null)
|
|
HeuristicsPaneHost.Visibility = Visibility.Collapsed;
|
|
if (FilesystemPaneHost != null)
|
|
FilesystemPaneHost.Visibility = Visibility.Collapsed;
|
|
if (ProcessRelationsPaneHost != null)
|
|
ProcessRelationsPaneHost.Visibility = Visibility.Collapsed;
|
|
if (IntelligenceColumn != null)
|
|
IntelligenceColumn.Width = new GridLength(0);
|
|
if (IntelligenceSplitterColumn != null)
|
|
IntelligenceSplitterColumn.Width = new GridLength(0);
|
|
}
|
|
|
|
private void ApplyAdvancedLayout()
|
|
{
|
|
if (EtwPaneHost != null)
|
|
EtwPaneHost.Visibility = Visibility.Visible;
|
|
if (HeuristicsPaneHost != null)
|
|
HeuristicsPaneHost.Visibility = Visibility.Visible;
|
|
if (FilesystemPaneHost != null)
|
|
FilesystemPaneHost.Visibility = Visibility.Visible;
|
|
if (ProcessRelationsPaneHost != null)
|
|
ProcessRelationsPaneHost.Visibility = Visibility.Visible;
|
|
if (IntelligenceColumn != null)
|
|
IntelligenceColumn.Width = new GridLength(560);
|
|
if (IntelligenceSplitterColumn != null)
|
|
IntelligenceSplitterColumn.Width = new GridLength(2);
|
|
}
|
|
|
|
private void ToolbarViewMenu_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
_ = e;
|
|
if (_toolbarViewMenuSyncing || !IsLoaded || sender is not ComboBox combo)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string tag = combo.SelectedItem switch { ComboBoxItem item when item.Tag is string itemTag => itemTag,
|
|
_ => string.Empty };
|
|
if (string.IsNullOrWhiteSpace(tag))
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (tag)
|
|
{
|
|
case "events":
|
|
SetExplorerPaneEnabled("Events", true);
|
|
ShowEventsPane();
|
|
break;
|
|
case "performance":
|
|
SetExplorerPaneEnabled("Performance", true);
|
|
ShowPerformancePane();
|
|
break;
|
|
case "etw":
|
|
ShowEtwPane();
|
|
break;
|
|
case "heuristics":
|
|
ShowHeuristicsPane();
|
|
break;
|
|
case "filesystem":
|
|
ShowFilesystemPane();
|
|
break;
|
|
case "registry":
|
|
ShowRegistryPane();
|
|
break;
|
|
case "process-relations":
|
|
ShowProcessRelationsPane();
|
|
break;
|
|
case "event-log":
|
|
SetExplorerPaneEnabled("Events", true);
|
|
ShowEventsPane();
|
|
OpenOrActivateEventLogWindow();
|
|
break;
|
|
case "inspector-etw":
|
|
ShowEtwPane();
|
|
OpenEtwInspector();
|
|
break;
|
|
case "inspector-heuristics":
|
|
ShowHeuristicsPane();
|
|
OpenHeuristicsInspector();
|
|
break;
|
|
case "inspector-filesystem":
|
|
ShowFilesystemPane();
|
|
OpenFilesystemInspector();
|
|
break;
|
|
case "inspector-registry":
|
|
ShowRegistryPane();
|
|
OpenRegistryInspector();
|
|
break;
|
|
case "inspector-relations":
|
|
ShowProcessRelationsPane();
|
|
OpenProcessRelationsInspector();
|
|
break;
|
|
case "diagnostics":
|
|
OpenDiagnosticsWindow();
|
|
break;
|
|
case "signature-intel-rules":
|
|
OpenSignatureIntelRulesWindow();
|
|
break;
|
|
case "child-process-graph":
|
|
OpenOrActivateChildProcessGraphWindow();
|
|
break;
|
|
}
|
|
|
|
combo.SelectedIndex = 0;
|
|
RebuildToolbarViewMenuOptions();
|
|
}
|
|
|
|
private void ToolbarViewMenu_DropDownOpened(object sender, EventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
RebuildToolbarViewMenuOptions();
|
|
}
|
|
|
|
private void RebuildToolbarViewMenuOptions()
|
|
{
|
|
if (ToolbarViewMenu == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_toolbarViewMenuSyncing = true;
|
|
ToolbarViewMenu.Items.Clear();
|
|
ToolbarViewMenu.Items.Add(new ComboBoxItem { Content = "Open Window..." });
|
|
|
|
AddToolbarViewOptionIfClosed("Events Pane", "events", IsEventsPaneOpen());
|
|
AddToolbarViewOptionIfClosed("Performance Pane", "performance", IsPerformancePaneOpen());
|
|
AddToolbarViewOptionIfClosed("ETW Pane", "etw", IsEtwPaneOpen());
|
|
AddToolbarViewOptionIfClosed("Heuristics Pane", "heuristics", IsHeuristicsPaneOpen());
|
|
AddToolbarViewOptionIfClosed("Filesystem Pane", "filesystem", IsFilesystemPaneOpen());
|
|
AddToolbarViewOptionIfClosed("Registry Pane", "registry", IsRegistryPaneOpen());
|
|
AddToolbarViewOptionIfClosed("Process Relations Pane", "process-relations", IsRelationsPaneOpen());
|
|
AddToolbarViewOptionIfClosed("Event Log Window", "event-log",
|
|
_eventLogWindow != null && _eventLogWindow.IsVisible);
|
|
AddToolbarViewOptionIfClosed("ETW Inspector", "inspector-etw", IsWindowOpenByTitle("ETW Inspector"));
|
|
AddToolbarViewOptionIfClosed("Heuristics Inspector", "inspector-heuristics",
|
|
IsWindowOpenByTitle("Detection Chain"));
|
|
AddToolbarViewOptionIfClosed("Filesystem Inspector", "inspector-filesystem",
|
|
IsWindowOpenByTitle("Filesystem"));
|
|
AddToolbarViewOptionIfClosed("Registry Inspector", "inspector-registry", IsWindowOpenByTitle("Registry"));
|
|
AddToolbarViewOptionIfClosed("Process Relations Inspector", "inspector-relations",
|
|
IsWindowOpenByTitle("Process Relations"));
|
|
AddToolbarViewOptionIfClosed(
|
|
"Diagnostics Window", "diagnostics",
|
|
Application.Current.Windows.OfType<Window>().Any(w => w is DiagnosticsWindow && w.IsVisible));
|
|
AddToolbarViewOptionIfClosed(
|
|
"Rules Intel", "signature-intel-rules",
|
|
Application.Current.Windows.OfType<Window>().Any(w => w is SignatureIntelRulesWindow && w.IsVisible));
|
|
AddToolbarViewOptionIfClosed("Child Process Graph", "child-process-graph",
|
|
_childProcessGraphWindow != null && _childProcessGraphWindow.IsVisible);
|
|
|
|
ToolbarViewMenu.SelectedIndex = 0;
|
|
_toolbarViewMenuSyncing = false;
|
|
}
|
|
|
|
private void AddToolbarViewOptionIfClosed(string label, string tag, bool isOpen)
|
|
{
|
|
if (ToolbarViewMenu == null || isOpen)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ToolbarViewMenu.Items.Add(new ComboBoxItem { Content = label, Tag = tag });
|
|
}
|
|
|
|
private bool IsEventsPaneOpen() => (FindExplorerItem("Events")?.IsEnabled ?? true) &&
|
|
(_eventsPaneVisible || _eventsPaneFloating || _eventsFloatWindow != null);
|
|
|
|
private bool IsPerformancePaneOpen() => (FindExplorerItem("Performance")?.IsEnabled ?? true) &&
|
|
(_performancePaneVisible || _performancePaneFloating ||
|
|
_performanceFloatWindow != null);
|
|
|
|
private bool IsEtwPaneOpen() => (FindExplorerItem("ETW")?.IsEnabled ?? true);
|
|
|
|
private bool IsHeuristicsPaneOpen() => (FindExplorerItem("Heuristics")?.IsEnabled ?? true);
|
|
|
|
private bool IsFilesystemPaneOpen() => (FindExplorerItem("Filesystem")?.IsEnabled ?? true);
|
|
|
|
private bool IsRegistryPaneOpen() => (FindExplorerItem("Registry")?.IsEnabled ?? true);
|
|
|
|
private bool IsRelationsPaneOpen() => (FindExplorerItem("Process Relations")?.IsEnabled ?? true);
|
|
|
|
private static bool IsWindowOpenByTitle(string title) =>
|
|
Application.Current?.Windows.OfType<Window>().Any(w => w.IsVisible &&
|
|
string.Equals(w.Title, title,
|
|
StringComparison.OrdinalIgnoreCase)) ??
|
|
false;
|
|
|
|
private void ToggleFloatDock()
|
|
{
|
|
if (_eventsFloatWindow == null)
|
|
{
|
|
UndockEventsPane();
|
|
}
|
|
else
|
|
{
|
|
RedockEventsPane();
|
|
}
|
|
}
|
|
|
|
private void ToggleEventLogDock()
|
|
{
|
|
if (_eventLogWindow == null)
|
|
{
|
|
UndockEventLog();
|
|
}
|
|
else
|
|
{
|
|
RedockEventLog();
|
|
}
|
|
}
|
|
|
|
private void UndockEventLog()
|
|
{
|
|
if (_eventLogWindow != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_eventLogWindow = new EventLogWindow { Owner = this, ShowInTaskbar = false };
|
|
_eventLogWindow.EtwFeedRequested += EventLogWindow_EtwFeedRequested;
|
|
_eventLogWindow.Closed += EventLogWindow_Closed;
|
|
EventsPaneHost.SetEventLogDetached(true);
|
|
UpdateDetachedEventLogWindow(immediate: true);
|
|
_eventLogWindow.Show();
|
|
}
|
|
|
|
private void RedockEventLog()
|
|
{
|
|
if (_eventLogWindow != null)
|
|
{
|
|
_eventLogWindow.EtwFeedRequested -= EventLogWindow_EtwFeedRequested;
|
|
_eventLogWindow.Closed -= EventLogWindow_Closed;
|
|
_eventLogWindow.Close();
|
|
_eventLogWindow = null;
|
|
}
|
|
|
|
EventsPaneHost.SetEventLogDetached(false);
|
|
}
|
|
|
|
private void EventLogWindow_Closed(object? sender, EventArgs e)
|
|
{
|
|
if (_eventLogWindow != null)
|
|
{
|
|
_eventLogWindow.EtwFeedRequested -= EventLogWindow_EtwFeedRequested;
|
|
}
|
|
_eventLogWindow = null;
|
|
EventsPaneHost.SetEventLogDetached(false);
|
|
}
|
|
|
|
private void DetachedEventLogRefreshTimer_Tick(object? sender, EventArgs e)
|
|
{
|
|
_detachedEventLogRefreshTimer.Stop();
|
|
_eventLogWindow?.RefreshEvents(_focusedEvents);
|
|
}
|
|
|
|
private void UpdateDetachedEventLogWindow(bool immediate = false)
|
|
{
|
|
if (_eventLogWindow == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (immediate)
|
|
{
|
|
_detachedEventLogRefreshTimer.Stop();
|
|
_eventLogWindow.RefreshEvents(_focusedEvents);
|
|
return;
|
|
}
|
|
|
|
_detachedEventLogRefreshTimer.Stop();
|
|
_detachedEventLogRefreshTimer.Start();
|
|
}
|
|
|
|
private void EventLogWindow_EtwFeedRequested(object? sender, EventLogCardOpenRequestedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
if (e == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var displayMatches =
|
|
_focusedEvents
|
|
.Where(ev => EventMatchesLogCard(ev, e.Group, e.SubType, e.Summary, e.Details, e.Pid, e.Tid))
|
|
.ToList();
|
|
OpenEventLogDetailWindow("Event Log Detail", ExpandFocusedEventMembers(displayMatches));
|
|
}
|
|
|
|
private void EventsPaneHost_EventLogEntryOpenRequested(object? sender, EventLogEntryOpenRequestedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
if (e?.Event == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TelemetryEvent selected = e.Event;
|
|
OpenEventLogDetailWindow("Event Detail", GetFocusedEventMembers(selected));
|
|
}
|
|
|
|
private void OpenEventLogDetailWindow(string title, IReadOnlyList<TelemetryEvent> matches)
|
|
{
|
|
if (matches == null || matches.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GroupedEventRow row = BuildEventLogDetailRow(matches);
|
|
var detail = new SimpleEventDetailWindow(title, row) { Owner = this };
|
|
detail.Show();
|
|
detail.Activate();
|
|
}
|
|
|
|
private static bool EventMatchesLogCard(TelemetryEvent ev, string group, string subType, string summary,
|
|
string details, int pid, int tid)
|
|
{
|
|
if (ev == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return string.Equals(ev.Group ?? string.Empty, group ?? string.Empty, StringComparison.OrdinalIgnoreCase) &&
|
|
string.Equals(ev.SubType ?? string.Empty, subType ?? string.Empty,
|
|
StringComparison.OrdinalIgnoreCase) &&
|
|
string.Equals(ev.Summary ?? string.Empty, summary ?? string.Empty, StringComparison.Ordinal) &&
|
|
string.Equals(ev.Details ?? string.Empty, details ?? string.Empty, StringComparison.Ordinal) &&
|
|
ev.PID == pid && ev.TID == tid;
|
|
}
|
|
|
|
private static GroupedEventRow BuildEventLogDetailRow(IReadOnlyList<TelemetryEvent> matches)
|
|
{
|
|
TelemetryEvent first = matches[0];
|
|
string group = string.IsNullOrWhiteSpace(first.Group) ? "Other" : first.Group;
|
|
string subType = first.SubType ?? string.Empty;
|
|
string eventName = string.IsNullOrWhiteSpace(subType) ? group : $"{group}/{subType}";
|
|
string summary = first.Summary ?? string.Empty;
|
|
string key =
|
|
$"{group}\u001F{subType}\u001F{first.PID}\u001F{first.TID}\u001F{summary}\u001F{first.Details}";
|
|
|
|
var row = new GroupedEventRow { GroupKey = key, LastSeenUtc = matches.Max(x => x.TimestampUtc),
|
|
Event = eventName, Severity = "Event",
|
|
Detection = summary, Hits = matches.Count };
|
|
|
|
foreach (TelemetryEvent ev in matches.OrderByDescending(x => x.TimestampUtc))
|
|
{
|
|
string actor = string.IsNullOrWhiteSpace(ev.ProcessName) ? $"pid:{ev.PID}" : ev.ProcessName;
|
|
string target = ev.TID > 0 ? $"tid={ev.TID}" : string.Empty;
|
|
|
|
row.Details.Add(new GroupedEventDetailRow {
|
|
TimestampUtc = ev.TimestampUtc, Event = eventName, Severity = "Event",
|
|
Detection = ev.Summary ?? string.Empty, Source = group, Actor = actor, Target = target,
|
|
ActorPid = ev.PID > 0 ? unchecked((uint)ev.PID) : 0u, TargetPid = 0u,
|
|
ActorToolTip = ev.PID > 0 ? $"PID {ev.PID}" : string.Empty, Details = ev.Details ?? string.Empty
|
|
});
|
|
}
|
|
|
|
return row;
|
|
}
|
|
|
|
private void TogglePerformanceFloatDock()
|
|
{
|
|
if (_performanceFloatWindow == null)
|
|
{
|
|
UndockPerformancePane();
|
|
}
|
|
else
|
|
{
|
|
RedockPerformancePane();
|
|
}
|
|
}
|
|
|
|
private void ToggleEtwFloatDock()
|
|
{
|
|
if (_etwFloatWindow == null)
|
|
UndockEtwPane();
|
|
else
|
|
RedockEtwPane();
|
|
}
|
|
|
|
private void ToggleHeuristicsFloatDock()
|
|
{
|
|
if (_heuristicsFloatWindow == null)
|
|
UndockHeuristicsPane();
|
|
else
|
|
RedockHeuristicsPane();
|
|
}
|
|
|
|
private void ResetDock_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_eventsFloatWindow?.Close();
|
|
_eventLogWindow?.Close();
|
|
_performanceFloatWindow?.Close();
|
|
_etwFloatWindow?.Close();
|
|
_heuristicsFloatWindow?.Close();
|
|
|
|
_laneFocusKey = null;
|
|
EventsPaneHost.Timeline.ClearAllLaneFilters();
|
|
EventsPaneHost.Timeline.ViewDurationSeconds = 120;
|
|
EventsPaneHost.Timeline.ViewStartSeconds = 0;
|
|
EventsPaneHost.Scroll.Value = 0;
|
|
_followLiveTimeline = true;
|
|
ShowEventsPane();
|
|
ShowPerformancePane();
|
|
var sw = FindExplorerItem("ETW");
|
|
if (sw != null)
|
|
sw.IsEnabled = true;
|
|
var heur = FindExplorerItem("Heuristics");
|
|
if (heur != null)
|
|
heur.IsEnabled = true;
|
|
var fs = FindExplorerItem("Filesystem");
|
|
if (fs != null)
|
|
fs.IsEnabled = true;
|
|
var rel = FindExplorerItem("Process Relations");
|
|
if (rel != null)
|
|
rel.IsEnabled = true;
|
|
var ipc = FindExplorerItem("IPC Uplink");
|
|
if (ipc != null)
|
|
ipc.IsEnabled = false;
|
|
_performanceOnTop = false;
|
|
_heuristicsOnTop = false;
|
|
ApplyPaneOrder();
|
|
ApplyIntelPaneOrder();
|
|
HideDockPreview();
|
|
FocusViewport();
|
|
SyncPerformanceViewToTimeline();
|
|
}
|
|
|
|
private void DockEventsToggle_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
ToggleFloatDock();
|
|
}
|
|
|
|
private void DockPerfToggle_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
TogglePerformanceFloatDock();
|
|
}
|
|
|
|
private void DockEtwToggle_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
ToggleEtwFloatDock();
|
|
}
|
|
|
|
private void DockHeurToggle_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
ToggleHeuristicsFloatDock();
|
|
}
|
|
|
|
private void DockSwapMain_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
TogglePaneOrder();
|
|
}
|
|
|
|
private void DockSwapIntel_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
ToggleIntelPaneOrder();
|
|
}
|
|
|
|
private void ChildProcessView_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
OpenOrActivateChildProcessGraphWindow();
|
|
}
|
|
|
|
private void OpenLaneSettings()
|
|
{
|
|
var w = new LaneSettingsWindow(this);
|
|
w.Owner = this;
|
|
w.Show();
|
|
}
|
|
|
|
private void Diagnostics_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
OpenDiagnosticsWindow();
|
|
}
|
|
|
|
private void ExtendedActivityCopySummary_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (TryGetSelectedExtendedActivityRow(sender, out ExtendedActivityRowSnapshot? row))
|
|
{
|
|
Clipboard.SetText(
|
|
$"{row.LastSeenUtc:O} {row.TypeLabel} {row.ActorLabel}->{row.TargetLabel} {row.SubjectLabel} {row.OperationLabel} hits={row.Hits}");
|
|
}
|
|
}
|
|
|
|
private void ExtendedActivityCopyDetails_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (TryGetSelectedExtendedActivityRow(sender, out ExtendedActivityRowSnapshot? row))
|
|
{
|
|
Clipboard.SetText(string.Join(
|
|
Environment.NewLine,
|
|
new[] { $"Type: {row.TypeLabel}", $"Actor: {row.ActorLabel}", $"Target: {row.TargetLabel}",
|
|
$"Subject: {row.SubjectLabel}", $"Operation: {row.OperationLabel}",
|
|
$"Last Seen: {row.LastSeenUtc:O}", $"Hits: {row.Hits}", $"Details: {row.DetailLabel}" }));
|
|
}
|
|
}
|
|
|
|
private void ExtendedActivityGrid_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
DataGridRow? row = FindAncestor<DataGridRow>(e.OriginalSource as DependencyObject);
|
|
if (row == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
row.IsSelected = true;
|
|
row.Focus();
|
|
}
|
|
|
|
private static bool TryGetSelectedExtendedActivityRow(object sender,
|
|
[NotNullWhen(true)] out ExtendedActivityRowSnapshot? row)
|
|
{
|
|
row = null;
|
|
if (sender is MenuItem { Parent : ContextMenu { PlacementTarget : DataGrid grid } } &&
|
|
grid.SelectedItem is ExtendedActivityRowSnapshot selected)
|
|
{
|
|
row = selected;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static T? FindAncestor<T>(DependencyObject? source)
|
|
where T : DependencyObject
|
|
{
|
|
while (source != null)
|
|
{
|
|
if (source is T match)
|
|
{
|
|
return match;
|
|
}
|
|
|
|
source = VisualTreeHelper.GetParent(source);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void PerformancePaneHost_ThreadDoubleClicked(object? sender, ThreadUsageRow row)
|
|
{
|
|
int pid = _currentSession?.Pid ?? TryGetPid();
|
|
var w = new ThreadStackWindow(
|
|
pid, row.Tid, row.State, initialHistory: GetThreadStackHistory(pid, row.Tid, row.State),
|
|
onSnapshotCaptured: snapshot => PersistThreadStackSnapshot(pid, row.Tid, row.State, snapshot),
|
|
observationTimeUtcProvider: GetCurrentObservedUtc,
|
|
liveCaptureAvailableProvider: () => _currentSession != null && _currentSession.Pid == pid &&
|
|
!_currentSession.TargetExited && !_currentSession.OfflineSnapshot,
|
|
threadStateProvider: tid =>
|
|
PerformancePaneHost.SnapshotTopThreads().FirstOrDefault(x => x.Tid == tid)?.State ??
|
|
row.State) { Owner = this };
|
|
w.Show();
|
|
}
|
|
|
|
private void PerformancePaneHost_DisassemblyRequested(object? sender, MemoryDisassemblyRequestedEventArgs e)
|
|
{
|
|
if (_backendSession == null || !BkdcNative.IsAvailable)
|
|
return;
|
|
var w = new DisassemblyWindow(_backendSession, e.ProcessId, e.BaseAddress, e.RegionSize, e.Label,
|
|
e.SnapshotBytes, e.SnapshotOffset) { Owner = this };
|
|
w.Show();
|
|
}
|
|
|
|
private void OpenParallelStacksWindow()
|
|
{
|
|
int pid = _currentSession?.Pid ?? TryGetPid();
|
|
IReadOnlyList<ThreadUsageRow> rows = PerformancePaneHost.SnapshotTopThreads();
|
|
if (pid <= 0 || rows.Count == 0)
|
|
{
|
|
string reason =
|
|
PerformancePaneHost.IsTargetSuspended
|
|
? "The target process is launch-suspended — thread sampling has not run yet. Resume the process first, then open parallel stacks."
|
|
: "No thread rows are available for stack comparison.";
|
|
ThemedMessageBox.Show(this, reason, "Parallel Stacks", MessageBoxButton.OK,
|
|
MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
|
|
var window = new ParallelStacksWindow(
|
|
pid, rows, historyProvider: GetThreadStackHistory, observationTimeUtcProvider: GetCurrentObservedUtc,
|
|
liveCaptureAvailableProvider: () => _currentSession != null && _currentSession.Pid == pid &&
|
|
!_currentSession.TargetExited && !_currentSession.OfflineSnapshot,
|
|
threadSnapshotProvider: () => PerformancePaneHost.SnapshotTopThreads(),
|
|
onSnapshotCaptured: (processId, tid, state, snapshot) =>
|
|
PersistThreadStackSnapshot(processId, tid, state, snapshot)) { Owner = this };
|
|
window.Show();
|
|
}
|
|
|
|
private async void FindProcess_Click(object sender, RoutedEventArgs e) =>
|
|
await OpenProcessPickerAndConnectAsync(showLaunchOptions: true);
|
|
|
|
private async void NewProcessTab_Click(object sender, RoutedEventArgs e) =>
|
|
await OpenProcessPickerAndConnectAsync(showLaunchOptions: true);
|
|
|
|
private void CloseProcessTab_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if ((sender as FrameworkElement)?.Tag is not ProcessSessionTab tab)
|
|
return;
|
|
|
|
if (_processTabs.Count <= 1)
|
|
return;
|
|
|
|
bool wasSelected = ReferenceEquals(ProcessTabs.SelectedItem, tab);
|
|
if (ReferenceEquals(_currentSession, tab))
|
|
SaveCurrentSessionState();
|
|
|
|
_processTabs.Remove(tab);
|
|
|
|
if (_processTabs.Count == 0)
|
|
{
|
|
_currentSession = null;
|
|
PerformancePaneHost.SetProcessLiveDataAvailable(false);
|
|
PerformancePaneHost.SetTargetSuspended(false);
|
|
StatusBlock.Text = "NO TARGET SELECTED";
|
|
RefreshProcessStateBadge();
|
|
return;
|
|
}
|
|
|
|
if (wasSelected)
|
|
{
|
|
_currentSession = null;
|
|
ProcessTabs.SelectedItem = _processTabs[0];
|
|
}
|
|
}
|
|
|
|
private void ProcessTabs_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_suppressTabSelectionChange)
|
|
return;
|
|
|
|
if (ProcessTabs.SelectedItem is not ProcessSessionTab tab)
|
|
return;
|
|
|
|
_queuedSessionSwitchTab = tab;
|
|
if (_sessionSwitchQueued)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_sessionSwitchQueued = true;
|
|
Dispatcher.BeginInvoke(new Action(
|
|
() =>
|
|
{
|
|
_sessionSwitchQueued = false;
|
|
ProcessSessionTab? pending = _queuedSessionSwitchTab;
|
|
_queuedSessionSwitchTab = null;
|
|
if (pending == null || !ReferenceEquals(ProcessTabs.SelectedItem, pending) ||
|
|
_isMainWindowShuttingDown)
|
|
{
|
|
return;
|
|
}
|
|
|
|
SwitchToSession(pending);
|
|
RefreshProcessStateBadge();
|
|
}), DispatcherPriority.Background);
|
|
}
|
|
|
|
private DateTime GetCurrentObservedUtc()
|
|
{
|
|
if (EventsPaneHost?.Timeline == null)
|
|
{
|
|
return DateTime.UtcNow;
|
|
}
|
|
|
|
return _captureStartUtc + TimeSpan.FromSeconds(EventsPaneHost.Timeline.ViewStartSeconds +
|
|
EventsPaneHost.Timeline.ViewDurationSeconds);
|
|
}
|
|
|
|
private static string ResolveHookDllPathFromInterfaceDirectory()
|
|
{
|
|
string baseDirectory = AppContext.BaseDirectory;
|
|
if (string.IsNullOrWhiteSpace(baseDirectory))
|
|
{
|
|
baseDirectory = Environment.CurrentDirectory;
|
|
}
|
|
|
|
return Path.Combine(baseDirectory, "SR71.dll");
|
|
}
|
|
|
|
private static string ResolveDllHostPathFromInterfaceDirectory()
|
|
{
|
|
string baseDirectory = AppContext.BaseDirectory;
|
|
if (string.IsNullOrWhiteSpace(baseDirectory))
|
|
{
|
|
baseDirectory = Environment.CurrentDirectory;
|
|
}
|
|
|
|
return Path.Combine(baseDirectory, "BlackbirdDllHost.exe");
|
|
}
|
|
|
|
private static string QuoteCommandLineArgument(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
return "\"\"";
|
|
}
|
|
|
|
var builder = new System.Text.StringBuilder();
|
|
builder.Append('"');
|
|
int backslashes = 0;
|
|
foreach (char ch in value)
|
|
{
|
|
if (ch == '\\')
|
|
{
|
|
backslashes += 1;
|
|
continue;
|
|
}
|
|
|
|
if (ch == '"')
|
|
{
|
|
builder.Append('\\', backslashes * 2 + 1);
|
|
builder.Append('"');
|
|
backslashes = 0;
|
|
continue;
|
|
}
|
|
|
|
if (backslashes > 0)
|
|
{
|
|
builder.Append('\\', backslashes);
|
|
backslashes = 0;
|
|
}
|
|
builder.Append(ch);
|
|
}
|
|
|
|
if (backslashes > 0)
|
|
{
|
|
builder.Append('\\', backslashes * 2);
|
|
}
|
|
builder.Append('"');
|
|
return builder.ToString();
|
|
}
|
|
|
|
private static string BuildDllHostCommandLineArguments(LaunchProfile launchProfile)
|
|
{
|
|
var args = new List<string> {
|
|
"--dll",
|
|
QuoteCommandLineArgument(launchProfile.AnalysisSubjectPath),
|
|
"--mode",
|
|
launchProfile.DllMode switch { DllLaunchMode.Export => "export", DllLaunchMode.Rundll => "rundll",
|
|
DllLaunchMode.Register => "register",
|
|
DllLaunchMode.Unregister => "unregister",
|
|
DllLaunchMode.Install => "install",
|
|
_ => "load" },
|
|
"--wait-ms",
|
|
launchProfile.DllWaitMilliseconds.ToString(System.Globalization.CultureInfo.InvariantCulture)
|
|
};
|
|
|
|
if (launchProfile.HasDllExportName)
|
|
{
|
|
args.Add("--export");
|
|
args.Add(QuoteCommandLineArgument(launchProfile.DllExportName));
|
|
}
|
|
if (launchProfile.HasDllExportOrdinal)
|
|
{
|
|
args.Add("--ordinal");
|
|
args.Add(launchProfile.DllExportOrdinal.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
|
}
|
|
if (launchProfile.HasDllArgument)
|
|
{
|
|
args.Add("--arg");
|
|
args.Add(QuoteCommandLineArgument(launchProfile.DllArgument));
|
|
}
|
|
if (launchProfile.HasDllLoadFlags)
|
|
{
|
|
args.Add("--load-flags");
|
|
args.Add("0x" +
|
|
launchProfile.DllLoadFlags.ToString("X", System.Globalization.CultureInfo.InvariantCulture));
|
|
}
|
|
if (launchProfile.DllFreeOnExit)
|
|
{
|
|
args.Add("--free-on-exit");
|
|
}
|
|
if (launchProfile.DllInstallDisable)
|
|
{
|
|
args.Add("--install-disable");
|
|
}
|
|
|
|
return string.Join(" ", args);
|
|
}
|
|
|
|
private bool TrySendUserHookRequest(uint mode, uint processId, uint flags, string? imagePath,
|
|
out BlackbirdNative.BkSetUserHookTargetResponse response, out string error)
|
|
{
|
|
response = default;
|
|
error = string.Empty;
|
|
|
|
if (!BkctlDeviceSession.TryOpen(out var control, out error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
using (control)
|
|
{
|
|
string hookPath = ResolveHookDllPathFromInterfaceDirectory();
|
|
if (!File.Exists(hookPath))
|
|
{
|
|
error = $"SetUserHookTarget failed because the hook DLL is missing: '{hookPath}'.";
|
|
return false;
|
|
}
|
|
|
|
if (!BlackbirdNative.SetUserHookTarget(control.Handle, mode, processId, flags, imagePath,
|
|
BlackbirdNative.AnalysisSubjectKindProcess, null, hookPath, null,
|
|
null, null, 0, 0, 0, false,
|
|
BlackbirdNative.LaunchIntegrityDefault, out response))
|
|
{
|
|
error = BkctlDeviceSession.FormatUserHookOperationError(
|
|
"SetUserHookTarget", BlackbirdNative.LastError("SetUserHookTarget failed"), hookPath);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private bool TryAttachUsermodeHooks(int pid, out string error)
|
|
{
|
|
error = string.Empty;
|
|
if (pid <= 0)
|
|
{
|
|
error = "Invalid PID for hook attach.";
|
|
return false;
|
|
}
|
|
|
|
if (!TrySendUserHookRequest(BlackbirdNative.IpcUserHookTargetAttach, unchecked((uint)pid), 0, null, out _,
|
|
out error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
OutputCapture.AppendLine($"Hook attached via controller: PID {pid}");
|
|
return true;
|
|
}
|
|
|
|
private bool EnsureNoHookControllerCallInFlight(string operation)
|
|
{
|
|
if (!_hookControllerCallInFlight)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
string message =
|
|
"A hook launch or attach request is still waiting for the controller to return. " +
|
|
"Blackbird will not start another hook operation until that call unwinds.";
|
|
StatusBlock.Text = "HOOK CONTROLLER REQUEST STILL IN PROGRESS";
|
|
OutputCapture.AppendLine($"Blocked overlapping hook operation ({operation}): previous request in-flight.");
|
|
ThemedMessageBox.Show(this, message, "Hook Operation In Progress", MessageBoxButton.OK,
|
|
MessageBoxImage.Warning);
|
|
return false;
|
|
}
|
|
|
|
private AnalysisOperationGuard BeginAnalysisOperation(string operation)
|
|
{
|
|
var guard = new AnalysisOperationGuard(this, operation);
|
|
_activeAnalysisOperation = guard;
|
|
return guard;
|
|
}
|
|
|
|
private void CancelActiveAnalysisOperationForShutdown()
|
|
{
|
|
AnalysisOperationGuard? guard = _activeAnalysisOperation;
|
|
if (guard != null)
|
|
{
|
|
guard.Cancel();
|
|
guard.CleanupAfterAbort();
|
|
}
|
|
|
|
CompleteHookControllerCall();
|
|
_openingProcessPicker = false;
|
|
_connectInProgress = false;
|
|
}
|
|
|
|
private void CleanupAbortedAnalysisOperation(AnalysisOperationGuard guard)
|
|
{
|
|
DisposePreparedLaunchBackendSession();
|
|
ClearPendingLaunchOptions();
|
|
|
|
if (guard.SessionStartAttempted || _isMainWindowShuttingDown)
|
|
{
|
|
StopLiveAnalysisSession(LiveAnalysisStopReason.OperationAbort, preserveApiGraphSnapshot: true,
|
|
fastTeardown: true, stopPerformance: true);
|
|
}
|
|
|
|
if (guard.LaunchOwnedPid > 0)
|
|
{
|
|
if (TryTerminateTargetProcess(guard.LaunchOwnedPid, out string terminateError))
|
|
{
|
|
AppendOutputFromAnyThread(
|
|
$"Analysis operation cleanup terminated launch-owned target: PID {guard.LaunchOwnedPid}");
|
|
}
|
|
else
|
|
{
|
|
AppendOutputFromAnyThread(
|
|
$"Analysis operation cleanup could not terminate PID {guard.LaunchOwnedPid}: {terminateError}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CompleteHookControllerCall()
|
|
{
|
|
_hookControllerCallInFlight = false;
|
|
}
|
|
|
|
private void TrackLiveAnalysisSession(int pid, ProcessSessionTab? tab)
|
|
{
|
|
_liveAnalysisSession = new LiveAnalysisSessionLease(pid, tab, tab?.LaunchOwnedByInterface == true);
|
|
}
|
|
|
|
private void StopLiveAnalysisSession(LiveAnalysisStopReason reason, bool preserveApiGraphSnapshot = false,
|
|
bool fastTeardown = false, bool stopPerformance = false)
|
|
{
|
|
LiveAnalysisSessionLease? lease = _liveAnalysisSession;
|
|
if (lease != null && !lease.TryBeginTeardown())
|
|
{
|
|
lease = null;
|
|
}
|
|
_liveAnalysisSession = null;
|
|
|
|
StopTargetExitWatcher();
|
|
StopBackendSession(preserveApiGraphSnapshot, fastTeardown);
|
|
|
|
if (stopPerformance)
|
|
{
|
|
_perf?.Stop();
|
|
_samplerPid = 0;
|
|
if (PerformancePaneHost != null)
|
|
{
|
|
PerformancePaneHost.SetProcessLiveDataAvailable(false);
|
|
PerformancePaneHost.SetTargetSuspended(false);
|
|
}
|
|
_targetExecutionSuspended = false;
|
|
}
|
|
|
|
string reasonText = reason switch { LiveAnalysisStopReason.Shutdown => "shutdown",
|
|
LiveAnalysisStopReason.TargetExited => "target-exit",
|
|
LiveAnalysisStopReason.OperationAbort => "operation-abort",
|
|
LiveAnalysisStopReason.SessionSwitch => "session-switch",
|
|
_ => "teardown" };
|
|
if (lease != null)
|
|
{
|
|
AppendOutputFromAnyThread(
|
|
$"Live analysis session detached pid={lease.Pid} reason={reasonText} launchOwned={lease.LaunchOwnedByInterface}");
|
|
}
|
|
}
|
|
|
|
private enum LiveAnalysisStopReason
|
|
{
|
|
SessionSwitch,
|
|
TargetExited,
|
|
OperationAbort,
|
|
Shutdown
|
|
}
|
|
|
|
private sealed class LiveAnalysisSessionLease
|
|
{
|
|
private int _teardownStarted;
|
|
|
|
internal LiveAnalysisSessionLease(int pid, ProcessSessionTab? tab, bool launchOwnedByInterface)
|
|
{
|
|
Pid = pid;
|
|
Tab = tab;
|
|
LaunchOwnedByInterface = launchOwnedByInterface;
|
|
}
|
|
|
|
internal int Pid { get; }
|
|
internal ProcessSessionTab? Tab { get; }
|
|
internal bool LaunchOwnedByInterface { get; }
|
|
|
|
internal bool TryBeginTeardown()
|
|
{
|
|
return Interlocked.Exchange(ref _teardownStarted, 1) == 0;
|
|
}
|
|
}
|
|
|
|
private sealed class AnalysisOperationGuard : IDisposable
|
|
{
|
|
private readonly MainWindow _owner;
|
|
private int _disposed;
|
|
private int _cleanupStarted;
|
|
private bool _completed;
|
|
|
|
internal AnalysisOperationGuard(MainWindow owner, string operation)
|
|
{
|
|
_owner = owner;
|
|
Operation = operation;
|
|
Cancellation = new CancellationTokenSource();
|
|
}
|
|
|
|
internal string Operation { get; }
|
|
internal CancellationTokenSource Cancellation { get; }
|
|
internal CancellationToken Token => Cancellation.Token;
|
|
internal int LaunchOwnedPid { get; private set; }
|
|
internal bool SessionStartAttempted { get; private set; }
|
|
|
|
internal void TrackLaunchOwnedPid(int pid)
|
|
{
|
|
if (pid > 0)
|
|
{
|
|
LaunchOwnedPid = pid;
|
|
}
|
|
}
|
|
|
|
internal void MarkSessionStartAttempted()
|
|
{
|
|
SessionStartAttempted = true;
|
|
}
|
|
|
|
internal void Complete()
|
|
{
|
|
_completed = true;
|
|
}
|
|
|
|
internal void Cancel()
|
|
{
|
|
try
|
|
{
|
|
Cancellation.Cancel();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
internal void CleanupAfterAbort()
|
|
{
|
|
if (_completed || Interlocked.Exchange(ref _cleanupStarted, 1) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_owner.CleanupAbortedAnalysisOperation(this);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(_owner._activeAnalysisOperation, this))
|
|
{
|
|
_owner._activeAnalysisOperation = null;
|
|
}
|
|
|
|
CleanupAfterAbort();
|
|
Cancellation.Dispose();
|
|
}
|
|
}
|
|
|
|
private void AppendOutputFromAnyThread(string message)
|
|
{
|
|
if (Dispatcher.HasShutdownStarted || Dispatcher.HasShutdownFinished)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Dispatcher.CheckAccess())
|
|
{
|
|
OutputCapture.AppendLine(message);
|
|
return;
|
|
}
|
|
|
|
Dispatcher.BeginInvoke(new Action(() => OutputCapture.AppendLine(message)), DispatcherPriority.Background);
|
|
}
|
|
|
|
private void CompleteHookControllerCallFromWorker<T>(Task<T> task, Action<T>? lateResultCleanup)
|
|
{
|
|
Exception? cleanupError = null;
|
|
if (task.Status == TaskStatus.RanToCompletion)
|
|
{
|
|
try
|
|
{
|
|
lateResultCleanup?.Invoke(task.Result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
cleanupError = ex;
|
|
}
|
|
}
|
|
else if (task.IsFaulted)
|
|
{
|
|
_ = task.Exception;
|
|
}
|
|
|
|
if (cleanupError != null)
|
|
{
|
|
AppendOutputFromAnyThread($"Late hook controller cleanup failed: {cleanupError.Message}");
|
|
}
|
|
if (Dispatcher.HasShutdownStarted || Dispatcher.HasShutdownFinished)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Dispatcher.BeginInvoke(new Action(CompleteHookControllerCall), DispatcherPriority.Background);
|
|
}
|
|
|
|
private async Task<(bool Completed, T Result)> RunHookControllerCallWithTimeoutAsync<T>(
|
|
Func<T> operation, LoadingWindow loading, string timeoutStatus, string timeoutDetail,
|
|
Action<T>? lateResultCleanup = null, CancellationToken cancellationToken = default)
|
|
{
|
|
_hookControllerCallInFlight = true;
|
|
Task<T> operationTask = Task.Run(operation);
|
|
loading.StartTimeout(HookControllerCallTimeout);
|
|
|
|
Task delayTask = Task.Delay(HookControllerCallTimeout, cancellationToken);
|
|
Task completed = await Task.WhenAny(operationTask, delayTask);
|
|
if (!ReferenceEquals(completed, operationTask))
|
|
{
|
|
if (!cancellationToken.IsCancellationRequested && !_isMainWindowShuttingDown)
|
|
{
|
|
loading.SetTimedOut(timeoutStatus, timeoutDetail, HookControllerCallTimeout);
|
|
OutputCapture.AppendLine(
|
|
$"Hook controller request timed out after {HookControllerCallTimeoutSeconds}s; waiting for native IPC unwind.");
|
|
}
|
|
else
|
|
{
|
|
AppendOutputFromAnyThread("Hook controller request cancelled during interface teardown.");
|
|
}
|
|
_ = operationTask.ContinueWith(task => CompleteHookControllerCallFromWorker(task, lateResultCleanup),
|
|
CancellationToken.None,
|
|
TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
|
if (!cancellationToken.IsCancellationRequested && !_isMainWindowShuttingDown)
|
|
{
|
|
await Task.Delay(700);
|
|
}
|
|
return (false, default!);
|
|
}
|
|
|
|
try
|
|
{
|
|
T result = await operationTask;
|
|
return (true, result);
|
|
}
|
|
finally
|
|
{
|
|
loading.StopTimeout();
|
|
CompleteHookControllerCall();
|
|
}
|
|
}
|
|
|
|
private bool TryLaunchWithUsermodeHooksAndPrepareSession(string imagePath, bool useEarlyBirdApc,
|
|
LaunchProfile launchProfile, out int pid,
|
|
out BlackbirdBackendSession? preparedSession,
|
|
out string error)
|
|
{
|
|
pid = 0;
|
|
preparedSession = null;
|
|
error = string.Empty;
|
|
if (string.IsNullOrWhiteSpace(imagePath))
|
|
{
|
|
error = "Launch path is empty.";
|
|
return false;
|
|
}
|
|
if (launchProfile.HasCommandLineArguments &&
|
|
launchProfile.CommandLineArguments.Length >= MaxControllerLaunchArgumentChars)
|
|
{
|
|
error =
|
|
$"Launch arguments are too long for the controller IPC limit ({MaxControllerLaunchArgumentChars - 1} characters).";
|
|
return false;
|
|
}
|
|
|
|
uint flags = useEarlyBirdApc ? BlackbirdNative.IpcUserHookFlagLaunchEarlybirdApc : 0;
|
|
if (launchProfile.LeaveSuspendedAfterReady)
|
|
{
|
|
flags |= BlackbirdNative.IpcUserHookFlagDeferredLaunchGateRelease;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!BkctlDeviceSession.TryOpen(out var control, out error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
using (control)
|
|
{
|
|
string hookPath = ResolveHookDllPathFromInterfaceDirectory();
|
|
if (!File.Exists(hookPath))
|
|
{
|
|
error = $"Hook launch failed because the hook DLL is missing: '{hookPath}'.";
|
|
return false;
|
|
}
|
|
|
|
string environmentOverrides = launchProfile.ToIpcEnvironmentOverrideBlock();
|
|
if (!BlackbirdNative.SetUserHookTarget(
|
|
control.Handle, BlackbirdNative.IpcUserHookTargetLaunch, 0, flags, imagePath,
|
|
launchProfile.HasAnalysisSubject ? BlackbirdNative.AnalysisSubjectKindDll
|
|
: BlackbirdNative.AnalysisSubjectKindProcess,
|
|
launchProfile.HasAnalysisSubject ? launchProfile.AnalysisSubjectPath : null, hookPath,
|
|
launchProfile.HasWorkingDirectory ? launchProfile.WorkingDirectory : null,
|
|
string.IsNullOrWhiteSpace(environmentOverrides) ? null : environmentOverrides,
|
|
launchProfile.HasCommandLineArguments ? launchProfile.CommandLineArguments : null,
|
|
launchProfile.ParentProcessId, MapLaunchPriorityClass(launchProfile.Priority),
|
|
launchProfile.AffinityMask, launchProfile.InheritHandles,
|
|
(uint)launchProfile.IntegrityLevel,
|
|
out BlackbirdNative.BkSetUserHookTargetResponse response))
|
|
{
|
|
error = BkctlDeviceSession.FormatUserHookOperationError(
|
|
"SetUserHookTarget(launch)", BlackbirdNative.LastError("SetUserHookTarget(launch) failed"),
|
|
hookPath);
|
|
return false;
|
|
}
|
|
|
|
if (response.ProcessId == 0)
|
|
{
|
|
error = "Controller launch returned no PID.";
|
|
return false;
|
|
}
|
|
|
|
pid = unchecked((int)response.ProcessId);
|
|
preparedSession = BlackbirdBackendSession.StartFromHandle(pid, BlackbirdNative.StreamAll,
|
|
useUsermodeHooks: true, control.Handle);
|
|
_ = control.DetachHandle();
|
|
|
|
OutputCapture.AppendLine(
|
|
launchProfile.HasAnalysisSubject
|
|
? $"Hook launch via controller (earlybird-apc): host={imagePath} subject={launchProfile.AnalysisSubjectPath} \u2192 PID {pid} (pre-armed session)"
|
|
: $"Hook launch via controller (earlybird-apc): {imagePath} \u2192 PID {pid} (pre-armed session)");
|
|
return true;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (preparedSession != null)
|
|
{
|
|
preparedSession.Dispose();
|
|
preparedSession = null;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(error))
|
|
{
|
|
error = ex.Message;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async void Connect_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
using AnalysisOperationGuard operation = BeginAnalysisOperation("attach/analyze");
|
|
await ConnectToCurrentPidAsync(operation.Token, operation);
|
|
if (operation.SessionStartAttempted && !operation.Token.IsCancellationRequested && !_isMainWindowShuttingDown)
|
|
{
|
|
operation.Complete();
|
|
}
|
|
}
|
|
|
|
private async Task ConnectToCurrentPidAsync(CancellationToken cancellationToken,
|
|
AnalysisOperationGuard? operationGuard = null)
|
|
{
|
|
if (_connectInProgress || cancellationToken.IsCancellationRequested || _isMainWindowShuttingDown)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_connectInProgress = true;
|
|
try
|
|
{
|
|
int pid = TryGetPid();
|
|
if (pid <= 0)
|
|
{
|
|
DisposePreparedLaunchBackendSession();
|
|
ClearPendingLaunchOptions();
|
|
StatusBlock.Text = "ENTER A VALID PID";
|
|
RefreshProcessStateBadge();
|
|
return;
|
|
}
|
|
|
|
bool hookPreconfigured = _pendingHookPreconfigured;
|
|
bool allowPreparedLaunchAttach =
|
|
hookPreconfigured && _preparedLaunchBackendSession != null && _preparedLaunchBackendPid == pid;
|
|
if (_pendingLaunchOwnedByInterface)
|
|
{
|
|
operationGuard?.TrackLaunchOwnedPid(pid);
|
|
}
|
|
|
|
if (!TryOpenTargetProcess(pid, out var processName, out var failure, out var accessDenied))
|
|
{
|
|
if (!allowPreparedLaunchAttach)
|
|
{
|
|
DisposePreparedLaunchBackendSession();
|
|
ClearPendingLaunchOptions();
|
|
StatusBlock.Text = failure;
|
|
if (accessDenied)
|
|
{
|
|
ThemedMessageBox.Show(
|
|
this,
|
|
$"Access denied while opening PID {pid}. The process handle could not be opened with required access rights.",
|
|
"Target Attach Failed", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
|
|
RefreshProcessStateBadge();
|
|
return;
|
|
}
|
|
|
|
processName = $"PID {pid}";
|
|
StatusBlock.Text = $"CONNECTED TO PROTECTED TARGET ({pid})";
|
|
OutputCapture.AppendLine(
|
|
$"Protected launch attach accepted via prepared controller session: PID {pid}");
|
|
}
|
|
else
|
|
{
|
|
StatusBlock.Text = $"CONNECTED TO {processName} ({pid})";
|
|
}
|
|
|
|
var tab = AddOrSelectProcessTab(pid, $"{processName} ({pid})", select: true);
|
|
if (_pendingLaunchOptions)
|
|
{
|
|
tab.LaunchOwnedByInterface = _pendingLaunchOwnedByInterface;
|
|
}
|
|
if (_pendingAnalysisSubjectKind == LaunchTargetKind.Dll &&
|
|
!string.IsNullOrWhiteSpace(_pendingAnalysisSubjectPath))
|
|
{
|
|
tab.AnalysisSubjectKind = LaunchTargetKind.Dll;
|
|
tab.AnalysisSubjectPath = _pendingAnalysisSubjectPath;
|
|
tab.AnalysisHostPath = _pendingAnalysisHostPath;
|
|
string subjectName = Path.GetFileName(_pendingAnalysisSubjectPath);
|
|
tab.Title = NormalizeSessionTitle(
|
|
$"{(string.IsNullOrWhiteSpace(subjectName) ? "DLL" : subjectName)} via BlackbirdDllHost ({pid})");
|
|
DiagnosticsState.SetValue("Analysis Subject", tab.AnalysisSubjectPath);
|
|
}
|
|
bool launchStartsSuspended = _pendingLaunchStartsSuspended;
|
|
bool leaveSuspendedAfterReady = _pendingLeaveSuspendedAfterReady;
|
|
if (hookPreconfigured)
|
|
{
|
|
OutputCapture.AppendLine(
|
|
$"Protected launch attach state: pid={pid} backendPrepared={allowPreparedLaunchAttach} leaveSuspended={leaveSuspendedAfterReady} uiStartsSuspended={launchStartsSuspended}");
|
|
}
|
|
if (_pendingLaunchOptions)
|
|
{
|
|
tab.UseUsermodeHooks = _pendingUseUsermodeHooks;
|
|
tab.AutoOpenApiGraphOnNextStart = _pendingAutoOpenApiGraph;
|
|
}
|
|
tab.LaunchStartsSuspendedPending = launchStartsSuspended;
|
|
tab.DeferredLaunchGateResumePending = false;
|
|
ClearPendingLaunchOptions();
|
|
|
|
if (tab.UseUsermodeHooks && !hookPreconfigured)
|
|
{
|
|
LoadingWindow? hookLoading = null;
|
|
try
|
|
{
|
|
if (cancellationToken.IsCancellationRequested || _isMainWindowShuttingDown)
|
|
{
|
|
DisposePreparedLaunchBackendSession();
|
|
return;
|
|
}
|
|
if (!EnsureNoHookControllerCallInFlight("attach usermode hooks"))
|
|
{
|
|
DisposePreparedLaunchBackendSession();
|
|
return;
|
|
}
|
|
|
|
hookLoading = new LoadingWindow { Owner = this };
|
|
hookLoading.SetProgress(38, "Attaching usermode hooks...",
|
|
$"Injecting SR71.dll into PID {pid}. UI timeout is {HookControllerCallTimeoutSeconds}s.");
|
|
hookLoading.Show();
|
|
await Dispatcher.InvokeAsync(() =>
|
|
{},
|
|
DispatcherPriority.Render);
|
|
|
|
var hookWait = await RunHookControllerCallWithTimeoutAsync(
|
|
() =>
|
|
{
|
|
bool ok = TryAttachUsermodeHooks(pid, out string err);
|
|
return (ok, err);
|
|
},
|
|
hookLoading,
|
|
"Hook attach timed out",
|
|
$"The controller did not complete SR71 attach for PID {pid} within {HookControllerCallTimeoutSeconds}s.",
|
|
cancellationToken: cancellationToken);
|
|
|
|
if (!hookWait.Completed)
|
|
{
|
|
DisposePreparedLaunchBackendSession();
|
|
if (cancellationToken.IsCancellationRequested || _isMainWindowShuttingDown)
|
|
{
|
|
return;
|
|
}
|
|
StatusBlock.Text = $"HOOK ATTACH TIMED OUT FOR PID {pid}";
|
|
ThemedMessageBox.Show(
|
|
this,
|
|
$"Timed out attaching usermode hooks to PID {pid}. The controller IPC call is still being allowed to unwind, and overlapping hook operations are blocked until it returns.",
|
|
"Hook Attach Timed Out", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
RefreshProcessStateBadge();
|
|
return;
|
|
}
|
|
|
|
var hookResult = hookWait.Result;
|
|
|
|
if (!hookResult.ok)
|
|
{
|
|
DisposePreparedLaunchBackendSession();
|
|
StatusBlock.Text = $"HOOK ATTACH FAILED FOR PID {pid}";
|
|
ThemedMessageBox.Show(this,
|
|
$"Failed to attach usermode hooks for PID {pid}.\n\n{hookResult.err}",
|
|
"Hook Attach Failed", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
RefreshProcessStateBadge();
|
|
return;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (hookLoading != null && hookLoading.IsVisible)
|
|
{
|
|
hookLoading.Close();
|
|
}
|
|
}
|
|
|
|
if (!IsLoaded)
|
|
{
|
|
DisposePreparedLaunchBackendSession();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (cancellationToken.IsCancellationRequested || _isMainWindowShuttingDown)
|
|
{
|
|
DisposePreparedLaunchBackendSession();
|
|
return;
|
|
}
|
|
|
|
tab.TargetExited = false;
|
|
tab.TargetExitReason = string.Empty;
|
|
tab.OfflineSnapshot = false;
|
|
if (!ReferenceEquals(_currentSession, tab))
|
|
{
|
|
operationGuard?.MarkSessionStartAttempted();
|
|
SwitchToSession(tab);
|
|
}
|
|
else
|
|
{
|
|
operationGuard?.MarkSessionStartAttempted();
|
|
StartLiveCaptureForPid(pid, tab.UseUsermodeHooks, launchStartsSuspended);
|
|
}
|
|
|
|
if (hookPreconfigured && !leaveSuspendedAfterReady)
|
|
{
|
|
OutputCapture.AppendLine($"Protected launch auto-resume requested: PID {pid}");
|
|
if (!TryControlTargetExecution(suspend: false, out string resumeError))
|
|
{
|
|
OutputCapture.AppendLine($"Protected launch auto-resume failed: PID {pid} error={resumeError}");
|
|
_targetExecutionSuspended = true;
|
|
PerformancePaneHost.SetTargetSuspended(true);
|
|
StatusBlock.Text = $"SESSION READY, TARGET STILL SUSPENDED: PID {pid}";
|
|
ThemedMessageBox.Show(
|
|
this,
|
|
$"Blackbird attached successfully, but the target could not be resumed automatically.\n\n{resumeError}",
|
|
"Launch Resume Failed", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
RefreshProcessStateBadge();
|
|
RefreshToolbarCommandState();
|
|
return;
|
|
}
|
|
|
|
_targetExecutionSuspended = false;
|
|
PerformancePaneHost.SetTargetSuspended(false);
|
|
StatusBlock.Text =
|
|
$"LIVE CAPTURE READY: {NormalizeSessionTitle(_currentSession?.Title ?? $"PID {pid}")}";
|
|
OutputCapture.AppendLine($"Protected launch auto-resume completed: PID {pid}");
|
|
}
|
|
else if (launchStartsSuspended)
|
|
{
|
|
if (hookPreconfigured)
|
|
{
|
|
OutputCapture.AppendLine($"Protected launch left suspended by operator option: PID {pid}");
|
|
}
|
|
_targetExecutionSuspended = true;
|
|
PerformancePaneHost.SetTargetSuspended(true);
|
|
StatusBlock.Text =
|
|
$"LIVE CAPTURE READY, TARGET SUSPENDED: {NormalizeSessionTitle(_currentSession?.Title ?? $"PID {pid}")}";
|
|
}
|
|
|
|
_ = RunPreflightAsync(pid);
|
|
RefreshProcessStateBadge();
|
|
}
|
|
finally
|
|
{
|
|
_connectInProgress = false;
|
|
}
|
|
}
|
|
|
|
private async void Launch_Click(object sender, RoutedEventArgs e) =>
|
|
await OpenProcessPickerAndConnectAsync(showLaunchOptions: true);
|
|
|
|
private async Task OpenProcessPickerAndConnectAsync(bool showLaunchOptions)
|
|
{
|
|
if (_openingProcessPicker)
|
|
return;
|
|
|
|
DisposePreparedLaunchBackendSession();
|
|
ClearPendingLaunchOptions();
|
|
_openingProcessPicker = true;
|
|
LoadingWindow? loading = null;
|
|
|
|
try
|
|
{
|
|
loading = new LoadingWindow { Owner = this };
|
|
loading.SetProgress(14, "Preparing process picker...", "Initializing process view shell.");
|
|
loading.Show();
|
|
await Dispatcher.InvokeAsync(() =>
|
|
{},
|
|
System.Windows.Threading.DispatcherPriority.Render);
|
|
|
|
var picker = new ProcessPickerWindow { Owner = this, ShowLaunchOptions = showLaunchOptions };
|
|
|
|
loading.SetProgress(62, "Preparing process list...", "Enumerating launch targets before showing picker.");
|
|
await Dispatcher.InvokeAsync(() =>
|
|
{},
|
|
DispatcherPriority.Render);
|
|
picker.PrimeForFirstShow();
|
|
loading.SetProgress(100, "Opening picker...",
|
|
"Process picker is ready.");
|
|
await Dispatcher.InvokeAsync(() =>
|
|
{},
|
|
DispatcherPriority.Render);
|
|
loading.Close();
|
|
loading = null;
|
|
|
|
bool? result = picker.ShowDialog();
|
|
if (result != true)
|
|
return;
|
|
|
|
int selectedPid = picker.SelectedPid;
|
|
bool hookPreconfigured = false;
|
|
bool useUsermodeHooks = false;
|
|
bool autoOpenApiGraph = false;
|
|
bool useEarlyBirdApcLaunch = false;
|
|
LaunchTargetKind launchTargetKind =
|
|
picker.LaunchSelectedImage ? picker.SelectedLaunchTargetKind : LaunchTargetKind.Executable;
|
|
LaunchProfile launchProfile = new();
|
|
if (showLaunchOptions && (picker.LaunchSelectedImage || selectedPid > 0))
|
|
{
|
|
var parametersWindow = new LaunchParametersWindow(isLaunchTarget: picker.LaunchSelectedImage,
|
|
targetKind: launchTargetKind) { Owner = this };
|
|
bool? parametersAccepted = parametersWindow.ShowDialog();
|
|
if (parametersAccepted != true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
useUsermodeHooks = parametersWindow.UseUsermodeHooks;
|
|
autoOpenApiGraph = parametersWindow.AutoOpenApiGraphWindow;
|
|
useEarlyBirdApcLaunch = parametersWindow.UseEarlyBirdApcLaunch;
|
|
launchProfile = parametersWindow.LaunchProfile;
|
|
launchProfile.TargetKind = launchTargetKind;
|
|
}
|
|
|
|
using AnalysisOperationGuard operation =
|
|
BeginAnalysisOperation(showLaunchOptions && picker.LaunchSelectedImage
|
|
? "launch/analyze"
|
|
: "attach/analyze");
|
|
|
|
if (showLaunchOptions && picker.LaunchSelectedImage)
|
|
{
|
|
string selectedImagePath = picker.LaunchImagePath?.Trim() ?? string.Empty;
|
|
if (string.IsNullOrWhiteSpace(selectedImagePath))
|
|
{
|
|
ThemedMessageBox.Show(this, "Launch path is empty.", "Launch failed", MessageBoxButton.OK,
|
|
MessageBoxImage.Error);
|
|
return;
|
|
}
|
|
|
|
string launchImagePath = selectedImagePath;
|
|
if (launchTargetKind == LaunchTargetKind.Dll)
|
|
{
|
|
string dllHostPath = ResolveDllHostPathFromInterfaceDirectory();
|
|
if (!File.Exists(dllHostPath))
|
|
{
|
|
ThemedMessageBox.Show(
|
|
this, $"DLL launch failed because the DLL host is missing: '{dllHostPath}'.",
|
|
"DLL launch failed", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return;
|
|
}
|
|
|
|
launchProfile.TargetKind = LaunchTargetKind.Dll;
|
|
launchProfile.AnalysisSubjectPath = selectedImagePath;
|
|
launchProfile.AnalysisHostPath = dllHostPath;
|
|
launchProfile.CommandLineArguments = BuildDllHostCommandLineArguments(launchProfile);
|
|
if (!launchProfile.HasWorkingDirectory)
|
|
{
|
|
launchProfile.WorkingDirectory = Path.GetDirectoryName(selectedImagePath) ?? string.Empty;
|
|
}
|
|
launchImagePath = dllHostPath;
|
|
useUsermodeHooks = true;
|
|
useEarlyBirdApcLaunch = true;
|
|
}
|
|
|
|
if (useUsermodeHooks)
|
|
{
|
|
LoadingWindow? launchLoading = null;
|
|
bool launchOk;
|
|
int launchedPid = 0;
|
|
BlackbirdBackendSession? preparedSession = null;
|
|
string launchError = string.Empty;
|
|
try
|
|
{
|
|
if (!EnsureNoHookControllerCallInFlight("launch target with hooks"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
launchLoading = new LoadingWindow { Owner = this };
|
|
launchLoading.SetProgress(42, "Launching target with hooks...",
|
|
"Submitting launch + SR71 staging request. UI timeout is " +
|
|
HookControllerCallTimeoutSeconds + "s.");
|
|
launchLoading.Show();
|
|
await Dispatcher.InvokeAsync(() =>
|
|
{},
|
|
DispatcherPriority.Render);
|
|
|
|
var launchWait = await RunHookControllerCallWithTimeoutAsync(
|
|
() =>
|
|
{
|
|
bool ok = TryLaunchWithUsermodeHooksAndPrepareSession(
|
|
launchImagePath, useEarlyBirdApcLaunch, launchProfile,
|
|
out int taskPid, out BlackbirdBackendSession? taskSession,
|
|
out string taskError);
|
|
return (ok, taskPid, taskSession, taskError);
|
|
},
|
|
launchLoading,
|
|
"Hook launch timed out",
|
|
"The controller did not return from target launch and SR71 readiness " +
|
|
$"within {HookControllerCallTimeoutSeconds}s.",
|
|
lateResult =>
|
|
{
|
|
lateResult.taskSession?.Dispose();
|
|
if (lateResult.taskPid > 0)
|
|
{
|
|
if (TryTerminateTargetProcess(lateResult.taskPid,
|
|
out string terminateError))
|
|
{
|
|
AppendOutputFromAnyThread(
|
|
$"Late hook launch result was terminated fail-closed: PID {lateResult.taskPid}");
|
|
}
|
|
else
|
|
{
|
|
AppendOutputFromAnyThread(
|
|
$"Late hook launch result cleanup could not terminate PID {lateResult.taskPid}: {terminateError}");
|
|
}
|
|
}
|
|
},
|
|
cancellationToken: operation.Token);
|
|
|
|
if (!launchWait.Completed)
|
|
{
|
|
launchOk = false;
|
|
launchError =
|
|
$"Controller hook launch did not return within {HookControllerCallTimeoutSeconds} seconds. " +
|
|
"Blackbird has blocked overlapping hook launches until the native IPC call unwinds.";
|
|
}
|
|
else
|
|
{
|
|
launchOk = launchWait.Result.ok;
|
|
launchedPid = launchWait.Result.taskPid;
|
|
preparedSession = launchWait.Result.taskSession;
|
|
launchError = launchWait.Result.taskError;
|
|
if (launchedPid > 0)
|
|
{
|
|
operation.TrackLaunchOwnedPid(launchedPid);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (launchLoading != null && launchLoading.IsVisible)
|
|
{
|
|
launchLoading.Close();
|
|
}
|
|
}
|
|
|
|
if (!launchOk)
|
|
{
|
|
if (preparedSession != null)
|
|
{
|
|
try
|
|
{
|
|
preparedSession.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
if (operation.Token.IsCancellationRequested || _isMainWindowShuttingDown)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ThemedMessageBox.Show(this, launchError, "Hook launch failed", MessageBoxButton.OK,
|
|
MessageBoxImage.Error);
|
|
return;
|
|
}
|
|
|
|
selectedPid = launchedPid;
|
|
operation.TrackLaunchOwnedPid(selectedPid);
|
|
_preparedLaunchBackendSession = preparedSession;
|
|
_preparedLaunchBackendPid = selectedPid;
|
|
hookPreconfigured = true;
|
|
}
|
|
else
|
|
{
|
|
if (!BlackbirdNative.TryLaunchProcess(launchImagePath, launchProfile, out selectedPid,
|
|
out string launchError))
|
|
{
|
|
ThemedMessageBox.Show(this, launchError, "Launch failed", MessageBoxButton.OK,
|
|
MessageBoxImage.Error);
|
|
return;
|
|
}
|
|
|
|
operation.TrackLaunchOwnedPid(selectedPid);
|
|
}
|
|
}
|
|
|
|
if (selectedPid <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_pendingLaunchOptions = showLaunchOptions;
|
|
_pendingUseUsermodeHooks = showLaunchOptions && useUsermodeHooks;
|
|
_pendingAutoOpenApiGraph = showLaunchOptions && autoOpenApiGraph;
|
|
_pendingHookPreconfigured = hookPreconfigured;
|
|
_pendingLaunchStartsSuspended =
|
|
showLaunchOptions && picker.LaunchSelectedImage && launchProfile.LeaveSuspendedAfterReady;
|
|
_pendingLeaveSuspendedAfterReady =
|
|
showLaunchOptions && picker.LaunchSelectedImage && launchProfile.LeaveSuspendedAfterReady;
|
|
_pendingLaunchOwnedByInterface = showLaunchOptions && picker.LaunchSelectedImage;
|
|
_pendingAnalysisSubjectKind = launchProfile.TargetKind;
|
|
_pendingAnalysisSubjectPath =
|
|
launchProfile.HasAnalysisSubject ? launchProfile.AnalysisSubjectPath : string.Empty;
|
|
_pendingAnalysisHostPath =
|
|
!string.IsNullOrWhiteSpace(launchProfile.AnalysisHostPath)
|
|
? launchProfile.AnalysisHostPath
|
|
: (launchProfile.HasAnalysisSubject ? ResolveDllHostPathFromInterfaceDirectory()
|
|
: string.Empty);
|
|
PidBox.Text = selectedPid.ToString();
|
|
await ConnectToCurrentPidAsync(operation.Token, operation);
|
|
if (operation.SessionStartAttempted && !operation.Token.IsCancellationRequested &&
|
|
!_isMainWindowShuttingDown)
|
|
{
|
|
operation.Complete();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (loading != null && loading.IsVisible)
|
|
{
|
|
loading.Close();
|
|
}
|
|
|
|
_openingProcessPicker = false;
|
|
}
|
|
}
|
|
|
|
internal void ConnectToStartupPid(int pid)
|
|
{
|
|
if (pid <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PidBox.Text = pid.ToString();
|
|
Connect_Click(this, new RoutedEventArgs());
|
|
}
|
|
|
|
internal async Task BeginStartupLaunchFlowAsync()
|
|
{
|
|
await OpenProcessPickerAndConnectAsync(showLaunchOptions: true);
|
|
}
|
|
|
|
private void ApplyPausedTimelineRanges()
|
|
{
|
|
if (EventsPaneHost?.Timeline == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_currentSession == null)
|
|
{
|
|
EventsPaneHost.Timeline.SetPauseRanges(null);
|
|
return;
|
|
}
|
|
|
|
var ranges = _currentSession.PausedTimelineSpans
|
|
.Select(x => new TimelinePauseRange { StartUtc = x.StartUtc, EndUtc = x.EndUtc })
|
|
.ToList();
|
|
|
|
if (_currentSession.ActivePauseStartUtc.HasValue)
|
|
{
|
|
ranges.Add(new TimelinePauseRange { StartUtc = _currentSession.ActivePauseStartUtc.Value,
|
|
EndUtc = DateTime.UtcNow });
|
|
}
|
|
|
|
EventsPaneHost.Timeline.SetPauseRanges(ranges);
|
|
}
|
|
|
|
private void DisposePreparedLaunchBackendSession()
|
|
{
|
|
if (_preparedLaunchBackendSession != null)
|
|
{
|
|
try
|
|
{
|
|
_preparedLaunchBackendSession.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
_preparedLaunchBackendSession = null;
|
|
_preparedLaunchBackendPid = 0;
|
|
}
|
|
|
|
private void ClearPendingLaunchOptions()
|
|
{
|
|
_pendingLaunchOptions = false;
|
|
_pendingUseUsermodeHooks = false;
|
|
_pendingAutoOpenApiGraph = false;
|
|
_pendingHookPreconfigured = false;
|
|
_pendingLaunchStartsSuspended = false;
|
|
_pendingLeaveSuspendedAfterReady = false;
|
|
_pendingLaunchOwnedByInterface = false;
|
|
_pendingAnalysisSubjectKind = LaunchTargetKind.Executable;
|
|
_pendingAnalysisSubjectPath = string.Empty;
|
|
_pendingAnalysisHostPath = string.Empty;
|
|
}
|
|
|
|
private static uint MapLaunchPriorityClass(LaunchPriorityPreset priority) => priority switch {
|
|
LaunchPriorityPreset.Idle => 0x00000040,
|
|
LaunchPriorityPreset.BelowNormal => 0x00004000,
|
|
LaunchPriorityPreset.Normal => 0x00000020,
|
|
LaunchPriorityPreset.AboveNormal => 0x00008000,
|
|
LaunchPriorityPreset.High => 0x00000080,
|
|
LaunchPriorityPreset.Realtime => 0x00000100,
|
|
_ => 0u
|
|
};
|
|
|
|
internal async Task<string> TryOpenSessionFromStartupPathAsync(string path)
|
|
{
|
|
return await TryOpenSessionArchivePathAsync(path, merge: false);
|
|
}
|
|
|
|
private void Suspend_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!TryControlTargetExecution(suspend: true, out string error))
|
|
{
|
|
ThemedMessageBox.Show(this, error, "Suspend Target", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return;
|
|
}
|
|
|
|
_targetExecutionSuspended = true;
|
|
if (_currentSession != null && !_currentSession.OfflineSnapshot && !_currentSession.TargetExited &&
|
|
!_currentSession.ActivePauseStartUtc.HasValue)
|
|
{
|
|
_currentSession.ActivePauseStartUtc = DateTime.UtcNow;
|
|
}
|
|
_perf?.Stop();
|
|
PerformancePaneHost.SetTargetSuspended(true);
|
|
PerformancePaneHost.SetProcessLiveDataAvailable(false);
|
|
PerformancePaneHost.RefreshLiveProcessDetails();
|
|
ApplyPausedTimelineRanges();
|
|
SetIntegrityDiagnosticsForSuspension();
|
|
StatusBlock.Text = $"TARGET PAUSED: PID {TryGetPid()}";
|
|
RefreshProcessStateBadge();
|
|
RefreshToolbarCommandState();
|
|
}
|
|
|
|
private void Resume_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!TryControlTargetExecution(suspend: false, out string error))
|
|
{
|
|
ThemedMessageBox.Show(this, error, "Resume Target", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return;
|
|
}
|
|
|
|
if (_currentSession != null)
|
|
{
|
|
_currentSession.DeferredLaunchGateResumePending = false;
|
|
}
|
|
_targetExecutionSuspended = false;
|
|
ClearIntegrityDiagnosticsForSuspension();
|
|
PerformancePaneHost.SetTargetSuspended(false);
|
|
if (_currentSession != null && _currentSession.ActivePauseStartUtc.HasValue)
|
|
{
|
|
DateTime resumeUtc = DateTime.UtcNow;
|
|
_currentSession.PausedTimelineSpans.Add(
|
|
new PausedTimelineSpan { StartUtc = _currentSession.ActivePauseStartUtc.Value,
|
|
EndUtc = resumeUtc });
|
|
_currentSession.ActivePauseStartUtc = null;
|
|
_latestEventTimestampUtc = resumeUtc;
|
|
ApplyPausedTimelineRanges();
|
|
UpdateScrollBar();
|
|
_followLiveTimeline = true;
|
|
double viewport = Math.Max(1, EventsPaneHost.Scroll.ViewportSize);
|
|
double maxStart = ComputeTimelineMaxStart(viewport);
|
|
EventsPaneHost.Timeline.ViewStartSeconds = maxStart;
|
|
EventsPaneHost.Scroll.Value = maxStart;
|
|
SyncPerformanceViewToTimeline();
|
|
}
|
|
if (_currentSession != null && !_currentSession.OfflineSnapshot && !_currentSession.TargetExited &&
|
|
_perf != null)
|
|
{
|
|
_perf.SetTargetPid(_currentSession.Pid);
|
|
_perf.ResetBaselines();
|
|
_perf.Start();
|
|
_perf.RequestImmediateSample();
|
|
}
|
|
PerformancePaneHost.SetProcessLiveDataAvailable(true);
|
|
PerformancePaneHost.RefreshLiveProcessDetails();
|
|
SchedulePostResumeProcessRefresh(pid: _currentSession?.Pid ?? TryGetPid());
|
|
StatusBlock.Text = $"TARGET RESUMED: PID {TryGetPid()}";
|
|
RefreshProcessStateBadge();
|
|
RefreshToolbarCommandState();
|
|
}
|
|
|
|
private void SchedulePostResumeProcessRefresh(int pid)
|
|
{
|
|
if (pid <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
await Task.Delay(250).ConfigureAwait(false);
|
|
await Dispatcher.InvokeAsync(
|
|
() =>
|
|
{
|
|
ProcessSessionTab? session = _currentSession;
|
|
if (_targetExecutionSuspended || session == null || session.Pid != pid ||
|
|
session.TargetExited)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_perf?.RequestImmediateSample();
|
|
PerformancePaneHost.SetProcessLiveDataAvailable(true);
|
|
PerformancePaneHost.RefreshLiveProcessDetails();
|
|
RefreshProcessStateBadge();
|
|
RefreshToolbarCommandState();
|
|
},
|
|
DispatcherPriority.Background);
|
|
});
|
|
}
|
|
|
|
private static readonly string[] _integrityDiagnosticKeys =
|
|
["Hook Integrity", "AMSI Integrity", "ETW Integrity"];
|
|
|
|
private static void SetIntegrityDiagnosticsForSuspension()
|
|
{
|
|
foreach (string key in _integrityDiagnosticKeys)
|
|
{
|
|
string? current = DiagnosticsState.GetValue(key);
|
|
if (current != null && current.Contains("Disabled", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (current == null || !current.Contains("TAMPERED", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
DiagnosticsState.SetValue(key, "INACTIVE (target suspended)");
|
|
}
|
|
}
|
|
if (DiagnosticsState.GetValue("Usermode Hooks")?.Contains("Disabled", StringComparison.OrdinalIgnoreCase) !=
|
|
true)
|
|
{
|
|
DiagnosticsState.SetValue("Usermode Hooks", "INACTIVE (target suspended)");
|
|
}
|
|
}
|
|
|
|
private static void ClearIntegrityDiagnosticsForSuspension()
|
|
{
|
|
foreach (string key in _integrityDiagnosticKeys)
|
|
{
|
|
string? current = DiagnosticsState.GetValue(key);
|
|
if (current != null && current.Contains("INACTIVE", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
DiagnosticsState.SetValue(key, "INACTIVE (resuming)");
|
|
}
|
|
}
|
|
if (DiagnosticsState.GetValue("Usermode Hooks")?.Contains("INACTIVE", StringComparison.OrdinalIgnoreCase) ==
|
|
true)
|
|
{
|
|
DiagnosticsState.SetValue("Usermode Hooks", "INACTIVE (resuming)");
|
|
}
|
|
}
|
|
|
|
private void TerminateLaunchOwnedTargetsOnShutdown()
|
|
{
|
|
foreach (ProcessSessionTab tab in _processTabs)
|
|
{
|
|
if (!tab.LaunchOwnedByInterface || tab.OfflineSnapshot || tab.TargetExited || tab.Pid <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (TryTerminateTargetProcess(tab.Pid, out _))
|
|
{
|
|
tab.TargetExited = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TerminateTarget_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
int pid = _currentSession?.Pid ?? TryGetPid();
|
|
if (pid <= 0)
|
|
{
|
|
ThemedMessageBox.Show(this, "No target process is selected.", "Terminate Target", MessageBoxButton.OK,
|
|
MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
if (_currentSession?.OfflineSnapshot == true)
|
|
{
|
|
ThemedMessageBox.Show(this, "Cannot terminate an offline session.", "Terminate Target",
|
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
if (_currentSession?.TargetExited == true)
|
|
{
|
|
ThemedMessageBox.Show(this, $"PID {pid} has already exited.", "Terminate Target", MessageBoxButton.OK,
|
|
MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
|
|
var confirm = ThemedMessageBox.Show(this, $"Terminate PID {pid} and stop live capture for this tab?",
|
|
"Terminate Target", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
|
if (confirm != MessageBoxResult.Yes)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryTerminateTargetProcess(pid, out string error))
|
|
{
|
|
ThemedMessageBox.Show(this, error, "Terminate Target", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return;
|
|
}
|
|
|
|
HandleTargetProcessExit(pid);
|
|
}
|
|
private void Restart_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
}
|
|
|
|
private bool TryControlTargetExecution(bool suspend, out string error)
|
|
{
|
|
error = string.Empty;
|
|
int pid = _currentSession?.Pid ?? TryGetPid();
|
|
if (pid <= 0)
|
|
{
|
|
error = "No target process is selected.";
|
|
return false;
|
|
}
|
|
|
|
if (_currentSession?.OfflineSnapshot == true)
|
|
{
|
|
error = "Cannot control execution for an offline session.";
|
|
return false;
|
|
}
|
|
|
|
if (_currentSession?.TargetExited == true)
|
|
{
|
|
error = $"PID {pid} has already exited.";
|
|
return false;
|
|
}
|
|
|
|
if (_backendSession == null)
|
|
{
|
|
error = "No active backend session.";
|
|
return false;
|
|
}
|
|
|
|
bool ok = _backendSession.ControlProcessExecution(unchecked((uint)pid), suspend);
|
|
if (!ok)
|
|
{
|
|
int ioErr = Marshal.GetLastWin32Error();
|
|
error = $"{(suspend ? "Suspend" : "Resume")} failed via controller (win32={ioErr}).";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static string AppendEnvironmentOverride(string existing, string line)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(line))
|
|
{
|
|
return existing ?? string.Empty;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(existing))
|
|
{
|
|
return line;
|
|
}
|
|
|
|
string[] entries = existing.Replace("\r\n", "\n", StringComparison.Ordinal)
|
|
.Replace('\r', '\n')
|
|
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
|
|
int separator = line.IndexOf('=');
|
|
if (separator > 0)
|
|
{
|
|
string name = line[..separator];
|
|
foreach (string entry in entries)
|
|
{
|
|
int existingSeparator = entry.IndexOf('=');
|
|
if (existingSeparator > 0 &&
|
|
string.Equals(entry[..existingSeparator], name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return existing;
|
|
}
|
|
}
|
|
}
|
|
|
|
return existing + "\n" + line;
|
|
}
|
|
|
|
private bool TryTerminateTargetProcess(int pid, out string error)
|
|
{
|
|
error = string.Empty;
|
|
IntPtr handle = Kernel32Native.OpenProcess(
|
|
ProcessTerminate | ProcessQueryLimitedInformation | ProcessSynchronize, false, unchecked((uint)pid));
|
|
if (handle == IntPtr.Zero)
|
|
{
|
|
int openErr = Marshal.GetLastWin32Error();
|
|
error = $"Failed to open PID {pid} for termination (win32={openErr}).";
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!Kernel32Native.TerminateProcess(handle, 1))
|
|
{
|
|
int terminateErr = Marshal.GetLastWin32Error();
|
|
error = $"TerminateProcess failed for PID {pid} (win32={terminateErr}).";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
finally
|
|
{
|
|
_ = Kernel32Native.CloseHandle(handle);
|
|
}
|
|
}
|
|
|
|
private void SeedFake()
|
|
{
|
|
var t0 = _captureStartUtc;
|
|
|
|
AppendEvent(new TelemetryEvent { TimestampUtc = t0.AddSeconds(1), PID = 1234, TID = 10, Group = "Execution",
|
|
SubType = "CreateProcess", Summary = "proc created" });
|
|
AppendEvent(new TelemetryEvent { TimestampUtc = t0.AddSeconds(2), PID = 1234, TID = 11, Group = "Thread",
|
|
SubType = "RemoteThread", Summary = "remote thread start" });
|
|
AppendEvent(new TelemetryEvent { TimestampUtc = t0.AddSeconds(6), PID = 1234, TID = 12, Group = "Registry",
|
|
SubType = "SetValue", Summary = "reg set value" });
|
|
AppendEvent(new TelemetryEvent { TimestampUtc = t0.AddSeconds(12), PID = 1234, TID = 13, Group = "Handles",
|
|
SubType = "Duplicate", Summary = "dup handle" });
|
|
AppendEvent(new TelemetryEvent { TimestampUtc = t0.AddSeconds(35), PID = 1234, TID = 14,
|
|
Group = "Injection", SubType = "MapView", Summary = "write+map" });
|
|
}
|
|
|
|
private void UndockEventsPane()
|
|
{
|
|
if (_eventsFloatWindow != null)
|
|
return;
|
|
|
|
if (EventsDockBorder.Child == EventsPaneHost)
|
|
EventsDockBorder.Child = null;
|
|
|
|
_eventsPaneFloating = true;
|
|
_eventsPaneVisible = true;
|
|
ApplyDockVisibilityFromExplorer();
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
if (_eventsFloatWindow != null)
|
|
return;
|
|
|
|
_eventsFloatWindow = new EventsFloatWindow(
|
|
EventsPaneHost) { Owner = this, ShowInTaskbar = false };
|
|
_eventsFloatWindow.Closing += EventsFloatWindow_Closing;
|
|
_eventsFloatWindow.Closed += EventsFloatWindow_Closed;
|
|
_eventsFloatWindow.Show();
|
|
}),
|
|
System.Windows.Threading.DispatcherPriority.Background);
|
|
}
|
|
|
|
private void RedockEventsPane()
|
|
{
|
|
if (_eventsFloatWindow != null)
|
|
{
|
|
_eventsFloatWindow.Closing -= EventsFloatWindow_Closing;
|
|
_eventsFloatWindow.Closed -= EventsFloatWindow_Closed;
|
|
if (_eventsFloatWindow.Content == EventsPaneHost)
|
|
_eventsFloatWindow.Content = null;
|
|
_eventsFloatWindow.Close();
|
|
_eventsFloatWindow = null;
|
|
}
|
|
|
|
_eventsPaneFloating = false;
|
|
if (EventsDockBorder.Child == null)
|
|
EventsDockBorder.Child = EventsPaneHost;
|
|
_eventsPaneVisible = true;
|
|
_draggingEventsPaneHeader = false;
|
|
HideDockPreview();
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void UndockPerformancePane()
|
|
{
|
|
if (_performanceFloatWindow != null)
|
|
return;
|
|
|
|
if (PerformanceDockBorder.Child == PerformancePaneHost)
|
|
PerformanceDockBorder.Child = null;
|
|
|
|
_performancePaneFloating = true;
|
|
_performancePaneVisible = true;
|
|
ApplyDockVisibilityFromExplorer();
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
if (_performanceFloatWindow != null)
|
|
return;
|
|
|
|
_performanceFloatWindow = new PerformanceFloatWindow(
|
|
PerformancePaneHost) { Owner = this, ShowInTaskbar = false };
|
|
_performanceFloatWindow.Closing += PerformanceFloatWindow_Closing;
|
|
_performanceFloatWindow.Closed += PerformanceFloatWindow_Closed;
|
|
_performanceFloatWindow.Show();
|
|
}),
|
|
System.Windows.Threading.DispatcherPriority.Background);
|
|
}
|
|
|
|
private void RedockPerformancePane()
|
|
{
|
|
if (_performanceFloatWindow != null)
|
|
{
|
|
_performanceFloatWindow.Closing -= PerformanceFloatWindow_Closing;
|
|
_performanceFloatWindow.Closed -= PerformanceFloatWindow_Closed;
|
|
if (_performanceFloatWindow.Content == PerformancePaneHost)
|
|
_performanceFloatWindow.Content = null;
|
|
_performanceFloatWindow.Close();
|
|
_performanceFloatWindow = null;
|
|
}
|
|
|
|
_performancePaneFloating = false;
|
|
if (PerformanceDockBorder.Child == null)
|
|
PerformanceDockBorder.Child = PerformancePaneHost;
|
|
_performancePaneVisible = true;
|
|
_draggingPerformancePaneHeader = false;
|
|
HideDockPreview();
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void UndockEtwPane()
|
|
{
|
|
if (_etwFloatWindow != null)
|
|
return;
|
|
|
|
if (EtwDockBorder.Child == EtwPaneHost)
|
|
EtwDockBorder.Child = null;
|
|
|
|
_etwPaneFloating = true;
|
|
ApplyDockVisibilityFromExplorer();
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
if (_etwFloatWindow != null)
|
|
return;
|
|
|
|
_etwFloatWindow = new EtwFloatWindow(
|
|
EtwPaneHost) { Owner = this, ShowInTaskbar = false };
|
|
_etwFloatWindow.Closing += EtwFloatWindow_Closing;
|
|
_etwFloatWindow.Closed += EtwFloatWindow_Closed;
|
|
_etwFloatWindow.Show();
|
|
}),
|
|
System.Windows.Threading.DispatcherPriority.Background);
|
|
}
|
|
|
|
private void RedockEtwPane()
|
|
{
|
|
if (_etwFloatWindow != null)
|
|
{
|
|
_etwFloatWindow.Closing -= EtwFloatWindow_Closing;
|
|
_etwFloatWindow.Closed -= EtwFloatWindow_Closed;
|
|
if (_etwFloatWindow.Content == EtwPaneHost)
|
|
_etwFloatWindow.Content = null;
|
|
_etwFloatWindow.Close();
|
|
_etwFloatWindow = null;
|
|
}
|
|
|
|
_etwPaneFloating = false;
|
|
if (EtwDockBorder.Child == null)
|
|
EtwDockBorder.Child = EtwPaneHost;
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void UndockHeuristicsPane()
|
|
{
|
|
if (_heuristicsFloatWindow != null)
|
|
return;
|
|
|
|
if (HeuristicsDockBorder.Child == HeuristicsPaneHost)
|
|
HeuristicsDockBorder.Child = null;
|
|
|
|
_heuristicsPaneFloating = true;
|
|
ApplyDockVisibilityFromExplorer();
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
if (_heuristicsFloatWindow != null)
|
|
return;
|
|
|
|
_heuristicsFloatWindow = new HeuristicsFloatWindow(
|
|
HeuristicsPaneHost) { Owner = this, ShowInTaskbar = false };
|
|
_heuristicsFloatWindow.Closing += HeuristicsFloatWindow_Closing;
|
|
_heuristicsFloatWindow.Closed += HeuristicsFloatWindow_Closed;
|
|
_heuristicsFloatWindow.Show();
|
|
}),
|
|
System.Windows.Threading.DispatcherPriority.Background);
|
|
}
|
|
|
|
private void RedockHeuristicsPane()
|
|
{
|
|
if (_heuristicsFloatWindow != null)
|
|
{
|
|
_heuristicsFloatWindow.Closing -= HeuristicsFloatWindow_Closing;
|
|
_heuristicsFloatWindow.Closed -= HeuristicsFloatWindow_Closed;
|
|
if (_heuristicsFloatWindow.Content == HeuristicsPaneHost)
|
|
_heuristicsFloatWindow.Content = null;
|
|
_heuristicsFloatWindow.Close();
|
|
_heuristicsFloatWindow = null;
|
|
}
|
|
|
|
_heuristicsPaneFloating = false;
|
|
if (HeuristicsDockBorder.Child == null)
|
|
HeuristicsDockBorder.Child = HeuristicsPaneHost;
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void EventsFloatWindow_Closed(object? sender, EventArgs e)
|
|
{
|
|
_eventsFloatWindow = null;
|
|
if (EventsDockBorder.Child == null)
|
|
RedockEventsPane();
|
|
}
|
|
|
|
private void EventsFloatWindow_Closing(object? sender, CancelEventArgs e)
|
|
{
|
|
bool explorerEnabled = FindExplorerItem("Events")?.IsEnabled ?? true;
|
|
if (_isMainWindowShuttingDown || !explorerEnabled || !_eventsPaneVisible ||
|
|
sender is not EventsFloatWindow window || !ReferenceEquals(window.Content, EventsPaneHost))
|
|
{
|
|
return;
|
|
}
|
|
|
|
e.Cancel = true;
|
|
RedockEventsPane();
|
|
}
|
|
|
|
private void PerformanceFloatWindow_Closed(object? sender, EventArgs e)
|
|
{
|
|
_performanceFloatWindow = null;
|
|
if (PerformanceDockBorder.Child == null)
|
|
RedockPerformancePane();
|
|
}
|
|
|
|
private void PerformanceFloatWindow_Closing(object? sender, CancelEventArgs e)
|
|
{
|
|
bool explorerEnabled = FindExplorerItem("Performance")?.IsEnabled ?? true;
|
|
if (_isMainWindowShuttingDown || !explorerEnabled || !_performancePaneVisible ||
|
|
sender is not PerformanceFloatWindow window || !ReferenceEquals(window.Content, PerformancePaneHost))
|
|
{
|
|
return;
|
|
}
|
|
|
|
e.Cancel = true;
|
|
RedockPerformancePane();
|
|
}
|
|
|
|
private void EtwFloatWindow_Closed(object? sender, EventArgs e)
|
|
{
|
|
_etwFloatWindow = null;
|
|
if (EtwDockBorder.Child == null)
|
|
RedockEtwPane();
|
|
}
|
|
|
|
private void EtwFloatWindow_Closing(object? sender, CancelEventArgs e)
|
|
{
|
|
bool explorerEnabled = FindExplorerItem("ETW")?.IsEnabled ?? true;
|
|
if (_isMainWindowShuttingDown || !explorerEnabled || sender is not EtwFloatWindow window ||
|
|
!ReferenceEquals(window.Content, EtwPaneHost))
|
|
{
|
|
return;
|
|
}
|
|
|
|
e.Cancel = true;
|
|
RedockEtwPane();
|
|
}
|
|
|
|
private void HeuristicsFloatWindow_Closed(object? sender, EventArgs e)
|
|
{
|
|
_heuristicsFloatWindow = null;
|
|
if (HeuristicsDockBorder.Child == null)
|
|
RedockHeuristicsPane();
|
|
}
|
|
|
|
private void HeuristicsFloatWindow_Closing(object? sender, CancelEventArgs e)
|
|
{
|
|
bool explorerEnabled = FindExplorerItem("Heuristics")?.IsEnabled ?? true;
|
|
if (_isMainWindowShuttingDown || !explorerEnabled || sender is not HeuristicsFloatWindow window ||
|
|
!ReferenceEquals(window.Content, HeuristicsPaneHost))
|
|
{
|
|
return;
|
|
}
|
|
|
|
e.Cancel = true;
|
|
RedockHeuristicsPane();
|
|
}
|
|
|
|
private void ApiViewDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
UpdateApiViewSelection(ApiViewDataGrid?.SelectedItem as ApiCallGraphMainRowView);
|
|
RefreshApiGraphSelectionVisual();
|
|
}
|
|
|
|
private void ApiViewTextFilter_Changed(object sender, TextChangedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
RefreshApiViewPresentation();
|
|
}
|
|
|
|
private void ApiViewSelectionFilter_Changed(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
RefreshApiViewPresentation();
|
|
}
|
|
|
|
private void ApiViewMode_Changed(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
_apiViewPresentationMode = ApiViewModeBox?.SelectedIndex == 1 ? ApiViewPresentationMode.ThreadTimeline
|
|
: ApiViewPresentationMode.CallGraph;
|
|
RefreshApiViewPresentation();
|
|
}
|
|
|
|
private void ApiViewClearFilters_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_ = sender;
|
|
_ = e;
|
|
|
|
if (ApiFilterCallBox != null)
|
|
ApiFilterCallBox.Text = string.Empty;
|
|
if (ApiFilterActionBox != null)
|
|
ApiFilterActionBox.Text = string.Empty;
|
|
if (ApiFilterCallerBox != null)
|
|
ApiFilterCallerBox.Text = string.Empty;
|
|
if (ApiFilterTargetBox != null)
|
|
ApiFilterTargetBox.Text = string.Empty;
|
|
if (ApiFilterThreadBox != null)
|
|
ApiFilterThreadBox.Text = string.Empty;
|
|
if (ApiFilterRegionBox != null)
|
|
ApiFilterRegionBox.Text = string.Empty;
|
|
if (ApiFilterProtectBox != null)
|
|
ApiFilterProtectBox.Text = string.Empty;
|
|
if (ApiFilterMinHitsBox != null)
|
|
ApiFilterMinHitsBox.Text = string.Empty;
|
|
if (ApiFilterSensorBox != null)
|
|
ApiFilterSensorBox.SelectedIndex = 0;
|
|
if (ApiFilterOriginBox != null)
|
|
ApiFilterOriginBox.SelectedIndex = 0;
|
|
RefreshApiViewPresentation();
|
|
}
|
|
|
|
private void UpdateApiViewSelection(ApiCallGraphMainRowView? selected)
|
|
{
|
|
if (ApiViewSelectedTitleBlock == null || ApiViewSelectedMetaBlock == null ||
|
|
ApiViewSelectedActionValue == null || ApiViewSelectedSensorValue == null ||
|
|
ApiViewSelectedOriginValue == null || ApiViewSelectedFramesValue == null ||
|
|
ApiViewSelectedSourceValue == null || ApiViewSelectedTargetValue == null ||
|
|
ApiViewSelectedThreadValue == null || ApiViewSelectedHitsValue == null ||
|
|
ApiViewSelectedField2Label == null || ApiViewSelectedField4Label == null ||
|
|
ApiViewSelectedSizeValue == null || ApiViewSelectedProtectValue == null ||
|
|
ApiViewSelectedDetailValue == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (selected == null)
|
|
{
|
|
ApiViewSelectedTitleBlock.Text = "No selection";
|
|
ApiViewSelectedMetaBlock.Text = "Select a row to inspect decoded details";
|
|
ApiViewSelectedActionValue.Text = string.Empty;
|
|
ApiViewSelectedSensorValue.Text = string.Empty;
|
|
ApiViewSelectedOriginValue.Text = string.Empty;
|
|
ApiViewSelectedFramesValue.Text = string.Empty;
|
|
ApiViewSelectedSourceValue.Text = string.Empty;
|
|
ApiViewSelectedTargetValue.Text = string.Empty;
|
|
ApiViewSelectedThreadValue.Text = string.Empty;
|
|
ApiViewSelectedHitsValue.Text = string.Empty;
|
|
ApiViewSelectedField2Label.Text = "Context";
|
|
ApiViewSelectedField4Label.Text = "Flags";
|
|
ApiViewSelectedSizeValue.Text = string.Empty;
|
|
ApiViewSelectedProtectValue.Text = string.Empty;
|
|
ApiViewSelectedDetailValue.Text = string.Empty;
|
|
return;
|
|
}
|
|
|
|
ApiViewSelectedTitleBlock.Text = selected.ApiName;
|
|
ApiViewSelectedMetaBlock.Text =
|
|
$"Caller {selected.SourceLabel} | Target {selected.TargetLabel} | Thread {selected.ThreadLabel} | Hits {selected.Hits} | First Seen {selected.FirstSeen} | Last Seen {selected.LastSeen}" +
|
|
(string.IsNullOrWhiteSpace(selected.AbsoluteLastSeen) ? string.Empty
|
|
: $" ({selected.AbsoluteLastSeen})");
|
|
ApiViewSelectedActionValue.Text = selected.ActionLabel;
|
|
ApiViewSelectedSensorValue.Text = selected.SensorLabel;
|
|
ApiViewSelectedOriginValue.Text = selected.CallerOriginLabel;
|
|
ApiViewSelectedFramesValue.Text = selected.CallChainLabel;
|
|
ApiViewSelectedSourceValue.Text = selected.SourceLabel;
|
|
ApiViewSelectedTargetValue.Text = selected.TargetLabel;
|
|
ApiViewSelectedThreadValue.Text = selected.ThreadLabel;
|
|
ApiViewSelectedHitsValue.Text = selected.Hits.ToString();
|
|
ApiViewSelectedField2Label.Text = selected.Field2Label;
|
|
ApiViewSelectedField4Label.Text = selected.Field4Label;
|
|
ApiViewSelectedSizeValue.Text = selected.SizeLabel;
|
|
ApiViewSelectedProtectValue.Text = selected.ProtectLabel;
|
|
ApiViewSelectedDetailValue.Text = selected.DetailFull;
|
|
}
|
|
|
|
private void TogglePaneOrder()
|
|
{
|
|
_performanceOnTop = !_performanceOnTop;
|
|
ApplyPaneOrder();
|
|
}
|
|
|
|
private void ToggleIntelPaneOrder()
|
|
{
|
|
_heuristicsOnTop = !_heuristicsOnTop;
|
|
ApplyIntelPaneOrder();
|
|
}
|
|
|
|
private void ApplyPaneOrder()
|
|
{
|
|
if (_performanceOnTop)
|
|
{
|
|
Grid.SetRow(PerformancePaneRow, 0);
|
|
Grid.SetRow(EventsPaneRow, 2);
|
|
PerformancePaneRow.Margin = new Thickness(0, 0, 0, 3);
|
|
EventsPaneRow.Margin = new Thickness(0, 3, 0, 0);
|
|
}
|
|
else
|
|
{
|
|
Grid.SetRow(EventsPaneRow, 0);
|
|
Grid.SetRow(PerformancePaneRow, 2);
|
|
EventsPaneRow.Margin = new Thickness(0, 0, 0, 3);
|
|
PerformancePaneRow.Margin = new Thickness(0, 3, 0, 0);
|
|
}
|
|
}
|
|
|
|
private void ApplyIntelPaneOrder()
|
|
{
|
|
if (_heuristicsOnTop)
|
|
{
|
|
Grid.SetRow(HeuristicsDockBorder, 0);
|
|
Grid.SetRow(EtwDockBorder, 2);
|
|
HeuristicsDockBorder.Margin = new Thickness(0, 0, 0, 3);
|
|
EtwDockBorder.Margin = new Thickness(0, 3, 0, 0);
|
|
}
|
|
else
|
|
{
|
|
Grid.SetRow(EtwDockBorder, 0);
|
|
Grid.SetRow(HeuristicsDockBorder, 2);
|
|
EtwDockBorder.Margin = new Thickness(0, 0, 0, 3);
|
|
HeuristicsDockBorder.Margin = new Thickness(0, 3, 0, 0);
|
|
}
|
|
|
|
ApplyDockVisibilityFromExplorer();
|
|
}
|
|
|
|
private void BeginPaneHeaderDrag(bool isEventsPane, Point screenPosition)
|
|
{
|
|
if (isEventsPane)
|
|
{
|
|
if (_eventsFloatWindow == null)
|
|
ToggleFloatDock();
|
|
|
|
if (_eventsFloatWindow == null)
|
|
return;
|
|
|
|
_draggingEventsPaneHeader = true;
|
|
_draggingPerformancePaneHeader = false;
|
|
_floatingPaneDragOffset =
|
|
new Vector(screenPosition.X - _eventsFloatWindow.Left, screenPosition.Y - _eventsFloatWindow.Top);
|
|
}
|
|
else
|
|
{
|
|
if (_performanceFloatWindow == null)
|
|
TogglePerformanceFloatDock();
|
|
|
|
if (_performanceFloatWindow == null)
|
|
return;
|
|
|
|
_draggingPerformancePaneHeader = true;
|
|
_draggingEventsPaneHeader = false;
|
|
_floatingPaneDragOffset = new Vector(screenPosition.X - _performanceFloatWindow.Left,
|
|
screenPosition.Y - _performanceFloatWindow.Top);
|
|
}
|
|
|
|
ContinuePaneHeaderDrag(isEventsPane, screenPosition);
|
|
}
|
|
|
|
private void ContinuePaneHeaderDrag(bool isEventsPane, Point screenPosition)
|
|
{
|
|
if (isEventsPane && !_draggingEventsPaneHeader)
|
|
return;
|
|
if (!isEventsPane && !_draggingPerformancePaneHeader)
|
|
return;
|
|
|
|
var floatingWindow = isEventsPane ? (Window ?) _eventsFloatWindow: _performanceFloatWindow;
|
|
if (floatingWindow == null)
|
|
return;
|
|
|
|
floatingWindow.Left = screenPosition.X - _floatingPaneDragOffset.X;
|
|
floatingWindow.Top = screenPosition.Y - _floatingPaneDragOffset.Y;
|
|
UpdateDockPreview(screenPosition);
|
|
}
|
|
|
|
private void EndPaneHeaderDrag(bool isEventsPane, Point screenPosition)
|
|
{
|
|
if (isEventsPane)
|
|
_draggingEventsPaneHeader = false;
|
|
else
|
|
_draggingPerformancePaneHeader = false;
|
|
|
|
var slot = GetDockDropSlot(screenPosition);
|
|
HideDockPreview();
|
|
|
|
if (slot == DockDropSlot.None)
|
|
return;
|
|
|
|
if (isEventsPane)
|
|
{
|
|
_eventsFloatWindow?.Close();
|
|
_performanceOnTop = slot == DockDropSlot.Bottom;
|
|
}
|
|
else
|
|
{
|
|
_performanceFloatWindow?.Close();
|
|
_performanceOnTop = slot == DockDropSlot.Top;
|
|
}
|
|
|
|
ApplyPaneOrder();
|
|
}
|
|
|
|
private DockDropSlot GetDockDropSlot(Point screenPosition)
|
|
{
|
|
if (DockGrid.ActualWidth < 8 || DockGrid.ActualHeight < 8)
|
|
return DockDropSlot.None;
|
|
|
|
var dockTopLeft = DockGrid.PointToScreen(new Point(0, 0));
|
|
var dockRect = new Rect(dockTopLeft, new Size(DockGrid.ActualWidth, DockGrid.ActualHeight));
|
|
if (!dockRect.Contains(screenPosition))
|
|
return DockDropSlot.None;
|
|
|
|
double relativeY = screenPosition.Y - dockRect.Top;
|
|
return relativeY <= dockRect.Height * 0.5 ? DockDropSlot.Top : DockDropSlot.Bottom;
|
|
}
|
|
|
|
private void UpdateDockPreview(Point screenPosition)
|
|
{
|
|
var slot = GetDockDropSlot(screenPosition);
|
|
DockPreviewTop.Visibility = slot == DockDropSlot.Top ? Visibility.Visible : Visibility.Collapsed;
|
|
DockPreviewBottom.Visibility = slot == DockDropSlot.Bottom ? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
|
|
private void HideDockPreview()
|
|
{
|
|
DockPreviewTop.Visibility = Visibility.Collapsed;
|
|
DockPreviewBottom.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
private enum DockDropSlot
|
|
{
|
|
None,
|
|
Top,
|
|
Bottom
|
|
}
|
|
|
|
private readonly struct EventSelectionKey : IEquatable<EventSelectionKey>
|
|
{
|
|
public EventSelectionKey(TelemetryEvent ev)
|
|
{
|
|
TimestampUtc = ev.TimestampUtc;
|
|
Pid = ev.PID;
|
|
Tid = ev.TID;
|
|
Group = ev.Group ?? string.Empty;
|
|
SubType = ev.SubType ?? string.Empty;
|
|
Summary = ev.Summary ?? string.Empty;
|
|
Details = ev.Details ?? string.Empty;
|
|
}
|
|
|
|
private DateTime TimestampUtc { get; }
|
|
private int Pid { get; }
|
|
private int Tid { get; }
|
|
private string Group { get; }
|
|
private string SubType { get; }
|
|
private string Summary { get; }
|
|
private string Details { get; }
|
|
|
|
public bool Matches(TelemetryEvent ev)
|
|
{
|
|
return ev.TimestampUtc == TimestampUtc && ev.PID == Pid && ev.TID == Tid &&
|
|
string.Equals(ev.Group ?? string.Empty, Group, StringComparison.OrdinalIgnoreCase) &&
|
|
string.Equals(ev.SubType ?? string.Empty, SubType, StringComparison.OrdinalIgnoreCase) &&
|
|
string.Equals(ev.Summary ?? string.Empty, Summary, StringComparison.Ordinal) &&
|
|
string.Equals(ev.Details ?? string.Empty, Details, StringComparison.Ordinal);
|
|
}
|
|
|
|
public bool Equals(EventSelectionKey other)
|
|
{
|
|
return TimestampUtc == other.TimestampUtc && Pid == other.Pid && Tid == other.Tid &&
|
|
string.Equals(Group, other.Group, StringComparison.OrdinalIgnoreCase) &&
|
|
string.Equals(SubType, other.SubType, StringComparison.OrdinalIgnoreCase) &&
|
|
string.Equals(Summary, other.Summary, StringComparison.Ordinal) &&
|
|
string.Equals(Details, other.Details, StringComparison.Ordinal);
|
|
}
|
|
|
|
public override bool Equals(object? obj) => obj is EventSelectionKey other && Equals(other);
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
HashCode hash = new();
|
|
hash.Add(TimestampUtc);
|
|
hash.Add(Pid);
|
|
hash.Add(Tid);
|
|
hash.Add(Group, StringComparer.OrdinalIgnoreCase);
|
|
hash.Add(SubType, StringComparer.OrdinalIgnoreCase);
|
|
hash.Add(Summary, StringComparer.Ordinal);
|
|
hash.Add(Details, StringComparer.Ordinal);
|
|
return hash.ToHashCode();
|
|
}
|
|
}
|
|
|
|
private static TelemetryEvent CloneTelemetryEvent(TelemetryEvent src)
|
|
{
|
|
return new TelemetryEvent { TimestampUtc = src.TimestampUtc,
|
|
PID = src.PID,
|
|
TID = src.TID,
|
|
Group = src.Group,
|
|
SubType = src.SubType,
|
|
ProcessName = src.ProcessName,
|
|
Summary = src.Summary,
|
|
Details = src.Details };
|
|
}
|
|
|
|
private static PerformanceSample ClonePerformanceSample(PerformanceSample src)
|
|
{
|
|
return new PerformanceSample {
|
|
TimestampUtc = src.TimestampUtc,
|
|
CoreCount = src.CoreCount,
|
|
CpuPercent = src.CpuPercent,
|
|
CoresUsedPercent = src.CoresUsedPercent,
|
|
DiskReadBytesPerSec = src.DiskReadBytesPerSec,
|
|
DiskWriteBytesPerSec = src.DiskWriteBytesPerSec,
|
|
PrivateBytes = src.PrivateBytes,
|
|
ReservedBytes = src.ReservedBytes,
|
|
NetInBytesPerSec = src.NetInBytesPerSec,
|
|
NetOutBytesPerSec = src.NetOutBytesPerSec,
|
|
NetPacketsPerSec = src.NetPacketsPerSec,
|
|
TopThreads = src.TopThreads
|
|
.Select(t => new ThreadUsageSample { Tid = t.Tid, CpuMsDelta = t.CpuMsDelta,
|
|
State = t.State, WaitReason = t.WaitReason,
|
|
Kind = t.Kind, StartTimeUtc = t.StartTimeUtc,
|
|
TargetSuspended = t.TargetSuspended })
|
|
.ToList(),
|
|
CoreUsage = src.CoreUsage
|
|
.Select(c => new CoreUsageSample { CoreIndex = c.CoreIndex, BusyPercent = c.BusyPercent,
|
|
DominantTid = c.DominantTid,
|
|
DominantThreadKind = c.DominantThreadKind,
|
|
DominantThreadCpuMs = c.DominantThreadCpuMs,
|
|
ThreadCount = c.ThreadCount })
|
|
.ToList(),
|
|
MemoryMetrics = src.MemoryMetrics
|
|
.Select(m => new MemoryMetricSample { Metric = m.Metric, Value = m.Value,
|
|
BytesValue = m.BytesValue })
|
|
.ToList(),
|
|
MemoryPages = src.MemoryPages
|
|
.Select(m => new MemoryPageSample { BaseAddress = m.BaseAddress,
|
|
AllocationBase = m.AllocationBase,
|
|
RegionSize = m.RegionSize,
|
|
State = m.State,
|
|
Protect = m.Protect,
|
|
AllocationProtect = m.AllocationProtect,
|
|
Type = m.Type,
|
|
StateLabel = m.StateLabel,
|
|
ProtectLabel = m.ProtectLabel,
|
|
TypeLabel = m.TypeLabel,
|
|
Category = m.Category,
|
|
SpecialUse = m.SpecialUse,
|
|
BackingPath = m.BackingPath,
|
|
ModulePath = m.ModulePath,
|
|
Sr71Owned = m.Sr71Owned,
|
|
Sr71OwnerTag = m.Sr71OwnerTag,
|
|
WorkingSetValid = m.WorkingSetValid,
|
|
WorkingSetShared = m.WorkingSetShared,
|
|
WorkingSetShareCount = m.WorkingSetShareCount,
|
|
WorkingSetLocked = m.WorkingSetLocked,
|
|
WorkingSetLargePage = m.WorkingSetLargePage,
|
|
SnapshotOffset = m.SnapshotOffset,
|
|
SnapshotBytes = m.SnapshotBytes?.ToArray() })
|
|
.ToList()
|
|
};
|
|
}
|
|
|
|
private static ThreadLifecycleEventSample CloneThreadLifecycleEvent(ThreadLifecycleEventSample src)
|
|
{
|
|
return new ThreadLifecycleEventSample { TimestampUtc = src.TimestampUtc,
|
|
ProcessPid = src.ProcessPid,
|
|
ThreadId = src.ThreadId,
|
|
CreatorPid = src.CreatorPid,
|
|
Flags = src.Flags,
|
|
StartAddress = src.StartAddress,
|
|
ImageBase = src.ImageBase,
|
|
ImageSize = src.ImageSize,
|
|
EventKind = src.EventKind,
|
|
Notes = src.Notes };
|
|
}
|
|
|
|
private static MemoryRegionAttributionSample
|
|
CloneMemoryRegionAttributionSample(MemoryRegionAttributionSample src)
|
|
{
|
|
return new MemoryRegionAttributionSample { TimestampUtc = src.TimestampUtc,
|
|
ProcessStartKey = src.ProcessStartKey,
|
|
TargetPid = src.TargetPid,
|
|
ActorPid = src.ActorPid,
|
|
ActorTid = src.ActorTid,
|
|
AllocationBase = src.AllocationBase,
|
|
BaseAddress = src.BaseAddress,
|
|
RegionSize = src.RegionSize,
|
|
ApiName = src.ApiName,
|
|
EventKind = src.EventKind,
|
|
RegionKind = src.RegionKind,
|
|
RegionIdentity = src.RegionIdentity,
|
|
OriginPath = src.OriginPath,
|
|
SourceFamily = src.SourceFamily,
|
|
ExecutionContext = src.ExecutionContext,
|
|
CallerOrigin = src.CallerOrigin,
|
|
FirstUserFrame = src.FirstUserFrame,
|
|
FirstUserFrameModule = src.FirstUserFrameModule,
|
|
FrameSummary = src.FrameSummary,
|
|
UnwindClean = src.UnwindClean,
|
|
FrameChainHadGaps = src.FrameChainHadGaps,
|
|
ObservedByKernel = src.ObservedByKernel,
|
|
ObservedByUserHook = src.ObservedByUserHook,
|
|
BlackbirdOwned = src.BlackbirdOwned,
|
|
CrossProcess = src.CrossProcess,
|
|
ImageBacked = src.ImageBacked,
|
|
InitialProtection = src.InitialProtection,
|
|
CurrentProtection = src.CurrentProtection,
|
|
PreviousProtection = src.PreviousProtection,
|
|
FirstExecutableTransition = src.FirstExecutableTransition,
|
|
MapCount = src.MapCount,
|
|
WriteCount = src.WriteCount,
|
|
ProtectCount = src.ProtectCount,
|
|
ThreadStartCount = src.ThreadStartCount,
|
|
ProtectFlipCount = src.ProtectFlipCount,
|
|
RapidProtectFlipCount = src.RapidProtectFlipCount,
|
|
ExecutableFlipCount = src.ExecutableFlipCount,
|
|
GuardNoAccessFlipCount = src.GuardNoAccessFlipCount,
|
|
WritableExecutableFlipCount = src.WritableExecutableFlipCount,
|
|
ProtectionTransition = src.ProtectionTransition,
|
|
EntropyBits = src.EntropyBits,
|
|
MaxEntropyBits = src.MaxEntropyBits,
|
|
EntropyFlipCount = src.EntropyFlipCount,
|
|
RapidEntropyFlipCount = src.RapidEntropyFlipCount,
|
|
HighEntropyWriteCount = src.HighEntropyWriteCount,
|
|
SampleBytes = src.SampleBytes,
|
|
LifecycleSummary = src.LifecycleSummary,
|
|
ThreadStartObserved = src.ThreadStartObserved,
|
|
ThreadId = src.ThreadId,
|
|
ThreadStartAddress = src.ThreadStartAddress,
|
|
FunctionTableRegistered = src.FunctionTableRegistered,
|
|
FunctionTablePointer = src.FunctionTablePointer,
|
|
SignatureLevel = src.SignatureLevel,
|
|
SignatureType = src.SignatureType };
|
|
}
|
|
|
|
private sealed class ApiCallGraphMainRowView
|
|
{
|
|
public string GraphKey { get; set; } = string.Empty;
|
|
public string ThreadGroupKey { get; set; } = string.Empty;
|
|
public string ViewModeKey { get; set; } = "call";
|
|
public string ApiName { get; set; } = string.Empty;
|
|
public string OriginModule { get; set; } = string.Empty;
|
|
public string ActionLabel { get; set; } = string.Empty;
|
|
public string SensorLabel { get; set; } = string.Empty;
|
|
public string CallerOriginKey { get; set; } = string.Empty;
|
|
public string CallerOriginLabel { get; set; } = string.Empty;
|
|
public string CallChainLabel { get; set; } = string.Empty;
|
|
public Brush? CallerOriginBackground { get; set; }
|
|
public Brush? CallerOriginForeground { get; set; }
|
|
public Brush? SensorBackground { get; set; }
|
|
public Brush? SensorForeground { get; set; }
|
|
public Brush? HeatTrackBackground { get; set; }
|
|
public Brush? HeatFillBackground { get; set; }
|
|
public Brush? RowBackground { get; set; }
|
|
public Brush? RowBorderBrush { get; set; }
|
|
public string SourceLabel { get; set; } = string.Empty;
|
|
public string TargetLabel { get; set; } = string.Empty;
|
|
public string ThreadLabel { get; set; } = string.Empty;
|
|
public string Field1Label { get; set; } = "Base";
|
|
public string Field2Label { get; set; } = "Size";
|
|
public string Field3Label { get; set; } = "Alloc Type";
|
|
public string Field4Label { get; set; } = "Protect";
|
|
public string BaseLabel { get; set; } = string.Empty;
|
|
public string SizeLabel { get; set; } = string.Empty;
|
|
public string AllocTypeLabel { get; set; } = string.Empty;
|
|
public string ProtectLabel { get; set; } = string.Empty;
|
|
public string LastSeen { get; set; } = string.Empty;
|
|
public string AbsoluteLastSeen { get; set; } = string.Empty;
|
|
public string FirstSeen { get; set; } = string.Empty;
|
|
public string AbsoluteFirstSeen { get; set; } = string.Empty;
|
|
public DateTime FirstSeenUtc { get; set; }
|
|
public DateTime LastSeenUtc { get; set; }
|
|
public int Hits { get; set; }
|
|
public double HeatPercent { get; set; }
|
|
public double ActivityFillWidth { get; set; }
|
|
public string DetailFull { get; set; } = string.Empty;
|
|
}
|
|
|
|
public void SetLaneFocus(string? laneKey)
|
|
{
|
|
_laneFocusKey = string.IsNullOrWhiteSpace(laneKey) ? null : laneKey;
|
|
FocusViewport();
|
|
}
|
|
|
|
public string? GetLaneFocus() => _laneFocusKey;
|
|
}
|
|
|
|
public sealed class PausedTimelineSpan
|
|
{
|
|
public DateTime StartUtc { get; set; }
|
|
public DateTime EndUtc { get; set; }
|
|
}
|
|
|
|
public sealed class ProcessSessionTab : INotifyPropertyChanged
|
|
{
|
|
private int _pid;
|
|
private string _title = "";
|
|
public List<TelemetryEvent> Events { get; } = new();
|
|
public List<PerformanceSample> PerformanceHistory { get; } = new();
|
|
public List<MemoryRegionAttributionSample> MemoryRegionAttributionHistory { get; } = new();
|
|
public List<ThreadLifecycleEventSample> ThreadLifecycleHistory { get; } = new();
|
|
public List<ThreadStackHistoryArchiveEntry> ThreadStackHistories { get; } = new();
|
|
internal HashSet<string> ChildProcessExpandedKeys { get; } = new(StringComparer.Ordinal);
|
|
public DateTime CaptureStartUtc { get; set; } = DateTime.UtcNow;
|
|
public double ViewDurationSeconds { get; set; } = 120;
|
|
public double ViewStartSeconds { get; set; }
|
|
public string? LaneFocusKey { get; set; }
|
|
public bool UseUsermodeHooks { get; set; }
|
|
public bool KernelHooksEnabled { get; set; } = true;
|
|
public bool SignatureIntelEnabled { get; set; } = true;
|
|
public bool SignatureIntelMemoryScanEnabled { get; set; }
|
|
public bool SignatureIntelPageScanEnabled { get; set; }
|
|
public bool AutoOpenApiGraphOnNextStart { get; set; }
|
|
public bool LaunchStartsSuspendedPending { get; set; }
|
|
public bool DeferredLaunchGateResumePending { get; set; }
|
|
public bool LaunchOwnedByInterface { get; set; }
|
|
public LaunchTargetKind AnalysisSubjectKind { get; set; } = LaunchTargetKind.Executable;
|
|
public string AnalysisSubjectPath { get; set; } = string.Empty;
|
|
public string AnalysisHostPath { get; set; } = string.Empty;
|
|
public bool TargetExited { get; set; }
|
|
public string TargetExitReason { get; set; } = string.Empty;
|
|
public bool OfflineSnapshot { get; set; }
|
|
public string? BackingStorePath { get; set; }
|
|
public List<PausedTimelineSpan> PausedTimelineSpans { get; } = new();
|
|
public DateTime? ActivePauseStartUtc { get; set; }
|
|
|
|
public int Pid
|
|
{
|
|
get => _pid;
|
|
set {
|
|
if (_pid == value)
|
|
return;
|
|
_pid = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public string Title
|
|
{
|
|
get => _title;
|
|
set {
|
|
if (_title == value)
|
|
return;
|
|
_title = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
private void OnPropertyChanged([CallerMemberName] string? prop = null) =>
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
|
|
}
|
|
}
|