<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>~/home</title>
  <subtitle>Practical notes from a small homelab: Linux servers, nginx, certbot, Node.js under pm2, restic backups, DNS, and Windows debugging (crashes, bad RAM, CPU metrics, PowerShell).</subtitle>
  <link href="https://home.tigran.manukyan.dev/feed.xml" rel="self"/>
  <link href="https://home.tigran.manukyan.dev/"/>
  <id>https://home.tigran.manukyan.dev/</id>
  <updated>2026-11-05T12:00:00Z</updated>
  <author><name>~/home</name></author>
  <entry>
    <title>Upgrading Node under pm2 without the surprises</title>
    <link href="https://home.tigran.manukyan.dev/notes/node-upgrade-under-pm2.html"/>
    <id>https://home.tigran.manukyan.dev/notes/node-upgrade-under-pm2.html</id>
    <published>2026-11-05T12:00:00Z</published>
    <updated>2026-11-05T12:00:00Z</updated>
    <summary>When upgrading Node.js with nvm under pm2, pin nvm alias default to a major version, update the saved PATH in ~/.pm2/dump.pm2 for fork-mode apps, regenerate the pm2 systemd unit with the new node binary, and check readlink /proc/PID/exe before uninstalling the old version.</summary>
    <category term="Node.js"/>
    <category term="nvm"/>
    <category term="pm2"/>
    <category term="systemd"/>
    <category term="Linux"/>
    <content type="html">
