Distinguisher9k is a Windows system utility designed for detailed real-time monitoring and analysis of disk write activity on a per-process basis. It focuses on identifying which processes are persistently writing data to the disk, especially in the form of log files or system event logs, which can contribute to constant disk activity preventing drive idling.

Specifically, the tool addresses scenarios like the one you shared earlier, where system processes such as "System" and "svchost.exe" write continuously to various system log files and NTFS metadata files, including but not limited to:

- Windows Event Trace Log files (.etl) in directories like C:\Windows\System32\LogFiles\WMI\RtBackup\
- Local service heartbeat files like lastalive0.dat within user service profile directories
- Critical file system metadata files such as $LogFile, $Mft, $BitMap, and USN Journal ($UsnJrnl:$J), which are essential to the NTFS file system's operation
- Registry log files (e.g., SYSTEM.LOG1 and SYSTEM) in C:\Windows\System32\Config\
- Prefetch files used by Windows to optimize boot times and application loading

These constant low-level disk writes generate continuous IO activity, which can cause the physical disk to stay active without resting, impacting system performance or power management.

Distinguisher9k assists by:

- Continuously tracking disk write operations of all running processes with per-second granularity.
- Listing active processes sorted by recent write throughput to surface the highest disk consumers.
- Detecting and highlighting which files each process has open, focusing on files that are typical for logging or system tracing.
- Providing filtering mechanisms so users can quickly locate suspicious or unusually active processes by name or write volume thresholds.
- Rendering real-time charts that show the write activity trends of selected processes over approximately the last 60 seconds, helping in identifying periodic spikes or sustained logging behavior.
- Allowing direct user interventions such as terminating a selected process, exporting monitoring data to CSV files, and applying filters to focus on relevant processes.

By correlating processes with their open log files and visually presenting disk write trends, the tool empowers system administrators and power users to pinpoint potentially misbehaving or overly verbose drivers, services, or applications contributing to constant disk usage.

This is crucial for scenarios where system diagnostic logging (like ETW sessions, WMI logs, and system registry activities) combined with core OS metadata maintenance leads to persistent disk throughput that prevents HDD spin-down and increases wear or energy consumption. The tool helps identify such "chatty" processes so they can be investigated, optimized, or curtailed, resulting in better system responsiveness and potentially extended hardware life.



