mirror of
https://github.com/8damon/Blackbird-Platform
synced 2026-06-21 13:41:12 +00:00
1083 lines
50 KiB
C#
1083 lines
50 KiB
C#
using BlackbirdInterface.Capture;
|
|
using Microsoft.Win32;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Input;
|
|
|
|
namespace BlackbirdInterface
|
|
{
|
|
public partial class MainWindow
|
|
{
|
|
private static readonly TimeSpan SessionSpillInterval = TimeSpan.FromSeconds(15);
|
|
private const string CaptureArchiveExtension = ".bkcap";
|
|
private const string CaptureArchiveSaveFilter =
|
|
"Blackbird Capture Archive (*.bkcap)|*.bkcap|All files (*.*)|*.*";
|
|
private const string CaptureArchiveOpenFilter =
|
|
"Blackbird Capture Archive (*.bkcap)|*.bkcap|Legacy Blackbird Session Archive (*.swlkr;*.blackbird)|*.swlkr;*.blackbird|All files (*.*)|*.*";
|
|
private const int LiveGroupedDetailSpillThreshold = 24_000;
|
|
private const int LiveThreadStackSpillThreshold = 512;
|
|
private const int MaxLiveThreadStackSnapshotsPerThread = 256;
|
|
private const int MaxLiveThreadStackHistoriesPerTab = 512;
|
|
private readonly string _sessionCacheDirectory = Path.Combine(Path.GetTempPath(), "Blackbird", "session-cache");
|
|
private readonly HashSet<string> _ownedTemporaryWorkspaceRoots = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private string? _sessionFilePath;
|
|
|
|
private void EnsureSessionCacheDirectory()
|
|
{
|
|
Directory.CreateDirectory(_sessionCacheDirectory);
|
|
}
|
|
|
|
private string AllocateSessionCachePath(int pid)
|
|
{
|
|
EnsureSessionCacheDirectory();
|
|
return Path.Combine(_sessionCacheDirectory, $"pid-{pid}-{Guid.NewGuid():N}");
|
|
}
|
|
|
|
private bool IsSessionCachePath(string? path)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
string cacheRoot = Path.GetFullPath(_sessionCacheDirectory)
|
|
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
string candidate =
|
|
Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
|
|
return candidate.StartsWith(cacheRoot + Path.DirectorySeparatorChar,
|
|
StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(candidate, cacheRoot, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private SessionFileArchive CreateSingleTabArchive(SessionFileTab snapshot)
|
|
{
|
|
return new SessionFileArchive { Version = SessionFileStorage.CurrentVersion, SavedUtc = DateTime.UtcNow,
|
|
ActivePid = snapshot.Pid, Tabs = new List<SessionFileTab> { snapshot } };
|
|
}
|
|
|
|
private void RegisterTemporaryWorkspace(CaptureLoadedWorkspace workspace)
|
|
{
|
|
if (!workspace.IsTemporaryWorkspace || string.IsNullOrWhiteSpace(workspace.WorkspaceRootPath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_ownedTemporaryWorkspaceRoots.Add(Path.GetFullPath(workspace.WorkspaceRootPath));
|
|
}
|
|
|
|
private void ReleaseOwnedTemporaryWorkspaces()
|
|
{
|
|
foreach (string workspaceRoot in _ownedTemporaryWorkspaceRoots.ToArray())
|
|
{
|
|
try
|
|
{
|
|
SessionFileStorage.DeletePath(workspaceRoot);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
_ownedTemporaryWorkspaceRoots.Clear();
|
|
}
|
|
|
|
private SessionFileTab BuildTabSnapshot(ProcessSessionTab tab, bool preferExistingCaptureStore = true)
|
|
{
|
|
bool hasPersistedSnapshot = TryLoadTabSnapshot(tab, out SessionFileTab? persistedSnapshot) &&
|
|
persistedSnapshot != null;
|
|
bool hasInlineData = HasInlineSessionData(tab);
|
|
if (!hasInlineData && hasPersistedSnapshot)
|
|
{
|
|
persistedSnapshot ??= new SessionFileTab();
|
|
persistedSnapshot.Title = NormalizeSessionTitle(tab.Title);
|
|
persistedSnapshot.CaptureStartUtc = tab.CaptureStartUtc;
|
|
persistedSnapshot.ViewDurationSeconds = tab.ViewDurationSeconds;
|
|
persistedSnapshot.ViewStartSeconds = tab.ViewStartSeconds;
|
|
persistedSnapshot.LaneFocusKey = tab.LaneFocusKey;
|
|
persistedSnapshot.UseUsermodeHooks = tab.UseUsermodeHooks;
|
|
persistedSnapshot.TargetExited = tab.TargetExited;
|
|
persistedSnapshot.TargetExitReason = tab.TargetExitReason;
|
|
persistedSnapshot.OfflineSnapshot = tab.OfflineSnapshot;
|
|
persistedSnapshot.AnalysisSubjectKind = tab.AnalysisSubjectKind;
|
|
persistedSnapshot.AnalysisSubjectPath = tab.AnalysisSubjectPath;
|
|
persistedSnapshot.AnalysisHostPath = tab.AnalysisHostPath;
|
|
persistedSnapshot.CaptureStorePath = preferExistingCaptureStore ? tab.BackingStorePath : null;
|
|
return persistedSnapshot;
|
|
}
|
|
|
|
EnsureSessionMaterialized(tab);
|
|
|
|
List<TelemetryEvent> events = EnumerateSessionEvents(tab).Select(CloneTelemetryEvent).ToList();
|
|
List<PerformanceSample> performanceHistory =
|
|
tab.PerformanceHistory.Count > 0
|
|
? tab.PerformanceHistory.Select(ClonePerformanceSample).ToList()
|
|
: (persistedSnapshot?.PerformanceHistory.Select(ClonePerformanceSample).ToList() ??
|
|
new List<PerformanceSample>());
|
|
List<MemoryRegionAttributionSample> memoryRegionAttributionHistory =
|
|
tab.MemoryRegionAttributionHistory.Count > 0
|
|
? tab.MemoryRegionAttributionHistory.Select(CloneMemoryRegionAttributionSample).ToList()
|
|
: (persistedSnapshot?.MemoryRegionAttributionHistory.Select(CloneMemoryRegionAttributionSample)
|
|
.ToList() ??
|
|
new List<MemoryRegionAttributionSample>());
|
|
List<ThreadLifecycleEventSample> threadLifecycleHistory =
|
|
tab.ThreadLifecycleHistory.Count > 0
|
|
? tab.ThreadLifecycleHistory.Select(CloneThreadLifecycleEvent).ToList()
|
|
: (persistedSnapshot?.ThreadLifecycleHistory.Select(CloneThreadLifecycleEvent).ToList() ??
|
|
new List<ThreadLifecycleEventSample>());
|
|
List<GroupedEventRow> etw =
|
|
_etwHistoryByPid.TryGetValue(tab.Pid, out var etwRows)
|
|
? etwRows.Select(x => x.Clone()).ToList()
|
|
: (persistedSnapshot?.EtwGroups.Select(x => x.Clone()).ToList() ?? new List<GroupedEventRow>());
|
|
List<GroupedEventRow> heuristics =
|
|
_heuristicsHistoryByPid.TryGetValue(tab.Pid, out var heurRows)
|
|
? heurRows.Select(x => x.Clone()).ToList()
|
|
: (persistedSnapshot?.HeuristicsGroups.Select(x => x.Clone()).ToList() ??
|
|
new List<GroupedEventRow>());
|
|
List<GroupedEventRow> filesystem =
|
|
_filesystemHistoryByPid.TryGetValue(tab.Pid, out var fsRows)
|
|
? fsRows.Select(x => x.Clone()).ToList()
|
|
: (persistedSnapshot?.FilesystemGroups.Select(x => x.Clone()).ToList() ??
|
|
new List<GroupedEventRow>());
|
|
List<GroupedEventRow> registry = _registryHistoryByPid.TryGetValue(tab.Pid, out var regRows)
|
|
? regRows.Select(x => x.Clone()).ToList()
|
|
: (persistedSnapshot?.RegistryGroups.Select(x => x.Clone()).ToList() ??
|
|
new List<GroupedEventRow>());
|
|
List<GroupedEventRow> relations =
|
|
_relationsHistoryByPid.TryGetValue(tab.Pid, out var relRows)
|
|
? relRows.Select(x => x.Clone()).ToList()
|
|
: (persistedSnapshot?.ProcessRelationsGroups.Select(x => x.Clone()).ToList() ??
|
|
new List<GroupedEventRow>());
|
|
List<ApiCallGraphRowSnapshot> apiGraph =
|
|
_apiGraphHistoryByPid.TryGetValue(tab.Pid, out var apiRows)
|
|
? apiRows.Select(x => x.Clone()).ToList()
|
|
: (persistedSnapshot?.ApiGraphRows.Select(x => x.Clone()).ToList() ??
|
|
new List<ApiCallGraphRowSnapshot>());
|
|
List<ExtendedActivityRowSnapshot> extended =
|
|
_extendedHistoryByPid.TryGetValue(tab.Pid, out var extRows)
|
|
? extRows.Select(x => x.Clone()).ToList()
|
|
: (persistedSnapshot?.ExtendedActivityRows.Select(x => x.Clone()).ToList() ??
|
|
new List<ExtendedActivityRowSnapshot>());
|
|
List<ThreadStackHistoryArchiveEntry> threadStacks =
|
|
tab.ThreadStackHistories.Count > 0
|
|
? tab.ThreadStackHistories.Select(x => x.Clone()).ToList()
|
|
: (persistedSnapshot?.ThreadStackHistories.Select(x => x.Clone()).ToList() ??
|
|
new List<ThreadStackHistoryArchiveEntry>());
|
|
|
|
var snapshot =
|
|
new SessionFileTab { Pid = tab.Pid,
|
|
Title = NormalizeSessionTitle(tab.Title),
|
|
CaptureStartUtc = tab.CaptureStartUtc,
|
|
ViewDurationSeconds = tab.ViewDurationSeconds,
|
|
ViewStartSeconds = tab.ViewStartSeconds,
|
|
LaneFocusKey = tab.LaneFocusKey,
|
|
UseUsermodeHooks = tab.UseUsermodeHooks,
|
|
KernelHooksEnabled = tab.KernelHooksEnabled,
|
|
SignatureIntelEnabled = tab.SignatureIntelEnabled,
|
|
SignatureIntelMemoryScanEnabled = tab.SignatureIntelMemoryScanEnabled,
|
|
SignatureIntelPageScanEnabled = tab.SignatureIntelPageScanEnabled,
|
|
TargetExited = tab.TargetExited,
|
|
TargetExitReason = tab.TargetExitReason,
|
|
OfflineSnapshot = tab.OfflineSnapshot,
|
|
AnalysisSubjectKind = tab.AnalysisSubjectKind,
|
|
AnalysisSubjectPath = tab.AnalysisSubjectPath,
|
|
AnalysisHostPath = tab.AnalysisHostPath,
|
|
CaptureStorePath = preferExistingCaptureStore ? tab.BackingStorePath : null,
|
|
Events = events,
|
|
PerformanceHistory = performanceHistory,
|
|
MemoryRegionAttributionHistory = memoryRegionAttributionHistory,
|
|
ThreadLifecycleHistory = threadLifecycleHistory,
|
|
EtwGroups = etw,
|
|
HeuristicsGroups = heuristics,
|
|
FilesystemGroups = filesystem,
|
|
RegistryGroups = registry,
|
|
ProcessRelationsGroups = relations,
|
|
ApiGraphRows = apiGraph,
|
|
ExtendedActivityRows = extended,
|
|
ThreadStackHistories = threadStacks };
|
|
if (_captureProjection != null && tab.Pid == _filterRootPid)
|
|
{
|
|
_captureProjection.WaitForPendingStackCaptures(TimeSpan.FromMilliseconds(250));
|
|
_captureProjection.ApplyToTab(snapshot);
|
|
}
|
|
|
|
return snapshot;
|
|
}
|
|
|
|
private void SaveTabToBackingStore(ProcessSessionTab tab)
|
|
{
|
|
if (tab.Pid <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
SessionFileTab snapshot = BuildTabSnapshot(tab);
|
|
string path = tab.BackingStorePath ?? AllocateSessionCachePath(tab.Pid);
|
|
tab.BackingStorePath = path;
|
|
snapshot.CaptureStorePath = path;
|
|
|
|
SessionFileArchive archive = CreateSingleTabArchive(snapshot);
|
|
SessionFileStorage.SaveArchive(path, archive);
|
|
|
|
tab.Events.Clear();
|
|
tab.PerformanceHistory.Clear();
|
|
tab.MemoryRegionAttributionHistory.Clear();
|
|
tab.ThreadLifecycleHistory.Clear();
|
|
tab.ThreadStackHistories.Clear();
|
|
_etwHistoryByPid.Remove(tab.Pid);
|
|
_heuristicsHistoryByPid.Remove(tab.Pid);
|
|
_filesystemHistoryByPid.Remove(tab.Pid);
|
|
_registryHistoryByPid.Remove(tab.Pid);
|
|
_relationsHistoryByPid.Remove(tab.Pid);
|
|
_apiGraphHistoryByPid.Remove(tab.Pid);
|
|
_extendedHistoryByPid.Remove(tab.Pid);
|
|
}
|
|
|
|
private void EnsureSessionMaterialized(ProcessSessionTab tab)
|
|
{
|
|
bool hasInlineData =
|
|
tab.Events.Count > 0 || tab.PerformanceHistory.Count > 0 ||
|
|
tab.MemoryRegionAttributionHistory.Count > 0 || tab.ThreadLifecycleHistory.Count > 0 ||
|
|
_etwHistoryByPid.ContainsKey(tab.Pid) || _heuristicsHistoryByPid.ContainsKey(tab.Pid) ||
|
|
_filesystemHistoryByPid.ContainsKey(tab.Pid) || _registryHistoryByPid.ContainsKey(tab.Pid) ||
|
|
_relationsHistoryByPid.ContainsKey(tab.Pid) || _apiGraphHistoryByPid.ContainsKey(tab.Pid) ||
|
|
_extendedHistoryByPid.ContainsKey(tab.Pid);
|
|
if (hasInlineData)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!SessionFileStorage.Exists(tab.BackingStorePath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
SessionFileArchive archive = SessionFileStorage.LoadArchive(tab.BackingStorePath!);
|
|
SessionFileTab? snapshot =
|
|
archive.Tabs.FirstOrDefault(x => x.Pid == tab.Pid) ?? archive.Tabs.FirstOrDefault();
|
|
if (snapshot == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
tab.CaptureStartUtc = snapshot.CaptureStartUtc;
|
|
tab.ViewDurationSeconds = snapshot.ViewDurationSeconds;
|
|
tab.ViewStartSeconds = snapshot.ViewStartSeconds;
|
|
tab.LaneFocusKey = snapshot.LaneFocusKey;
|
|
tab.UseUsermodeHooks = snapshot.UseUsermodeHooks;
|
|
tab.KernelHooksEnabled = snapshot.KernelHooksEnabled;
|
|
tab.SignatureIntelEnabled = snapshot.SignatureIntelEnabled;
|
|
tab.SignatureIntelMemoryScanEnabled = snapshot.SignatureIntelMemoryScanEnabled;
|
|
tab.SignatureIntelPageScanEnabled = snapshot.SignatureIntelPageScanEnabled;
|
|
tab.TargetExited = snapshot.TargetExited;
|
|
tab.TargetExitReason = snapshot.TargetExitReason;
|
|
tab.OfflineSnapshot = snapshot.OfflineSnapshot;
|
|
tab.AnalysisSubjectKind = snapshot.AnalysisSubjectKind;
|
|
tab.AnalysisSubjectPath = snapshot.AnalysisSubjectPath;
|
|
tab.AnalysisHostPath = snapshot.AnalysisHostPath;
|
|
tab.BackingStorePath = snapshot.CaptureStorePath ?? tab.BackingStorePath;
|
|
|
|
tab.Events.Clear();
|
|
tab.Events.AddRange(snapshot.Events);
|
|
|
|
tab.PerformanceHistory.Clear();
|
|
tab.PerformanceHistory.AddRange(snapshot.PerformanceHistory.Select(ClonePerformanceSample));
|
|
tab.MemoryRegionAttributionHistory.Clear();
|
|
tab.MemoryRegionAttributionHistory.AddRange(
|
|
snapshot.MemoryRegionAttributionHistory.Select(CloneMemoryRegionAttributionSample));
|
|
tab.ThreadLifecycleHistory.Clear();
|
|
tab.ThreadLifecycleHistory.AddRange(snapshot.ThreadLifecycleHistory.Select(CloneThreadLifecycleEvent));
|
|
tab.ThreadStackHistories.Clear();
|
|
tab.ThreadStackHistories.AddRange(snapshot.ThreadStackHistories.Select(x => x.Clone()));
|
|
|
|
_etwHistoryByPid[tab.Pid] = snapshot.EtwGroups.Select(x => x.Clone()).ToList();
|
|
_heuristicsHistoryByPid[tab.Pid] = snapshot.HeuristicsGroups.Select(x => x.Clone()).ToList();
|
|
_filesystemHistoryByPid[tab.Pid] = snapshot.FilesystemGroups.Select(x => x.Clone()).ToList();
|
|
_registryHistoryByPid[tab.Pid] = snapshot.RegistryGroups.Select(x => x.Clone()).ToList();
|
|
_relationsHistoryByPid[tab.Pid] = snapshot.ProcessRelationsGroups.Select(x => x.Clone()).ToList();
|
|
_apiGraphHistoryByPid[tab.Pid] = snapshot.ApiGraphRows.Select(x => x.Clone()).ToList();
|
|
_extendedHistoryByPid[tab.Pid] = snapshot.ExtendedActivityRows.Select(x => x.Clone()).ToList();
|
|
}
|
|
|
|
private bool TryLoadTabSnapshot(ProcessSessionTab tab, out SessionFileTab? snapshot)
|
|
{
|
|
snapshot = null;
|
|
if (tab.Pid <= 0 || !SessionFileStorage.Exists(tab.BackingStorePath))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
SessionFileArchive archive = SessionFileStorage.LoadArchive(tab.BackingStorePath!);
|
|
snapshot = archive.Tabs.FirstOrDefault(x => x.Pid == tab.Pid) ?? archive.Tabs.FirstOrDefault();
|
|
return snapshot != null;
|
|
}
|
|
|
|
private bool HasInlineSessionData(ProcessSessionTab tab)
|
|
{
|
|
return (ReferenceEquals(tab, _currentSession) && _allEvents.Count > 0) || tab.Events.Count > 0 ||
|
|
tab.PerformanceHistory.Count > 0 || tab.MemoryRegionAttributionHistory.Count > 0 ||
|
|
tab.ThreadLifecycleHistory.Count > 0 || tab.ThreadStackHistories.Count > 0 ||
|
|
_etwHistoryByPid.ContainsKey(tab.Pid) || _heuristicsHistoryByPid.ContainsKey(tab.Pid) ||
|
|
_filesystemHistoryByPid.ContainsKey(tab.Pid) || _registryHistoryByPid.ContainsKey(tab.Pid) ||
|
|
_relationsHistoryByPid.ContainsKey(tab.Pid) || _apiGraphHistoryByPid.ContainsKey(tab.Pid) ||
|
|
_extendedHistoryByPid.ContainsKey(tab.Pid);
|
|
}
|
|
|
|
private IEnumerable<TelemetryEvent> EnumerateSessionEvents(ProcessSessionTab tab)
|
|
{
|
|
if (ReferenceEquals(tab, _currentSession))
|
|
{
|
|
return _allEvents;
|
|
}
|
|
|
|
return tab.Events;
|
|
}
|
|
|
|
private bool TryGetIntelDetailsFromBackingStore(int pid, IntelDetailsCategory category,
|
|
out IReadOnlyList<GroupedEventDetailRow> details)
|
|
{
|
|
details = Array.Empty<GroupedEventDetailRow>();
|
|
if (pid <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ProcessSessionTab? tab = _processTabs.FirstOrDefault(x => x.Pid == pid);
|
|
if (tab == null || !SessionFileStorage.Exists(tab.BackingStorePath))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
SessionFileArchive archive = SessionFileStorage.LoadArchive(tab.BackingStorePath!);
|
|
SessionFileTab? snapshot = archive.Tabs.FirstOrDefault(x => x.Pid == pid) ?? archive.Tabs.FirstOrDefault();
|
|
if (snapshot == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
IEnumerable<GroupedEventRow> groups =
|
|
category switch { IntelDetailsCategory.Etw => snapshot.EtwGroups,
|
|
IntelDetailsCategory.Heuristics => snapshot.HeuristicsGroups,
|
|
IntelDetailsCategory.Filesystem => snapshot.FilesystemGroups,
|
|
IntelDetailsCategory.ProcessRelations => snapshot.ProcessRelationsGroups,
|
|
_ => Enumerable.Empty<GroupedEventRow>() };
|
|
|
|
details = FlattenGroupedDetails(groups);
|
|
return details.Count > 0;
|
|
}
|
|
|
|
private SessionFileArchive BuildWorkspaceArchive()
|
|
{
|
|
_liveCaptureStore?.Flush();
|
|
SyncCurrentSessionStateToMemory();
|
|
|
|
var archive = new SessionFileArchive { Version = SessionFileStorage.CurrentVersion,
|
|
SavedUtc = DateTime.UtcNow, ActivePid = _currentSession?.Pid ?? 0 };
|
|
|
|
foreach (ProcessSessionTab tab in _processTabs)
|
|
{
|
|
bool reuseExistingCaptureStore =
|
|
!ReferenceEquals(tab, _currentSession) || tab.OfflineSnapshot || tab.TargetExited;
|
|
archive.Tabs.Add(BuildTabSnapshot(tab, reuseExistingCaptureStore));
|
|
}
|
|
|
|
return archive;
|
|
}
|
|
|
|
private async Task<SessionFileArchive> BuildWorkspaceArchiveAsync(string statusText)
|
|
{
|
|
Cursor? previousCursor = Mouse.OverrideCursor;
|
|
string previousStatus = StatusBlock.Text;
|
|
try
|
|
{
|
|
Mouse.OverrideCursor = Cursors.Wait;
|
|
StatusBlock.Text = statusText;
|
|
await Task.Yield();
|
|
|
|
_liveCaptureStore?.Flush();
|
|
await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Background);
|
|
|
|
SyncCurrentSessionStateToMemory();
|
|
await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Background);
|
|
|
|
var archive =
|
|
new SessionFileArchive { Version = SessionFileStorage.CurrentVersion, SavedUtc = DateTime.UtcNow,
|
|
ActivePid = _currentSession?.Pid ?? 0 };
|
|
|
|
List<ProcessSessionTab> tabs = _processTabs.ToList();
|
|
foreach (ProcessSessionTab tab in tabs)
|
|
{
|
|
bool reuseExistingCaptureStore =
|
|
!ReferenceEquals(tab, _currentSession) || tab.OfflineSnapshot || tab.TargetExited;
|
|
archive.Tabs.Add(BuildTabSnapshot(tab, reuseExistingCaptureStore));
|
|
await System.Windows.Threading.Dispatcher.Yield(
|
|
System.Windows.Threading.DispatcherPriority.Background);
|
|
}
|
|
|
|
return archive;
|
|
}
|
|
finally
|
|
{
|
|
Mouse.OverrideCursor = previousCursor;
|
|
if (string.Equals(StatusBlock.Text, statusText, StringComparison.Ordinal))
|
|
{
|
|
StatusBlock.Text = previousStatus;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task ApplyWorkspaceArchiveAsync(CaptureLoadedWorkspace workspace, bool merge)
|
|
{
|
|
SessionFileArchive archive = workspace.Archive;
|
|
if (archive.Tabs.Count == 0)
|
|
{
|
|
throw new InvalidDataException("Session archive does not contain any tabs.");
|
|
}
|
|
|
|
if (!merge)
|
|
{
|
|
SaveCurrentSessionState();
|
|
StopTargetExitWatcher();
|
|
StopBackendSession();
|
|
_perf?.Stop();
|
|
_samplerPid = 0;
|
|
|
|
_suppressTabSelectionChange = true;
|
|
_processTabs.Clear();
|
|
ProcessTabs.SelectedItem = null;
|
|
_suppressTabSelectionChange = false;
|
|
|
|
_etwHistoryByPid.Clear();
|
|
_heuristicsHistoryByPid.Clear();
|
|
_filesystemHistoryByPid.Clear();
|
|
_relationsHistoryByPid.Clear();
|
|
_apiGraphHistoryByPid.Clear();
|
|
_currentSession = null;
|
|
ReleaseOwnedTemporaryWorkspaces();
|
|
}
|
|
|
|
await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Background);
|
|
|
|
foreach (SessionFileTab incoming in archive.Tabs)
|
|
{
|
|
if (incoming.Pid <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ProcessSessionTab tab = _processTabs.FirstOrDefault(x => x.Pid == incoming.Pid) ??
|
|
AddOrSelectProcessTab(incoming.Pid, incoming.Title, select: false);
|
|
|
|
tab.Title = NormalizeSessionTitle(string.IsNullOrWhiteSpace(incoming.Title) ? $"PID {incoming.Pid}"
|
|
: incoming.Title);
|
|
tab.CaptureStartUtc = incoming.CaptureStartUtc;
|
|
tab.ViewDurationSeconds = incoming.ViewDurationSeconds;
|
|
tab.ViewStartSeconds = incoming.ViewStartSeconds;
|
|
tab.LaneFocusKey = incoming.LaneFocusKey;
|
|
tab.UseUsermodeHooks = incoming.UseUsermodeHooks;
|
|
tab.KernelHooksEnabled = incoming.KernelHooksEnabled;
|
|
tab.SignatureIntelEnabled = incoming.SignatureIntelEnabled;
|
|
tab.SignatureIntelMemoryScanEnabled = incoming.SignatureIntelMemoryScanEnabled;
|
|
tab.SignatureIntelPageScanEnabled = incoming.SignatureIntelPageScanEnabled;
|
|
tab.TargetExited = incoming.TargetExited;
|
|
tab.TargetExitReason = incoming.TargetExitReason;
|
|
tab.OfflineSnapshot = true;
|
|
tab.AnalysisSubjectKind = incoming.AnalysisSubjectKind;
|
|
tab.AnalysisSubjectPath = incoming.AnalysisSubjectPath;
|
|
tab.AnalysisHostPath = incoming.AnalysisHostPath;
|
|
|
|
bool usingBackingStore = false;
|
|
if (workspace.TabPaths.TryGetValue(incoming.Pid, out string? existingPath) &&
|
|
SessionFileStorage.Exists(existingPath))
|
|
{
|
|
tab.BackingStorePath = existingPath;
|
|
usingBackingStore = true;
|
|
}
|
|
else
|
|
{
|
|
tab.BackingStorePath = null;
|
|
MaterializeLoadedSnapshot(tab, incoming);
|
|
}
|
|
|
|
if (usingBackingStore)
|
|
{
|
|
tab.Events.Clear();
|
|
tab.PerformanceHistory.Clear();
|
|
tab.MemoryRegionAttributionHistory.Clear();
|
|
tab.ThreadLifecycleHistory.Clear();
|
|
tab.ThreadStackHistories.Clear();
|
|
_etwHistoryByPid.Remove(tab.Pid);
|
|
_heuristicsHistoryByPid.Remove(tab.Pid);
|
|
_filesystemHistoryByPid.Remove(tab.Pid);
|
|
_registryHistoryByPid.Remove(tab.Pid);
|
|
_relationsHistoryByPid.Remove(tab.Pid);
|
|
_apiGraphHistoryByPid.Remove(tab.Pid);
|
|
_extendedHistoryByPid.Remove(tab.Pid);
|
|
}
|
|
|
|
await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Background);
|
|
}
|
|
|
|
ProcessSessionTab? toSelect =
|
|
_processTabs.FirstOrDefault(x => x.Pid == archive.ActivePid) ?? _processTabs.FirstOrDefault();
|
|
if (toSelect == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
SessionFileTab? selectedSnapshot =
|
|
archive.Tabs.FirstOrDefault(x => x.Pid == toSelect.Pid) ?? archive.Tabs.FirstOrDefault();
|
|
if (selectedSnapshot != null)
|
|
{
|
|
await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Background);
|
|
MaterializeLoadedSnapshot(toSelect, selectedSnapshot);
|
|
}
|
|
|
|
_suppressTabSelectionChange = true;
|
|
ProcessTabs.SelectedItem = toSelect;
|
|
_suppressTabSelectionChange = false;
|
|
SwitchToSession(toSelect);
|
|
await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Background);
|
|
}
|
|
|
|
private void MaterializeLoadedSnapshot(ProcessSessionTab tab, SessionFileTab snapshot)
|
|
{
|
|
tab.Events.Clear();
|
|
tab.Events.AddRange(snapshot.Events.Select(CloneTelemetryEvent));
|
|
tab.PerformanceHistory.Clear();
|
|
tab.PerformanceHistory.AddRange(snapshot.PerformanceHistory.Select(ClonePerformanceSample));
|
|
tab.MemoryRegionAttributionHistory.Clear();
|
|
tab.MemoryRegionAttributionHistory.AddRange(
|
|
snapshot.MemoryRegionAttributionHistory.Select(CloneMemoryRegionAttributionSample));
|
|
tab.ThreadLifecycleHistory.Clear();
|
|
tab.ThreadLifecycleHistory.AddRange(snapshot.ThreadLifecycleHistory.Select(CloneThreadLifecycleEvent));
|
|
tab.ThreadStackHistories.Clear();
|
|
tab.ThreadStackHistories.AddRange(snapshot.ThreadStackHistories.Select(x => x.Clone()));
|
|
|
|
_etwHistoryByPid[tab.Pid] = snapshot.EtwGroups.Select(x => x.Clone()).ToList();
|
|
_heuristicsHistoryByPid[tab.Pid] = snapshot.HeuristicsGroups.Select(x => x.Clone()).ToList();
|
|
_filesystemHistoryByPid[tab.Pid] = snapshot.FilesystemGroups.Select(x => x.Clone()).ToList();
|
|
_registryHistoryByPid[tab.Pid] = snapshot.RegistryGroups.Select(x => x.Clone()).ToList();
|
|
_relationsHistoryByPid[tab.Pid] = snapshot.ProcessRelationsGroups.Select(x => x.Clone()).ToList();
|
|
_apiGraphHistoryByPid[tab.Pid] = snapshot.ApiGraphRows.Select(x => x.Clone()).ToList();
|
|
_extendedHistoryByPid[tab.Pid] = snapshot.ExtendedActivityRows.Select(x => x.Clone()).ToList();
|
|
}
|
|
|
|
private async void SaveSessionAs_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
await SaveSessionArchiveViaDialogAsync();
|
|
}
|
|
|
|
private async void ExportSession_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var dialog = new SaveFileDialog {
|
|
Filter = "Blackbird Capture Archive (*.bkcap)|*.bkcap|" + "SIEM JSON Lines (*.jsonl)|*.jsonl|" +
|
|
"SIEM CSV (*.csv)|*.csv|" + "CEF (*.cef)|*.cef|" +
|
|
"ATT&CK-ready CSV (*.attack.csv)|*.attack.csv|" + "All files (*.*)|*.*",
|
|
DefaultExt = CaptureArchiveExtension, AddExtension = true, OverwritePrompt = true,
|
|
FileName = $"Blackbird-export-{DateTime.UtcNow:yyyyMMdd-HHmmss}{CaptureArchiveExtension}"
|
|
};
|
|
|
|
if (dialog.ShowDialog(this) != true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
SessionFileArchive archive = await BuildWorkspaceArchiveAsync("Preparing session export...");
|
|
if (IsCaptureArchivePath(dialog.FileName))
|
|
{
|
|
await RunSessionStorageOperationAsync(
|
|
"Exporting session...",
|
|
() => Task.Run(() => SessionFileStorage.SaveArchive(dialog.FileName, archive)));
|
|
StatusBlock.Text = $"SESSION EXPORTED: {Path.GetFileName(dialog.FileName)}";
|
|
return;
|
|
}
|
|
|
|
SessionExportFormat format = ResolveSessionExportFormat(dialog.FileName, dialog.FilterIndex);
|
|
await RunSessionStorageOperationAsync(
|
|
"Exporting session...",
|
|
() => Task.Run(() => SessionExportService.Export(dialog.FileName, archive, format)));
|
|
StatusBlock.Text = $"SESSION EXPORTED: {Path.GetFileName(dialog.FileName)}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ThemedMessageBox.Show(this, $"Failed to export session.\n\n{ex.Message}", "Export Session",
|
|
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
private async void ExportDetections_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var dialog = new SaveFileDialog {
|
|
Filter = "SIEM Detection JSON Lines (*.detections.jsonl)|*.detections.jsonl|" +
|
|
"Splunk HEC Events (*.splunk.json)|*.splunk.json|" +
|
|
"Elastic ECS NDJSON (*.ecs.ndjson)|*.ecs.ndjson|" +
|
|
"CEF Detections (*.detections.cef)|*.detections.cef|" +
|
|
"Detection CSV (*.detections.csv)|*.detections.csv|" + "All files (*.*)|*.*",
|
|
DefaultExt = ".detections.jsonl", AddExtension = true, OverwritePrompt = true,
|
|
FileName = $"Blackbird-detections-{DateTime.UtcNow:yyyyMMdd-HHmmss}.detections.jsonl"
|
|
};
|
|
|
|
if (dialog.ShowDialog(this) != true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
SessionFileArchive archive = await BuildWorkspaceArchiveAsync("Preparing detections export...");
|
|
SessionExportFormat format = ResolveDetectionExportFormat(dialog.FileName, dialog.FilterIndex);
|
|
int exported = await RunSessionStorageOperationAsync(
|
|
"Exporting detections...",
|
|
() => Task.Run(() => SessionExportService.Export(dialog.FileName, archive, format)));
|
|
StatusBlock.Text = $"DETECTIONS EXPORTED: {exported} events -> {Path.GetFileName(dialog.FileName)}";
|
|
if (exported == 0)
|
|
{
|
|
ThemedMessageBox.Show(this, "No detections were available to export.", "Export Detections",
|
|
MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ThemedMessageBox.Show(this, $"Failed to export detections.\n\n{ex.Message}", "Export Detections",
|
|
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
private async Task<string> TryOpenSessionArchivePathAsync(string path, bool merge)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path) || !SessionFileStorage.Exists(path))
|
|
{
|
|
return "Session file not found.";
|
|
}
|
|
|
|
try
|
|
{
|
|
CaptureLoadedWorkspace workspace =
|
|
await RunSessionStorageOperationAsync(merge ? "Importing session..." : "Opening session...",
|
|
() => Task.Run(() => SessionFileStorage.LoadWorkspace(path)));
|
|
RegisterTemporaryWorkspace(workspace);
|
|
await ApplyWorkspaceArchiveAsync(workspace, merge);
|
|
if (!merge)
|
|
{
|
|
_sessionFilePath = path;
|
|
}
|
|
|
|
string verb = merge ? "IMPORTED" : "OPENED";
|
|
StatusBlock.Text = $"SESSION {verb}: {Path.GetFileName(path)}";
|
|
return string.Empty;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return ex.Message.Contains("manifest not found", StringComparison.OrdinalIgnoreCase)
|
|
? "Capture archive is invalid or incomplete."
|
|
: ex.Message;
|
|
}
|
|
}
|
|
|
|
private async void OpenSession_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var dialog =
|
|
new OpenFileDialog { Filter = CaptureArchiveOpenFilter, CheckFileExists = true, Multiselect = false };
|
|
|
|
if (dialog.ShowDialog(this) != true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string error = await TryOpenSessionArchivePathAsync(dialog.FileName, merge: false);
|
|
if (!string.IsNullOrEmpty(error))
|
|
{
|
|
ThemedMessageBox.Show(this, $"Failed to open session.\n\n{error}", "Open Session", MessageBoxButton.OK,
|
|
MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
private async void ImportSession_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var dialog =
|
|
new OpenFileDialog { Filter = CaptureArchiveOpenFilter, CheckFileExists = true, Multiselect = false };
|
|
|
|
if (dialog.ShowDialog(this) != true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string error = await TryOpenSessionArchivePathAsync(dialog.FileName, merge: true);
|
|
if (!string.IsNullOrEmpty(error))
|
|
{
|
|
ThemedMessageBox.Show(this, $"Failed to import session.\n\n{error}", "Import Session",
|
|
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
private bool PrepareSessionShutdown()
|
|
{
|
|
if (_isMainWindowShuttingDown || !HasSessionCacheData())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
MessageBoxResult choice = ThemedMessageBox.Show(
|
|
this,
|
|
"Save the current Blackbird session before exit?\n\nSelecting No removes the temporary session datastore on teardown.",
|
|
"Exit Session", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
|
|
if (choice == MessageBoxResult.Cancel)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return choice != MessageBoxResult.Yes || SaveSessionArchiveViaDialog();
|
|
}
|
|
|
|
private bool SaveSessionArchiveViaDialog()
|
|
{
|
|
var dialog =
|
|
new SaveFileDialog { Filter = CaptureArchiveSaveFilter, DefaultExt = CaptureArchiveExtension,
|
|
AddExtension = true, OverwritePrompt = true,
|
|
FileName =
|
|
$"Blackbird-{DateTime.UtcNow:yyyyMMdd-HHmmss}{CaptureArchiveExtension}" };
|
|
|
|
if (!string.IsNullOrWhiteSpace(_sessionFilePath))
|
|
{
|
|
dialog.InitialDirectory = Path.GetDirectoryName(_sessionFilePath);
|
|
}
|
|
|
|
if (dialog.ShowDialog(this) != true)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
SessionFileArchive archive = BuildWorkspaceArchive();
|
|
SessionFileStorage.SaveArchive(dialog.FileName, archive);
|
|
_sessionFilePath = dialog.FileName;
|
|
StatusBlock.Text = $"SESSION SAVED: {Path.GetFileName(dialog.FileName)}";
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ThemedMessageBox.Show(this, $"Failed to save session.\n\n{ex.Message}", "Save Session",
|
|
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async Task<bool> SaveSessionArchiveViaDialogAsync()
|
|
{
|
|
var dialog =
|
|
new SaveFileDialog { Filter = CaptureArchiveSaveFilter, DefaultExt = CaptureArchiveExtension,
|
|
AddExtension = true, OverwritePrompt = true,
|
|
FileName =
|
|
$"Blackbird-{DateTime.UtcNow:yyyyMMdd-HHmmss}{CaptureArchiveExtension}" };
|
|
|
|
if (!string.IsNullOrWhiteSpace(_sessionFilePath))
|
|
{
|
|
dialog.InitialDirectory = Path.GetDirectoryName(_sessionFilePath);
|
|
}
|
|
|
|
if (dialog.ShowDialog(this) != true)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
SessionFileArchive archive = await BuildWorkspaceArchiveAsync("Preparing session save...");
|
|
await RunSessionStorageOperationAsync(
|
|
"Saving session...",
|
|
() => Task.Run(() => SessionFileStorage.SaveArchive(dialog.FileName, archive)));
|
|
_sessionFilePath = dialog.FileName;
|
|
StatusBlock.Text = $"SESSION SAVED: {Path.GetFileName(dialog.FileName)}";
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ThemedMessageBox.Show(this, $"Failed to save session.\n\n{ex.Message}", "Save Session",
|
|
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async Task RunSessionStorageOperationAsync(string statusText, Func<Task> operation)
|
|
{
|
|
await RunSessionStorageOperationAsync(statusText, async () =>
|
|
{
|
|
await operation();
|
|
return true;
|
|
});
|
|
}
|
|
|
|
private async Task<T> RunSessionStorageOperationAsync<T>(string statusText, Func<Task<T>> operation)
|
|
{
|
|
Cursor? previousCursor = Mouse.OverrideCursor;
|
|
string previousStatus = StatusBlock.Text;
|
|
try
|
|
{
|
|
Mouse.OverrideCursor = Cursors.Wait;
|
|
StatusBlock.Text = statusText;
|
|
await Task.Yield();
|
|
return await operation();
|
|
}
|
|
finally
|
|
{
|
|
Mouse.OverrideCursor = previousCursor;
|
|
if (string.Equals(StatusBlock.Text, statusText, StringComparison.Ordinal))
|
|
{
|
|
StatusBlock.Text = previousStatus;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool IsCaptureArchivePath(string path)
|
|
{
|
|
string extension = Path.GetExtension(path ?? string.Empty);
|
|
return extension.Equals(CaptureArchiveExtension, StringComparison.OrdinalIgnoreCase) ||
|
|
extension.Equals(".swlkr", StringComparison.OrdinalIgnoreCase) ||
|
|
extension.Equals(".blackbird", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static SessionExportFormat ResolveSessionExportFormat(string path, int filterIndex)
|
|
{
|
|
string fileName = Path.GetFileName(path ?? string.Empty);
|
|
if (fileName.EndsWith(".attack.csv", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return SessionExportFormat.AttackCsv;
|
|
}
|
|
|
|
return Path.GetExtension(path ?? string.Empty).ToLowerInvariant() switch
|
|
{
|
|
".jsonl" => SessionExportFormat.JsonLines, ".csv" => SessionExportFormat.Csv,
|
|
".cef" => SessionExportFormat.Cef,
|
|
_ => filterIndex switch
|
|
{
|
|
2 => SessionExportFormat.JsonLines,
|
|
3 => SessionExportFormat.Csv,
|
|
4 => SessionExportFormat.Cef,
|
|
5 => SessionExportFormat.AttackCsv,
|
|
_ => SessionExportFormat.JsonLines
|
|
}
|
|
};
|
|
}
|
|
|
|
private static SessionExportFormat ResolveDetectionExportFormat(string path, int filterIndex)
|
|
{
|
|
if (filterIndex is >= 1 and <= 5)
|
|
{
|
|
return filterIndex switch {
|
|
1 => SessionExportFormat.DetectionJsonLines,
|
|
2 => SessionExportFormat.SplunkHecJson,
|
|
3 => SessionExportFormat.ElasticEcsJsonLines,
|
|
4 => SessionExportFormat.DetectionCef,
|
|
5 => SessionExportFormat.DetectionCsv,
|
|
_ => SessionExportFormat.DetectionJsonLines
|
|
};
|
|
}
|
|
|
|
string fileName = Path.GetFileName(path ?? string.Empty);
|
|
if (fileName.EndsWith(".detections.csv", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return SessionExportFormat.DetectionCsv;
|
|
}
|
|
if (fileName.EndsWith(".detections.cef", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return SessionExportFormat.DetectionCef;
|
|
}
|
|
if (fileName.EndsWith(".splunk.json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return SessionExportFormat.SplunkHecJson;
|
|
}
|
|
if (fileName.EndsWith(".ecs.ndjson", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return SessionExportFormat.ElasticEcsJsonLines;
|
|
}
|
|
if (fileName.EndsWith(".detections.jsonl", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return SessionExportFormat.DetectionJsonLines;
|
|
}
|
|
|
|
return SessionExportFormat.DetectionJsonLines;
|
|
}
|
|
|
|
private bool HasSessionCacheData()
|
|
{
|
|
if (_currentSession != null &&
|
|
(_allEvents.Count > 0 || _currentSession.PerformanceHistory.Count > 0 ||
|
|
_currentSession.ThreadLifecycleHistory.Count > 0 || _currentSession.ThreadStackHistories.Count > 0))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return _processTabs.Any(x => HasInlineSessionData(x) || SessionFileStorage.Exists(x.BackingStorePath));
|
|
}
|
|
|
|
private void CleanupTemporarySessionBackingStores()
|
|
{
|
|
foreach (ProcessSessionTab tab in _processTabs)
|
|
{
|
|
if (!IsSessionCachePath(tab.BackingStorePath) || !SessionFileStorage.Exists(tab.BackingStorePath))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
SessionFileStorage.DeletePath(tab.BackingStorePath);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
ReleaseOwnedTemporaryWorkspaces();
|
|
}
|
|
|
|
private void SpillCurrentSessionWorkingSetIfNeeded()
|
|
{
|
|
if (_currentSession == null || _currentSession.OfflineSnapshot || _currentSession.Pid <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_liveCaptureStore != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DateTime now = DateTime.UtcNow;
|
|
if (_currentSession.BackingStorePath != null &&
|
|
SessionFileStorage.Exists(_currentSession.BackingStorePath) &&
|
|
now - SessionFileStorage.GetLastWriteTimeUtc(_currentSession.BackingStorePath) < SessionSpillInterval)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int totalGroupedDetails = EtwPaneHost.DetailRowCount + HeuristicsPaneHost.DetailRowCount +
|
|
FilesystemPaneHost.DetailRowCount + ProcessRelationsPaneHost.DetailRowCount;
|
|
int threadStackSnapshots = _currentSession.ThreadStackHistories.Sum(x => x.Snapshots.Count);
|
|
if (totalGroupedDetails < LiveGroupedDetailSpillThreshold &&
|
|
threadStackSnapshots < LiveThreadStackSpillThreshold)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_currentSession.CaptureStartUtc = _captureStartUtc;
|
|
_currentSession.LaneFocusKey = _laneFocusKey;
|
|
_currentSession.ViewDurationSeconds = EventsPaneHost.Timeline.ViewDurationSeconds;
|
|
_currentSession.ViewStartSeconds = EventsPaneHost.Timeline.ViewStartSeconds;
|
|
SaveIntelSessionState(_currentSession.Pid);
|
|
|
|
SessionFileTab snapshot = BuildTabSnapshot(_currentSession);
|
|
string path = _currentSession.BackingStorePath ?? AllocateSessionCachePath(_currentSession.Pid);
|
|
_currentSession.BackingStorePath = path;
|
|
snapshot.CaptureStorePath = path;
|
|
SessionFileStorage.SaveArchive(path, CreateSingleTabArchive(snapshot));
|
|
SaveIntelSessionState(_currentSession.Pid);
|
|
}
|
|
|
|
private IReadOnlyList<ThreadStackSessionSnapshot> GetThreadStackHistory(int pid, int tid, string state)
|
|
{
|
|
ProcessSessionTab? tab = ResolveSessionTab(pid);
|
|
if (tab == null)
|
|
{
|
|
return Array.Empty<ThreadStackSessionSnapshot>();
|
|
}
|
|
|
|
EnsureSessionMaterialized(tab);
|
|
return tab.ThreadStackHistories.Where(x => x.Tid == tid)
|
|
.SelectMany(x => x.Snapshots)
|
|
.Select(x => x.Clone())
|
|
.OrderBy(x => x.CapturedAtUtc)
|
|
.ToList();
|
|
}
|
|
|
|
private void PersistThreadStackSnapshot(int pid, int tid, string state, ThreadStackSessionSnapshot snapshot)
|
|
{
|
|
if (snapshot == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ProcessSessionTab? tab = ResolveSessionTab(pid);
|
|
if (tab == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
EnsureSessionMaterialized(tab);
|
|
string normalizedState = state ?? string.Empty;
|
|
ThreadStackHistoryArchiveEntry? history = tab.ThreadStackHistories.FirstOrDefault(x => x.Tid == tid);
|
|
if (history == null)
|
|
{
|
|
history = new ThreadStackHistoryArchiveEntry { Tid = tid, State = normalizedState };
|
|
tab.ThreadStackHistories.Add(history);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(normalizedState))
|
|
{
|
|
history.State = normalizedState;
|
|
}
|
|
|
|
int existingIndex = history.Snapshots.FindIndex(x => x.CapturedAtUtc == snapshot.CapturedAtUtc);
|
|
if (existingIndex >= 0)
|
|
{
|
|
history.Snapshots[existingIndex] = snapshot.Clone();
|
|
}
|
|
else
|
|
{
|
|
history.Snapshots.Add(snapshot.Clone());
|
|
history.Snapshots = history.Snapshots.OrderBy(x => x.CapturedAtUtc).ToList();
|
|
}
|
|
|
|
if (history.Snapshots.Count > MaxLiveThreadStackSnapshotsPerThread)
|
|
{
|
|
history.Snapshots =
|
|
history.Snapshots.Skip(history.Snapshots.Count - MaxLiveThreadStackSnapshotsPerThread).ToList();
|
|
}
|
|
|
|
if (tab.ThreadStackHistories.Count > MaxLiveThreadStackHistoriesPerTab)
|
|
{
|
|
List<ThreadStackHistoryArchiveEntry> retained =
|
|
tab.ThreadStackHistories
|
|
.OrderByDescending(x => x.Snapshots.Count == 0 ? DateTime.MinValue
|
|
: x.Snapshots.Max(s => s.CapturedAtUtc))
|
|
.Take(MaxLiveThreadStackHistoriesPerTab)
|
|
.OrderBy(x => x.Tid)
|
|
.ToList();
|
|
tab.ThreadStackHistories.Clear();
|
|
tab.ThreadStackHistories.AddRange(retained);
|
|
}
|
|
|
|
if (ReferenceEquals(tab, _currentSession))
|
|
{
|
|
PerformancePaneHost.LoadThreadStackHistory(tab.ThreadStackHistories);
|
|
}
|
|
}
|
|
|
|
private ProcessSessionTab? ResolveSessionTab(int pid)
|
|
{
|
|
if (_currentSession != null && _currentSession.Pid == pid)
|
|
{
|
|
return _currentSession;
|
|
}
|
|
|
|
return _processTabs.FirstOrDefault(x => x.Pid == pid);
|
|
}
|
|
}
|
|
}
|