&lt;h1&gt;Upgrading Node under pm2 without the surprises&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-11-05 · node, pm2&lt;/p&gt;
&lt;p&gt;A security release came out, so I moved a few pm2 apps to a newer Node with nvm. &lt;code&gt;nvm install&lt;/code&gt; took a minute. Finding every place the old version was still hiding took longer. Here is the order that worked with one restart per app.&lt;/p&gt;
&lt;h2&gt;1. Pin the nvm default to a major version&lt;/h2&gt;
&lt;p&gt;If your default is &lt;code&gt;lts/*&lt;/code&gt;, nvm rewrites that alias every time it fetches the remote list. When a new line goes LTS, &lt;code&gt;default&lt;/code&gt; points at a version you never installed, and new shells start with no &lt;code&gt;node&lt;/code&gt; at all.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nvm install 24 --reinstall-packages-from=22
nvm alias default 24&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--reinstall-packages-from&lt;/code&gt; reinstalls the same pm2 &lt;em&gt;version&lt;/em&gt; you had, not the latest one. That&#x27;s what you want here.&lt;/p&gt;
&lt;h2&gt;2. Fork-mode apps remember the old PATH&lt;/h2&gt;
&lt;p&gt;pm2 saves each app&#x27;s environment in &lt;code&gt;~/.pm2/dump.pm2&lt;/code&gt;. Fork-mode apps spawn &lt;code&gt;node&lt;/code&gt; through the saved &lt;code&gt;PATH&lt;/code&gt;, so after a daemon restart they quietly keep running the old binary. Cluster-mode apps follow the daemon&#x27;s own node.&lt;/p&gt;
&lt;p&gt;Back up the dump, then replace the old version string in &lt;code&gt;PATH&lt;/code&gt;, &lt;code&gt;NVM_BIN&lt;/code&gt;, &lt;code&gt;NVM_INC&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt;. &lt;code&gt;pm2 restart --update-env&lt;/code&gt; also works, but run from a non-interactive SSH command it picks up that shell&#x27;s thinner PATH.&lt;/p&gt;
&lt;h2&gt;3. Regenerate the systemd unit&lt;/h2&gt;
&lt;p&gt;The unit made by &lt;code&gt;pm2 startup&lt;/code&gt; hard-codes the node path. Generate it again with the new binary:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NEW=$HOME/.nvm/versions/node/v24.x.y
sudo env PATH=$NEW/bin:$PATH $NEW/bin/node $NEW/lib/node_modules/pm2/bin/pm2 \
  startup systemd -u $USER --hp $HOME
sudo systemctl daemon-reload
sudo systemctl restart pm2-$USER&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Avoid &lt;code&gt;pm2 update&lt;/code&gt; over SSH for this. It restarts the daemon inside your SSH session&#x27;s scope instead of under systemd.&lt;/p&gt;
&lt;h2&gt;4. Verify, then uninstall&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;for p in $(pgrep -f node); do readlink /proc/$p/exe; done | sort | uniq -c
pm2 save&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only when nothing points at the old version, run &lt;code&gt;nvm uninstall&lt;/code&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Snipping Tool stuck on a busy cursor: a recycled PID</title>
    <link href="https://home.tigran.manukyan.dev/notes/snipping-tool-recycled-pid.html"/>
    <id>https://home.tigran.manukyan.dev/notes/snipping-tool-recycled-pid.html</id>
    <published>2026-11-03T12:00:00Z</published>
    <updated>2026-11-03T12:00:00Z</updated>
    <summary>If Win+Shift+S shows only a busy cursor and SnippingTool.exe processes pile up, run taskkill /f /im SnippingTool.exe. The hang comes from the Windows App SDK AppInstance code treating a recycled PID as alive and leaving a mutex owned.</summary>
    <category term="Windows 11"/>
    <category term="Snipping Tool"/>
    <category term="Windows App SDK"/>
    <category term="debugging"/>
    <content type="html">
&lt;h1&gt;Snipping Tool stuck on a busy cursor: a recycled PID&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-11-03 · windows, debugging&lt;/p&gt;
&lt;p&gt;The screenshot shortcut stopped working. Each press showed a busy cursor for a moment and left one more windowless &lt;code&gt;SnippingTool.exe&lt;/code&gt; behind. By the time I looked, 19 of them were sitting in Task Manager.&lt;/p&gt;
&lt;h2&gt;The fix&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;taskkill /f /im SnippingTool.exe&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All of them have to go. If you kill them one by one, kill the waiting ones before the one holding the lock, or they wake up and each opens an overlay.&lt;/p&gt;
&lt;h2&gt;What was going on&lt;/h2&gt;
&lt;p&gt;Wait Chain Traversal showed every new instance waiting on a mutex owned by one old instance. The rest came from reading &lt;code&gt;AppInstance.cpp&lt;/code&gt; in the open-source Windows App SDK:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instances register their PIDs in a shared memory section. Nobody removes their own PID on exit; the next launch prunes the dead ones.&lt;/li&gt;
&lt;li&gt;The only liveness check is whether &lt;code&gt;OpenProcess(SYNCHRONIZE)&lt;/code&gt; succeeds. If Windows has already given that PID to some unrelated process, the dead entry counts as alive.&lt;/li&gt;
&lt;li&gt;For each listed PID, the launcher creates a &lt;code&gt;&amp;lt;app&amp;gt;_&amp;lt;pid&amp;gt;_Mutex&lt;/code&gt; with initial ownership, assuming the real owner created it first. For a recycled PID, the launcher itself becomes the owner and keeps it until it exits.&lt;/li&gt;
&lt;li&gt;A cancelled snip (press Esc) leaves its process running. If that resident process is the one holding the leaked lock, every later launch waits forever.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In my case a Python process had picked up a PID that Snipping Tool had used an hour earlier. A few hours later one cancelled snip took the lock, and the shortcut died.&lt;/p&gt;
&lt;h2&gt;Useful tools for this kind of hang&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Event 201 in &lt;code&gt;Microsoft-Windows-AppModel-Runtime/Admin&lt;/code&gt; logs each launch. Compare with the timestamps in &lt;code&gt;Pictures\Screenshots&lt;/code&gt; to see which launches produced a file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cdb -pv -p &amp;lt;pid&amp;gt; -c &quot;!handle 0 f Mutant; qd&quot;&lt;/code&gt; from the Windows SDK lists named mutexes without symbols, and doesn&#x27;t stop the process.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Windows CPU % under-counts partial load</title>
    <link href="https://home.tigran.manukyan.dev/notes/windows-cpu-percent-lies.html"/>
    <id>https://home.tigran.manukyan.dev/notes/windows-cpu-percent-lies.html</id>
    <published>2026-10-20T12:00:00Z</published>
    <updated>2026-10-20T12:00:00Z</updated>
    <summary>Windows time-based CPU % (Task Manager, GetThreadTimes, PDH % Processor Time) under-reported a partially loaded 8-core/16-thread desktop CPU by up to about 45% compared with QueryThreadCycleTime. To judge a CPU bottleneck, use GPU utilization or PresentMon GPU Busy instead.</summary>
    <category term="Windows"/>
    <category term="CPU"/>
    <category term="performance"/>
    <category term="Task Manager"/>
    <content type="html">
&lt;h1&gt;Windows CPU % under-counts partial load&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-10-20 · windows, performance&lt;/p&gt;
&lt;p&gt;Games never seemed to push my 8-core CPU past 30% in Task Manager, even when they were clearly CPU-bound. So I wrote a small load generator and compared what Windows reports with what the CPU actually did.&lt;/p&gt;
&lt;h2&gt;The test&lt;/h2&gt;
&lt;p&gt;Start N threads that spin without ever blocking. For each thread, compare two numbers every second:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the time Windows credits it with (&lt;code&gt;GetThreadTimes&lt;/code&gt;, the same source as Task Manager&#x27;s &quot;Processor Time&quot;), and&lt;/li&gt;
&lt;li&gt;the cycles it actually ran (&lt;code&gt;QueryThreadCycleTime&lt;/code&gt;), divided by the clock.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What came out&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;1 busy thread: time accounting credited about 88% of the real run time.&lt;/li&gt;
&lt;li&gt;8 to 12 busy threads: it dropped to about 55%.&lt;/li&gt;
&lt;li&gt;All 16 hardware threads busy: both said 100%.&lt;/li&gt;
&lt;li&gt;The cycle counter said 98 to 99% the whole time, and throughput agreed: 8 threads did 8.2× the work of 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With 8 threads pegged, the system-wide &quot;time&quot; number showed 28.5% while about half the machine was really busy. The other counter, &quot;% Processor Utility&quot;, went the other way and read 80 to 85%, because it scales by boost clock against the base frequency. It also showed about 13% at idle.&lt;/p&gt;
&lt;h2&gt;What I use instead&lt;/h2&gt;
&lt;p&gt;For &quot;am I CPU-bound?&quot; the CPU % is close to useless. GPU usage that sits below its cap, or PresentMon&#x27;s GPU Busy compared to frame time, answers the question directly. I checked one third-party task manager too: it reads the same time-based counters, so it inherits the same under-count.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Certbot renewals that reload nginx on their own</title>
    <link href="https://home.tigran.manukyan.dev/notes/certbot-deploy-hooks.html"/>
    <id>https://home.tigran.manukyan.dev/notes/certbot-deploy-hooks.html</id>
    <published>2026-10-12T12:00:00Z</published>
    <updated>2026-10-12T12:00:00Z</updated>
    <summary>Certificates from certbot certonly renew, but nginx keeps serving the old one until reloaded. Put an executable script in /etc/letsencrypt/renewal-hooks/deploy/ that runs nginx -t &amp;&amp; systemctl reload nginx, and test it with certbot renew --dry-run --run-deploy-hooks.</summary>
    <category term="certbot"/>
    <category term="Let&#x27;s Encrypt"/>
    <category term="nginx"/>
    <category term="TLS"/>
    <content type="html">
&lt;h1&gt;Certbot renewals that reload nginx on their own&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-10-12 · tls, nginx&lt;/p&gt;
&lt;p&gt;Certbot&#x27;s systemd timer renews certificates fine. The catch: if you got the certificate with &lt;code&gt;certonly&lt;/code&gt;, nothing tells nginx to load the new file, and it keeps serving the old one until the next restart. You find out about 90 days later.&lt;/p&gt;
&lt;h2&gt;The fix&lt;/h2&gt;
&lt;p&gt;Drop a script into the deploy hook folder. Certbot runs everything in it after each successful renewal, and only then.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh &amp;lt;&amp;lt;&#x27;SH&#x27;
#!/bin/sh
nginx -t -q &amp;amp;&amp;amp; systemctl reload nginx
SH
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;nginx -t&lt;/code&gt; guard matters: if a broken config is lying around, the reload would fail anyway, and this way the hook doesn&#x27;t make things worse.&lt;/p&gt;
&lt;h2&gt;Check it without waiting three months&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo certbot renew --dry-run --run-deploy-hooks
systemctl list-timers | grep certbot&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Certbot skips deploy hooks on dry runs unless you pass &lt;code&gt;--run-deploy-hooks&lt;/code&gt;, which is why the flag is there.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>An old monitor that forgets to listen</title>
    <link href="https://home.tigran.manukyan.dev/notes/monitor-that-forgets-to-listen.html"/>
    <id>https://home.tigran.manukyan.dev/notes/monitor-that-forgets-to-listen.html</id>
    <published>2026-10-10T12:00:00Z</published>
    <updated>2026-10-10T12:00:00Z</updated>
    <summary>An old DVI monitor that stays black after boot or after a long signal gap can be woken by switching its input over DDC/CI (VCP 0x60) to VGA and back to DVI with NirSoft ControlMyMonitor, run from a scheduled task at logon.</summary>
    <category term="monitor"/>
    <category term="DDC/CI"/>
    <category term="Windows"/>
    <category term="hardware"/>
    <content type="html">
&lt;h1&gt;An old monitor that forgets to listen&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-10-10 · hardware, windows&lt;/p&gt;
&lt;p&gt;My second screen is a 22-inch panel with about 35,000 hours on it, fed over DVI. After every boot it stayed black with the backlight on. Windows saw it, the cable was fine, and unplugging it fixed it. That got old fast.&lt;/p&gt;
&lt;h2&gt;Narrowing it down&lt;/h2&gt;
&lt;p&gt;The monitor answers DDC/CI, so I could ask it what it thought was going on. When stuck, it reported the right input (DVI) and a locked vertical frequency of 0 (VCP &lt;code&gt;AE&lt;/code&gt;), so it simply wasn&#x27;t listening.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reproducible: Win+P &quot;PC screen only&quot; for 40 seconds, then Extend. A one-second gap (Win+Ctrl+Shift+B) does not trigger it.&lt;/li&gt;
&lt;li&gt;Re-selecting DVI does nothing. Restarting the signal from Windows does nothing.&lt;/li&gt;
&lt;li&gt;Switching the input to VGA and back to DVI wakes it up every time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The workaround&lt;/h2&gt;
&lt;p&gt;NirSoft&#x27;s ControlMyMonitor can write VCP codes from the command line. VCP &lt;code&gt;60&lt;/code&gt; is the input source; on this monitor 1 is VGA and 3 is DVI.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ControlMyMonitor.exe /SetValue &quot;\\.\DISPLAY2\Monitor0&quot; 60 1
timeout /t 2
ControlMyMonitor.exe /SetValue &quot;\\.\DISPLAY2\Monitor0&quot; 60 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The monitor ignores writes while it&#x27;s switching, so the real script re-sends until reading &lt;code&gt;60&lt;/code&gt; back confirms the value, then checks that &lt;code&gt;AE&lt;/code&gt; is no longer 0.&lt;/p&gt;
&lt;p&gt;It runs from a scheduled task at logon, with a second trigger a minute later as a safety net. 26 boots in a row came up stuck, and the script woke the screen on the first try every time.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>PowerShell 5.1 traps that only show up in the admin run</title>
    <link href="https://home.tigran.manukyan.dev/notes/powershell-51-traps.html"/>
    <id>https://home.tigran.manukyan.dev/notes/powershell-51-traps.html</id>
    <published>2026-09-23T12:00:00Z</published>
    <updated>2026-09-23T12:00:00Z</updated>
    <summary>Windows PowerShell 5.1 pitfalls: BOM-less UTF-8 scripts are read as ANSI, a failed [xml] cast does not stop the script, $null becomes an empty string in .NET calls, unary-comma returns break pipelines, variable names are case-insensitive, and native exit codes must be checked.</summary>
    <category term="PowerShell"/>
    <category term="Windows"/>
    <category term="scripting"/>
    <content type="html">
&lt;h1&gt;PowerShell 5.1 traps that only show up in the admin run&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-09-23 · windows, powershell&lt;/p&gt;
&lt;p&gt;I wrote a cleanup script and tested it with non-admin dry runs. Every real bug showed up in the elevated run. These are the ones worth remembering.&lt;/p&gt;
&lt;h2&gt;Keep .ps1 files pure ASCII&lt;/h2&gt;
&lt;p&gt;Windows PowerShell 5.1 reads a UTF-8 file without a BOM in the system&#x27;s ANSI code page. One non-ASCII character in a regex turned into three garbage characters and the pattern stopped matching.&lt;/p&gt;
&lt;h2&gt;A failed cast doesn&#x27;t stop the script&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;[xml]$x = $text&lt;/code&gt; on bad input writes an error and carries on with &lt;code&gt;$x&lt;/code&gt; empty. The error message also includes the whole input, which can be megabytes of red text. Use &lt;code&gt;XmlDocument.LoadXml&lt;/code&gt; in &lt;code&gt;try/catch&lt;/code&gt; and truncate the message.&lt;/p&gt;
&lt;h2&gt;$null becomes &quot;&quot; in .NET calls&lt;/h2&gt;
&lt;p&gt;Passing &lt;code&gt;$null&lt;/code&gt; to a .NET &lt;code&gt;string&lt;/code&gt; parameter sends an empty string. When the API really needs NULL, pass &lt;code&gt;[NullString]::Value&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The unary comma and the pipeline&lt;/h2&gt;
&lt;p&gt;A function that ends in &lt;code&gt;return ,$list&lt;/code&gt; sends the list as one object. &lt;code&gt;Get-Things | Where-Object { ... }&lt;/code&gt; then filters the whole array as a single item. Assign to a variable first, then pipe.&lt;/p&gt;
&lt;h2&gt;Variables are case-insensitive&lt;/h2&gt;
&lt;p&gt;A local &lt;code&gt;$folders = @()&lt;/code&gt; inside a function hid the script-level &lt;code&gt;$Folders&lt;/code&gt;. The loop ran over an empty array and the script reported success. Give config lists names that can&#x27;t collide.&lt;/p&gt;
&lt;h2&gt;Guards must fail closed&lt;/h2&gt;
&lt;p&gt;Start with &lt;code&gt;$blocked = $true&lt;/code&gt; and clear it only when the check fully completes. My first version did the opposite, and a parse failure would have let it proceed.&lt;/p&gt;
&lt;h2&gt;Check native exit codes&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pnputil /export-driver&lt;/code&gt; fails if the target folder doesn&#x27;t exist. Piped to &lt;code&gt;Out-Null&lt;/code&gt;, nobody noticed, and the next line deleted the package without a backup. Check &lt;code&gt;$LASTEXITCODE&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; that the file landed.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Restic to a second disk, the boring way</title>
    <link href="https://home.tigran.manukyan.dev/notes/restic-systemd-timer.html"/>
    <id>https://home.tigran.manukyan.dev/notes/restic-systemd-timer.html</id>
    <published>2026-08-30T12:00:00Z</published>
    <updated>2026-08-30T12:00:00Z</updated>
    <summary>Run restic backups from a systemd service and timer (OnCalendar, RandomizedDelaySec, Persistent=true) with restic forget --prune retention, credentials in an EnvironmentFile, and a monthly test restore.</summary>
    <category term="restic"/>
    <category term="backups"/>
    <category term="systemd"/>
    <category term="Linux"/>
    <content type="html">
&lt;h1&gt;Restic to a second disk, the boring way&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-08-30 · backups&lt;/p&gt;
&lt;p&gt;I tried a few backup tools with web UIs, then went back to restic and a systemd timer. The only backup that works is the one that runs without me.&lt;/p&gt;
&lt;h2&gt;Service&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# /etc/systemd/system/restic-backup.service
[Unit]
Description=restic backup

[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
ExecStart=/usr/bin/restic backup /etc /home /srv --exclude-caches
ExecStartPost=/usr/bin/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/etc/restic/env&lt;/code&gt; holds &lt;code&gt;RESTIC_REPOSITORY&lt;/code&gt; and &lt;code&gt;RESTIC_PASSWORD_FILE&lt;/code&gt;. Make it &lt;code&gt;chmod 600&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Timer&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# /etc/systemd/system/restic-backup.timer
[Timer]
OnCalendar=*-*-* 03:30
RandomizedDelaySec=20m
Persistent=true

[Install]
WantedBy=timers.target&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Persistent=true&lt;/code&gt; runs a missed backup at the next boot, which matters for a machine that is sometimes off at 3:30.&lt;/p&gt;
&lt;h2&gt;The part people skip&lt;/h2&gt;
&lt;p&gt;Once a month, restore one random file somewhere else and open it. &lt;code&gt;restic check --read-data-subset=5%&lt;/code&gt; is good too, but it doesn&#x27;t tell you whether you can find the file you actually need.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Bad RAM, one shared DLL, and badmemorylist</title>
    <link href="https://home.tigran.manukyan.dev/notes/bad-ram-badmemorylist.html"/>
    <id>https://home.tigran.manukyan.dev/notes/bad-ram-badmemorylist.html</id>
    <published>2026-08-14T12:00:00Z</published>
    <updated>2026-08-14T12:00:00Z</updated>
    <summary>Random Windows blue screens with no WHEA errors came from one bad chip on non-ECC DDR4. Identical ILLEGAL_INSTRUCTION crashes in one shared DLL across processes pointed to a bad physical page, MemTest86 confirmed it, and bcdedit /set {badmemory} badmemorylist fenced the pages off.</summary>
    <category term="Windows"/>
    <category term="BSOD"/>
    <category term="RAM"/>
    <category term="MemTest86"/>
    <category term="bcdedit"/>
    <content type="html">
&lt;h1&gt;Bad RAM, one shared DLL, and badmemorylist&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-08-14 · windows, hardware&lt;/p&gt;
&lt;p&gt;Two months of random blue screens, each with a different stop code: corrupt PTEs, critical process died, list entry corruption, page faults. Plus a few hard resets with no bugcheck at all. No WHEA errors anywhere.&lt;/p&gt;
&lt;h2&gt;Two wrong assumptions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&quot;No WHEA events, so it isn&#x27;t RAM.&quot;&lt;/strong&gt; Consumer non-ECC memory has no error detection. A flipped bit never reaches WHEA.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Crashes come in bursts, not a gradual curve, so it isn&#x27;t hardware.&quot;&lt;/strong&gt; One bad cell causes a burst the moment a busy page lands on it, and goes quiet when the page moves.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The tell&lt;/h2&gt;
&lt;p&gt;Three unrelated services crashed with &lt;code&gt;ILLEGAL_INSTRUCTION&lt;/code&gt; at the same offset in the same shared DLL, and the DLL on disk hashed clean. Windows maps one physical copy of a DLL&#x27;s code into every process. If that one frame is bad, every process decodes the same garbage. A reboot moved the page and the crashes stopped.&lt;/p&gt;
&lt;h2&gt;Confirming it&lt;/h2&gt;
&lt;p&gt;Windows Memory Diagnostic found errors within the first pass. MemTest86 found 3,278 errors over 4 passes, even at a slow stock speed. Only 12% came from the row-hammer test, so it wasn&#x27;t a borderline timing issue. 148 bad addresses fell into 16 pages in three clusters, and always the same two bits. That&#x27;s one chip.&lt;/p&gt;
&lt;h2&gt;Fencing it off&lt;/h2&gt;
&lt;p&gt;Windows can refuse to use specific page frame numbers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bcdedit /set {badmemory} badmemorylist 0x5EF3E0 0x5EF3E1 ...
bcdedit /enum {badmemory}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I padded each cluster to a 16-page block, 48 pages in all, which costs nothing you&#x27;d notice. Two things to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/set&lt;/code&gt; replaces the whole list. To add pages later, repeat all the old ones.&lt;/li&gt;
&lt;li&gt;Write down where the clusters are. If a later crash lands outside them, the chip is getting worse and the list isn&#x27;t enough. Mine did, two months later, so a new stick is on the list.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>The editor update that deletes the editor</title>
    <link href="https://home.tigran.manukyan.dev/notes/editor-update-deletes-editor.html"/>
    <id>https://home.tigran.manukyan.dev/notes/editor-update-deletes-editor.html</id>
    <published>2026-07-27T12:00:00Z</published>
    <updated>2026-07-27T12:00:00Z</updated>
    <summary>Cursor and other VS Code-based editors on Windows can delete themselves during an update when a stdio MCP server still runs the editor&#x27;s bundled node.exe. Recover by moving the staged build out of the _ folder, and kill those processes before updating.</summary>
    <category term="Cursor"/>
    <category term="VS Code"/>
    <category term="MCP"/>
    <category term="Windows"/>
    <content type="html">
&lt;h1&gt;The editor update that deletes the editor&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-07-27 · windows, tooling&lt;/p&gt;
&lt;p&gt;Twice this month an auto-update on Windows left me with no editor at all. The shortcut pointed at nothing, and the update log claimed success.&lt;/p&gt;
&lt;h2&gt;What happens&lt;/h2&gt;
&lt;p&gt;The editor (Cursor, but the updater comes from VS Code) stages the new build in a &lt;code&gt;_&lt;/code&gt; subfolder. The updater then deletes the old install &lt;em&gt;before&lt;/em&gt; moving the new one into place. Before it starts, it only checks whether the main &lt;code&gt;.exe&lt;/code&gt; is still running.&lt;/p&gt;
&lt;p&gt;The editor puts its own bundled &lt;code&gt;node.exe&lt;/code&gt; first on PATH when it starts stdio MCP servers. So &lt;code&gt;npx some-mcp-server&lt;/code&gt; runs on the editor&#x27;s node, and that process keeps running after the window closes. The delete hits a locked file, gives up after retries, and leaves a half-deleted install with the new build stranded in &lt;code&gt;_&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Recovering&lt;/h2&gt;
&lt;p&gt;No reinstall needed. Delete the gutted &lt;code&gt;resources&lt;/code&gt; folder, move everything from &lt;code&gt;_&lt;/code&gt; up one level, delete &lt;code&gt;_&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Preventing it&lt;/h2&gt;
&lt;p&gt;I turned off auto-update (&lt;code&gt;&quot;update.mode&quot;: &quot;none&quot;&lt;/code&gt;) and check for stragglers before updating by hand:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Get-Process | Where-Object { $_.Path -like &#x27;*\cursor\resources*&#x27; }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Turning the server off in the editor&#x27;s MCP settings is not enough. One server&#x27;s child processes survived the toggle. Trust the process list, not the UI. HTTP-based MCP servers don&#x27;t spawn anything local, so only stdio ones matter.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Folders called &quot;cache&quot; aren&#x27;t always cache</title>
    <link href="https://home.tigran.manukyan.dev/notes/cache-folders-are-not-always-cache.html"/>
    <id>https://home.tigran.manukyan.dev/notes/cache-folders-are-not-always-cache.html</id>
    <published>2026-07-09T12:00:00Z</published>
    <updated>2026-07-09T12:00:00Z</updated>
    <summary>Folders named cache or depots inside an app&#x27;s data directory can hold the installed build: deleting C:\ProgramData\LGHUB\depots broke Logitech G HUB. Check the app&#x27;s manifests before deleting, and prefer the app&#x27;s own cleanup.</summary>
    <category term="Windows"/>
    <category term="disk cleanup"/>
    <category term="Logitech G HUB"/>
    <content type="html">
&lt;h1&gt;Folders called &quot;cache&quot; aren&#x27;t always cache&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-07-09 · windows, housekeeping&lt;/p&gt;
&lt;p&gt;The system SSD was nearly full, so I went through &lt;code&gt;C:\ProgramData&lt;/code&gt; looking for big folders that sounded disposable. Logitech G HUB had gigabytes in &lt;code&gt;depots&lt;/code&gt; and &lt;code&gt;cache&lt;/code&gt;. They sounded like old downloads, so I deleted them. G HUB stopped working.&lt;/p&gt;
&lt;h2&gt;What &lt;code&gt;depots&lt;/code&gt; really was&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;current.json&lt;/code&gt; next to it listed the depot of the installed build with state &lt;code&gt;&quot;PRESENT&quot;&lt;/code&gt;. The app runs out of that folder and checks it. It held the current version, not leftovers. A reinstall over the top fixed it.&lt;/p&gt;
&lt;h2&gt;The rule I use now&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Inside an app&#x27;s own data folder, a name like cache, depots or downloads proves nothing. Look for manifests or version files that reference the folder first.&lt;/li&gt;
&lt;li&gt;Prefer the app&#x27;s own cleanup option or its uninstaller.&lt;/li&gt;
&lt;li&gt;If you can&#x27;t tell, file it under &quot;might need a reinstall&quot;, not &quot;safe&quot;.&lt;/li&gt;
&lt;li&gt;Generic caches are still fair game: npm, pip, shader caches, &lt;code&gt;%TEMP%&lt;/code&gt;. Those rebuild themselves by design.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Measure your DNS resolvers before picking one</title>
    <link href="https://home.tigran.manukyan.dev/notes/measuring-dns-resolvers.html"/>
    <id>https://home.tigran.manukyan.dev/notes/measuring-dns-resolvers.html</id>
    <published>2026-06-21T12:00:00Z</published>
    <updated>2026-06-21T12:00:00Z</updated>
    <summary>Public DNS resolver speed depends on your ISP&#x27;s routing. Benchmark 1.1.1.1, 8.8.8.8 and 9.9.9.9 from your own network with dig and random uncached names, and compare median and worst-case latency.</summary>
    <category term="DNS"/>
    <category term="networking"/>
    <category term="dig"/>
    <content type="html">
&lt;h1&gt;Measure your DNS resolvers before picking one&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-06-21 · networking&lt;/p&gt;
&lt;p&gt;Every &quot;fastest DNS&quot; list was measured from somebody else&#x27;s network. Anycast routing depends on your ISP&#x27;s peering, so the resolver that is 5 ms away for them can be 60 ms away for you.&lt;/p&gt;
&lt;h2&gt;A quick loop&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;for r in 1.1.1.1 8.8.8.8 9.9.9.9; do
  printf &#x27;%-10s&#x27; &quot;$r&quot;
  for i in 1 2 3 4 5; do
    dig @&quot;$r&quot; &quot;example$RANDOM.com&quot; +tries=1 +time=2 | awk &#x27;/Query time/{printf &quot;%5s ms&quot;, $4}&#x27;
  done
  echo
done&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The random name skips the resolver&#x27;s cache, so you measure the full lookup, not a cache hit. Run it at a few times of day. Evening numbers are usually the honest ones.&lt;/p&gt;
&lt;h2&gt;What to look at&lt;/h2&gt;
&lt;p&gt;Look at the median and the worst result, not the best one. A resolver that holds steady at 8 ms beats one that swings between 3 and 90 ms.&lt;/p&gt;
</content>
  </entry>
</feed>