```

import wx
import psutil
import threading
import time
import os
import csv
import matplotlib.pyplot as plt
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas

# Constants for log file detection
LOG_EXTENSIONS = ('.log', '.etl', '.evtx', '.txt')
LOG_DIRS = [
    os.path.expandvars(r'%SystemRoot%\System32\LogFiles'),
    os.path.expandvars(r'%ProgramData%'),
    os.path.expanduser(r'~\\AppData\\Local'),
    os.path.expanduser(r'~\\AppData\\Roaming'),
]

def is_log_file(filepath):
    if not filepath:
        return False
    filepath_lower = filepath.lower()
    if filepath_lower.endswith(LOG_EXTENSIONS):
        return True
    # Match if log directory is part of the path (case-insensitive)
    for d in LOG_DIRS:
        if d.lower() in filepath_lower:
            return True
    return False

class DiskWriteMonitorApp(wx.Frame):

    def __init__(self):
        super().__init__(None, title="Distinguisher9k - Disk Write Monitor", size=(900, 700))

        # State variables
        self.monitoring = False
        self.process_data = {}   # pid -> {'name': str, 'write_history': list[int], 'open_logs': set[str]}
        self.selected_pid = None
        self.lock = threading.Lock()
        self.monitor_thread = None

        # ===== Build UI =====
        panel = wx.Panel(self)

        main_sizer = wx.BoxSizer(wx.VERTICAL)

        # Controls (Start, Stop, Terminate, Export)
        btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.start_btn = wx.Button(panel, label="Start Monitoring")
        self.stop_btn = wx.Button(panel, label="Stop Monitoring")
        self.stop_btn.Disable()
        self.terminate_btn = wx.Button(panel, label="Terminate Process")
        self.terminate_btn.Disable()
        self.export_btn = wx.Button(panel, label="Export CSV")
        self.export_btn.Disable()

        btn_sizer.Add(self.start_btn, 0, wx.ALL, 5)
        btn_sizer.Add(self.stop_btn, 0, wx.ALL, 5)
        btn_sizer.Add(self.terminate_btn, 0, wx.ALL, 5)
        btn_sizer.Add(self.export_btn, 0, wx.ALL, 5)

        # Filters: Process name and min write KB/s
        filter_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.name_filter = wx.TextCtrl(panel)
        self.name_filter.SetHint("Filter by process name (case-insensitive)")
        self.throughput_filter = wx.TextCtrl(panel)
        self.throughput_filter.SetHint("Min write throughput KB/s (numeric)")
        self.throughput_filter.SetMinSize((150, -1))

        filter_sizer.Add(self.name_filter, 1, wx.ALL | wx.EXPAND, 5)
        filter_sizer.Add(self.throughput_filter, 0, wx.ALL, 5)

        # Process list (wx.ListCtrl)
        self.process_list = wx.ListCtrl(panel, style=wx.LC_REPORT | wx.BORDER_SUNKEN)
        self.process_list.InsertColumn(0, "Process Name", width=180)
        self.process_list.InsertColumn(1, "PID", width=80)
        self.process_list.InsertColumn(2, "Write KB/s", wx.LIST_FORMAT_RIGHT, 100)
        self.process_list.InsertColumn(3, "Open Log Files (max 3)", width=490)

        # Chart panel (matplotlib figure)
        self.chart_panel = wx.Panel(panel)
        self.chart_fig, self.chart_ax = plt.subplots(figsize=(8, 3), dpi=100)
        self.chart_canvas = FigureCanvas(self.chart_panel, -1, self.chart_fig)
        chart_sizer = wx.BoxSizer(wx.VERTICAL)
        chart_sizer.Add(self.chart_canvas, 1, wx.EXPAND)
        self.chart_panel.SetSizer(chart_sizer)

        # Put everything together
        main_sizer.Add(btn_sizer, 0, wx.CENTER)
        main_sizer.Add(filter_sizer, 0, wx.EXPAND)
        main_sizer.Add(self.process_list, 1, wx.ALL | wx.EXPAND, 5)
        main_sizer.Add(self.chart_panel, 0, wx.ALL | wx.EXPAND, 5)

        panel.SetSizer(main_sizer)

        # ===== Bind Events =====
        self.start_btn.Bind(wx.EVT_BUTTON, self.on_start)
        self.stop_btn.Bind(wx.EVT_BUTTON, self.on_stop)
        self.terminate_btn.Bind(wx.EVT_BUTTON, self.on_terminate)
        self.export_btn.Bind(wx.EVT_BUTTON, self.on_export)
        self.process_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_process_select)
        self.name_filter.Bind(wx.EVT_TEXT, self.on_filter_change)
        self.throughput_filter.Bind(wx.EVT_TEXT, self.on_filter_change)

        # Initialize selection to none
        self.selected_pid = None

    def on_start(self, event):
        if self.monitoring:
            return
        self.monitoring = True
        self.process_data.clear()
        self.selected_pid = None
        self.start_btn.Disable()
        self.stop_btn.Enable()
        self.terminate_btn.Disable()
        self.export_btn.Disable()
        self.process_list.DeleteAllItems()

        self.monitor_thread = threading.Thread(target=self.monitor_loop, daemon=True)
        self.monitor_thread.start()

    def on_stop(self, event):
        if not self.monitoring:
            return
        self.monitoring = False
        if self.monitor_thread and self.monitor_thread.is_alive():
            self.monitor_thread.join(timeout=5)
        self.start_btn.Enable()
        self.stop_btn.Disable()
        self.terminate_btn.Disable()
        self.export_btn.Disable()
        self.selected_pid = None
        self.process_data.clear()
        self.process_list.DeleteAllItems()
        self.chart_ax.clear()
        self.chart_ax.set_title("Monitoring stopped")
        self.chart_canvas.draw()

    def on_terminate(self, event):
        if self.selected_pid is None:
            wx.MessageBox("No process selected.", "Error", wx.ICON_ERROR)
            return
        try:
            proc = psutil.Process(self.selected_pid)
            proc.terminate()
            wx.MessageBox(f"Process {proc.name()} (PID {self.selected_pid}) terminated.", "Success", wx.ICON_INFORMATION)
        except Exception as e:
            wx.MessageBox(f"Failed to terminate process: {e}", "Error", wx.ICON_ERROR)

    def on_export(self, event):
        with wx.FileDialog(self, "Save CSV report", wildcard="CSV files (*.csv)|*.csv",
                           style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog:
            if fileDialog.ShowModal() == wx.ID_CANCEL:
                return
            path = fileDialog.GetPath()
            try:
                with open(path, 'w', newline='', encoding='utf-8') as csvfile:
                    writer = csv.writer(csvfile)
                    writer.writerow(['Process Name', 'PID', 'Current Write KB/s', 'Open Log Files'])
                    with self.lock:
                        for pid, pdata in self.process_data.items():
                            write_rate = pdata['write_history'][-1] if pdata['write_history'] else 0
                            open_logs = '; '.join(pdata['open_logs']) if pdata['open_logs'] else ''
                            writer.writerow([pdata['name'], pid, f"{write_rate/1024:.2f}", open_logs])
                wx.MessageBox(f"Report saved successfully to:\n{path}", "Success", wx.ICON_INFORMATION)
            except Exception as e:
                wx.MessageBox(f"Failed to save report:\n{e}", "Error", wx.ICON_ERROR)

    def on_process_select(self, event):
        index = event.GetIndex()
        pid = self.process_list.GetItemData(index)
        if pid != self.selected_pid:
            self.selected_pid = pid
            self.update_chart(pid)
            self.terminate_btn.Enable()
        else:
            self.selected_pid = None
            self.terminate_btn.Disable()
            self.clear_chart()

    def on_filter_change(self, event):
        # Debounce or throttle could be implemented for better UX on fast typing
        self.refresh_process_list()

    def monitor_loop(self):
        prev_write_bytes = {}
        while self.monitoring:
            current_data = {}
            for proc in psutil.process_iter(['pid', 'name']):
                try:
                    io = proc.io_counters()
                    if io is None:
                        continue
                    pid = proc.pid
                    name = proc.info['name']

                    write_bytes = io.write_bytes
                    prev_bytes = prev_write_bytes.get(pid, None)
                    rate_bytes = (write_bytes - prev_bytes) if prev_bytes is not None else 0
                    prev_write_bytes[pid] = write_bytes

                    # Open log files detection
                    open_logs = []
                    try:
                        for f in proc.open_files():
                            # Detect log files as per heuristic
                            if is_log_file(f.path):
                                open_logs.append(f.path)
                    except (psutil.AccessDenied, psutil.NoSuchProcess):
                        pass

                    current_data[pid] = {
                        'name': name,
                        'write_rate': rate_bytes,
                        'open_logs': open_logs,
                    }

                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    continue

            with self.lock:
                # Update self.process_data and keep write history capped to 60 samples
                for pid, pdata in current_data.items():
                    if pid not in self.process_data:
                        self.process_data[pid] = {'name': pdata['name'], 'write_history': [], 'open_logs': []}
                    self.process_data[pid]['write_history'].append(pdata['write_rate'])
                    if len(self.process_data[pid]['write_history']) > 60:
                        self.process_data[pid]['write_history'].pop(0)
                    self.process_data[pid]['open_logs'] = pdata['open_logs']

                # Remove pids that disappeared
                for pid in list(self.process_data.keys()):
                    if pid not in current_data:
                        del self.process_data[pid]

            wx.CallAfter(self.refresh_process_list)
            time.sleep(1)

    def refresh_process_list(self):
        filter_text = self.name_filter.GetValue().lower()
        # Min throughput filter, default 0 if invalid
        try:
            min_throughput = float(self.throughput_filter.GetValue())
            if min_throughput < 0:
                min_throughput = 0.0
        except Exception:
            min_throughput = 0.0

        self.process_list.Freeze()
        self.process_list.DeleteAllItems()

        with self.lock:
            # Sort descending by latest write_rate
            sorted_procs = sorted(
                self.process_data.items(),
                key=lambda kv: kv[1]['write_history'][-1] if kv[1]['write_history'] else 0,
                reverse=True)

            row_idx = 0
            for pid, pdata in sorted_procs:
                latest_rate_bytes = pdata['write_history'][-1] if pdata['write_history'] else 0
                latest_rate_kb = latest_rate_bytes / 1024

                if latest_rate_kb < min_throughput:
                    continue
                if filter_text and filter_text not in pdata['name'].lower():
                    continue

                # Summarize up to 3 log files (base names)
                logs_display = ', '.join(os.path.basename(f) for f in pdata['open_logs'][:3])
                if len(pdata['open_logs']) > 3:
                    logs_display += ', ...'

                self.process_list.Append((
                    pdata['name'],
                    str(pid),
                    f"{latest_rate_kb:.2f}",
                    logs_display,
                ))
                self.process_list.SetItemData(row_idx, pid)
                row_idx += 1

        self.process_list.Thaw()

    def update_chart(self, pid):
        with self.lock:
            pdata = self.process_data.get(pid, None)
            if not pdata or not pdata['write_history']:
                self.clear_chart("No data for selected process")
                return
            y = [max(0, x / 1024) for x in pdata['write_history']]  # KB/s positive only
            label = pdata['name']

        self.chart_ax.clear()
        self.chart_ax.plot(y, label=label, color='tab:blue')
        self.chart_ax.set_xlabel("Seconds (last 60)")
        self.chart_ax.set_ylabel("Write KB/s")
        self.chart_ax.set_title(f"Disk Write Activity Over Time - PID {pid} ({label})")
        self.chart_ax.legend()
        self.chart_ax.grid(True)
        self.chart_canvas.draw()

    def clear_chart(self, message=""):
        self.chart_ax.clear()
        if message:
            self.chart_ax.text(0.5, 0.5, message, ha='center', va='center', fontsize=12)
        self.chart_canvas.draw()


def main():
    app = wx.App(False)
    frame = DiskWriteMonitorApp()
    frame.Show()
    app.MainLoop()

if __name__ == '__main__':
    main()


```


"""
```



Here is a summary of the main mistakes and errors related to the overall construct that occurred throughout this chat:

    Missing Initialization of Key Attributes
    The matplotlib components (self.chart_ax, self.chart_fig, and self.chart_canvas) were sometimes not properly initialized as instance attributes before being used in methods like clear_chart(). This caused repeated AttributeError exceptions saying these attributes did not exist.

    Incorrect Use of wx.ListCtrl Methods
    The method ScrollList was called without the required two arguments (lines and per_page), causing a TypeError about insufficient arguments.

    Handling Updates in wx.ListCtrl
    Attempts to preserve selection and scroll position during list updates sometimes led to invisible or flickering rows in the process list. Complex logic for freezing/thawing the control and restoring the UI state was prone to causing visual glitches and instability.

    Confusion Over Insertion Methods
    Using InsertItem combined with SetItem and SetItemData for list control updates brought complexity and increased the chance of off-by-one or indexing errors, compared to simpler Append with a tuple of columns which worked more reliably.

    Premature or Improper UI Updates
    Calling UI update functions that referenced uninitialized or non-existing attributes resulted in errors. Calls to redraw or refresh controls were sometimes missing or misplaced, causing invisible or incomplete UI rendering.

    Thread-Safety and UI Thread Interaction
    While mostly addressed, there remained subtle risks due to improper synchronization between monitoring threads and UI updates. Using wx.CallAfter was required but not always consistently applied in earlier versions.

    Overcomplication and Repeated Rewrites
    Multiple rewrites of the entire app introduced features and complexity faster than could be fully tested and debugged, leading to recurring problems and oscillating issues instead of incremental, stable improvements.

    Ignoring Working Foundations
    Several later versions moved away from the simpler but more stable initial working code, adding unnecessary complexities that caused regressions in UI visibility and functionality.

    Inconsistent or Missing Comments and Documentation
    The clarity of the code and explanations was sometimes sacrificed, which hampered diagnosing issues and verifying correctness.

In summary, most errors were due to improper initialization order, incorrect GUI method calls, overcomplicated UI state management, and the challenges of properly coordinating background thread updates with the wxPython event-driven UI. The stable and effective solution relied on keeping UI updates simple, initializing graphics objects upfront, and carefully updating the list control with minimal but robust logic.


```
""".