<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ru">
  <title>~/home</title>
  <subtitle>Практические заметки из маленького хоумлаба: Linux-серверы, nginx, certbot, Node.js под pm2, бэкапы restic, DNS и отладка Windows (падения, битая память, метрики CPU, PowerShell).</subtitle>
  <link href="https://home.tigran.manukyan.dev/ru/feed.xml" rel="self"/>
  <link href="https://home.tigran.manukyan.dev/ru/"/>
  <id>https://home.tigran.manukyan.dev/ru/</id>
  <updated>2026-11-05T12:00:00Z</updated>
  <author><name>~/home</name></author>
  <entry>
    <title>Обновление Node под pm2 без сюрпризов</title>
    <link href="https://home.tigran.manukyan.dev/ru/notes/node-upgrade-under-pm2.html"/>
    <id>https://home.tigran.manukyan.dev/ru/notes/node-upgrade-under-pm2.html</id>
    <published>2026-11-05T12:00:00Z</published>
    <updated>2026-11-05T12:00:00Z</updated>
    <summary>При обновлении Node.js через nvm под pm2: привяжите nvm alias default к мажорной версии, поправьте сохранённый PATH в ~/.pm2/dump.pm2 для приложений в fork mode, перегенерируйте systemd-юнит pm2 с новым бинарником node и проверьте readlink /proc/PID/exe, прежде чем удалять старую версию.</summary>
    <category term="Node.js"/>
    <category term="nvm"/>
    <category term="pm2"/>
    <category term="systemd"/>
    <category term="Linux"/>
    <content type="html">
&lt;h1&gt;Обновление Node под pm2 без сюрпризов&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-11-05 · node, pm2&lt;/p&gt;
&lt;p&gt;Вышел security-релиз, и я перевёл несколько pm2-приложений на свежую версию Node через nvm. Сам &lt;code&gt;nvm install&lt;/code&gt; занял минуту. А вот найти все места, где пряталась старая версия, вышло заметно дольше. Ниже порядок, который сработал с одним рестартом на каждое приложение.&lt;/p&gt;
&lt;h2&gt;1. Привяжите default в nvm к мажорной версии&lt;/h2&gt;
&lt;p&gt;Если &lt;code&gt;default&lt;/code&gt; у вас указывает на &lt;code&gt;lts/*&lt;/code&gt;, nvm переписывает этот алиас при каждом запросе списка версий. Как только новая ветка становится LTS, &lt;code&gt;default&lt;/code&gt; начинает указывать на версию, которую вы не ставили, и новые шеллы стартуют вообще без &lt;code&gt;node&lt;/code&gt;.&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; ставит ту же &lt;em&gt;версию&lt;/em&gt; pm2, что была, а не последнюю. Здесь это ровно то, что нужно.&lt;/p&gt;
&lt;h2&gt;2. Приложения в fork mode помнят старый PATH&lt;/h2&gt;
&lt;p&gt;pm2 сохраняет окружение каждого приложения в &lt;code&gt;~/.pm2/dump.pm2&lt;/code&gt;. Приложения в fork mode запускают &lt;code&gt;node&lt;/code&gt; через сохранённый &lt;code&gt;PATH&lt;/code&gt;, поэтому после рестарта демона они тихо продолжают работать на старом бинарнике. Приложения в cluster mode используют node самого демона.&lt;/p&gt;
&lt;p&gt;Сделайте бэкап дампа и замените старую версию в &lt;code&gt;PATH&lt;/code&gt;, &lt;code&gt;NVM_BIN&lt;/code&gt;, &lt;code&gt;NVM_INC&lt;/code&gt; и &lt;code&gt;_&lt;/code&gt;. &lt;code&gt;pm2 restart --update-env&lt;/code&gt; тоже подойдёт, но если запускать его неинтерактивной командой по SSH, он подхватит урезанный PATH этого шелла.&lt;/p&gt;
&lt;h2&gt;3. Перегенерируйте systemd-юнит&lt;/h2&gt;
&lt;p&gt;В юните, который создаёт &lt;code&gt;pm2 startup&lt;/code&gt;, путь к node зашит намертво. Сгенерируйте его заново с новым бинарником:&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;&lt;code&gt;pm2 update&lt;/code&gt; по SSH для этого не подходит: он перезапустит демон внутри scope вашей SSH-сессии, а не под systemd.&lt;/p&gt;
&lt;h2&gt;4. Проверьте и только потом удаляйте&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;&lt;code&gt;nvm uninstall&lt;/code&gt; — только когда на старую версию больше ничего не ссылается.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Snipping Tool висит на курсоре загрузки: виноват переиспользованный PID</title>
    <link href="https://home.tigran.manukyan.dev/ru/notes/snipping-tool-recycled-pid.html"/>
    <id>https://home.tigran.manukyan.dev/ru/notes/snipping-tool-recycled-pid.html</id>
    <published>2026-11-03T12:00:00Z</published>
    <updated>2026-11-03T12:00:00Z</updated>
    <summary>Если по Win+Shift+S появляется только курсор загрузки, а процессы SnippingTool.exe копятся, помогает taskkill /f /im SnippingTool.exe. Причина в коде AppInstance из Windows App SDK: он считает переиспользованный PID живым и оставляет мьютекс захваченным.</summary>
    <category term="Windows 11"/>
    <category term="Snipping Tool"/>
    <category term="Windows App SDK"/>
    <category term="отладка"/>
    <content type="html">
&lt;h1&gt;Snipping Tool висит на курсоре загрузки: виноват переиспользованный PID&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-11-03 · windows, отладка&lt;/p&gt;
&lt;p&gt;Перестал работать хоткей для скриншотов. Каждое нажатие на секунду показывало курсор загрузки и оставляло ещё один &lt;code&gt;SnippingTool.exe&lt;/code&gt; без окна. Когда я наконец заглянул в диспетчер задач, их там висело 19.&lt;/p&gt;
&lt;h2&gt;Решение&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;taskkill /f /im SnippingTool.exe&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Убивать нужно все. Если по одному, то сначала прибейте ждущие, а потом тот, что держит лок, иначе ждущие проснутся и каждый откроет свой оверлей.&lt;/p&gt;
&lt;h2&gt;Что происходило&lt;/h2&gt;
&lt;p&gt;Wait Chain Traversal показал, что каждый новый экземпляр ждёт мьютекс, которым владеет один старый. Остальное прояснилось после чтения &lt;code&gt;AppInstance.cpp&lt;/code&gt; в открытом Windows App SDK:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Экземпляры регистрируют свои PID в общей секции памяти. При выходе свой PID никто не убирает, мёртвые записи вычищает следующий запуск.&lt;/li&gt;
&lt;li&gt;Единственная проверка «жив ли процесс» — удался ли &lt;code&gt;OpenProcess(SYNCHRONIZE)&lt;/code&gt;. Если Windows уже отдала этот PID другому, постороннему процессу, мёртвая запись считается живой.&lt;/li&gt;
&lt;li&gt;Для каждого PID из списка новый экземпляр создаёт &lt;code&gt;&amp;lt;app&amp;gt;_&amp;lt;pid&amp;gt;_Mutex&lt;/code&gt; с начальным владением, рассчитывая, что настоящий владелец создал его раньше. Если PID переиспользован, владельцем становится сам новый экземпляр и держит мьютекс до своего выхода.&lt;/li&gt;
&lt;li&gt;Отменённый скриншот (Esc) оставляет процесс висеть в памяти. Если именно он держит утёкший лок, все следующие запуски ждут вечно.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;У меня PID, который Snipping Tool использовал часом раньше, достался процессу Python. Через несколько часов один отменённый скриншот захватил лок, и хоткей умер.&lt;/p&gt;
&lt;h2&gt;Что пригодится при таких зависаниях&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Событие 201 в журнале &lt;code&gt;Microsoft-Windows-AppModel-Runtime/Admin&lt;/code&gt; фиксирует каждый запуск. Сравните со временем файлов в &lt;code&gt;Pictures\Screenshots&lt;/code&gt; и увидите, какие запуски дали скриншот.&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; из Windows SDK показывает именованные мьютексы без символов и не останавливает процесс.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Процент загрузки CPU в Windows врёт при частичной нагрузке</title>
    <link href="https://home.tigran.manukyan.dev/ru/notes/windows-cpu-percent-lies.html"/>
    <id>https://home.tigran.manukyan.dev/ru/notes/windows-cpu-percent-lies.html</id>
    <published>2026-10-20T12:00:00Z</published>
    <updated>2026-10-20T12:00:00Z</updated>
    <summary>Учёт загрузки по времени в Windows (диспетчер задач, GetThreadTimes, PDH % Processor Time) на частично нагруженном 8-ядерном 16-поточном CPU показывал до ~45% меньше, чем QueryThreadCycleTime. Чтобы понять, упираетесь ли вы в CPU, смотрите на загрузку GPU или GPU Busy в PresentMon.</summary>
    <category term="Windows"/>
    <category term="CPU"/>
    <category term="производительность"/>
    <category term="диспетчер задач"/>
    <content type="html">
&lt;h1&gt;Процент загрузки CPU в Windows врёт при частичной нагрузке&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-10-20 · windows, производительность&lt;/p&gt;
&lt;p&gt;В диспетчере задач игры ни разу не грузили мой 8-ядерный CPU больше чем на 30%, даже когда явно упирались в процессор. Я написал небольшой генератор нагрузки и сравнил то, что показывает Windows, с тем, что процессор делал на самом деле.&lt;/p&gt;
&lt;h2&gt;Тест&lt;/h2&gt;
&lt;p&gt;Запускаем N потоков, которые крутятся без единой блокировки. Для каждого потока раз в секунду сравниваем два числа:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;время, которое ему засчитала Windows (&lt;code&gt;GetThreadTimes&lt;/code&gt;, тот же источник, что использует диспетчер задач), и&lt;/li&gt;
&lt;li&gt;реально отработанные такты (&lt;code&gt;QueryThreadCycleTime&lt;/code&gt;), делённые на частоту.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Что получилось&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;1 занятый поток: учёт по времени засчитал около 88% реального времени работы.&lt;/li&gt;
&lt;li&gt;8–12 потоков: падает примерно до 55%.&lt;/li&gt;
&lt;li&gt;Заняты все 16 аппаратных потоков: оба способа показывают 100%.&lt;/li&gt;
&lt;li&gt;Счётчик тактов всё время показывал 98–99%, и реальная производительность с ним согласна: 8 потоков сделали в 8,2 раза больше работы, чем один.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;При 8 полностью загруженных потоках общесистемный «временной» показатель был 28,5%, хотя реально была занята примерно половина машины. Другой счётчик, «% Processor Utility», ошибается в обратную сторону и показывает 80–85%, потому что масштабирует результат по частоте буста относительно базовой. Причём в простое он показывал около 13%.&lt;/p&gt;
&lt;h2&gt;Что я смотрю вместо этого&lt;/h2&gt;
&lt;p&gt;На вопрос «упираюсь ли я в CPU?» процент загрузки CPU почти не отвечает. Загрузка GPU ниже потолка или GPU Busy из PresentMon в сравнении со временем кадра отвечают на него напрямую. Заодно проверил один сторонний диспетчер задач: он читает те же временные счётчики, так что и занижает так же.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Certbot продлил сертификат, а nginx отдаёт старый</title>
    <link href="https://home.tigran.manukyan.dev/ru/notes/certbot-deploy-hooks.html"/>
    <id>https://home.tigran.manukyan.dev/ru/notes/certbot-deploy-hooks.html</id>
    <published>2026-10-12T12:00:00Z</published>
    <updated>2026-10-12T12:00:00Z</updated>
    <summary>Сертификаты, выпущенные через certbot certonly, продлеваются, но nginx отдаёт старый, пока его не перезагрузить. Решение — исполняемый скрипт в /etc/letsencrypt/renewal-hooks/deploy/ с nginx -t &amp;&amp; systemctl reload nginx, проверка — 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 продлил сертификат, а nginx отдаёт старый&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-10-12 · tls, nginx&lt;/p&gt;
&lt;p&gt;Таймер certbot в systemd исправно продлевает сертификаты. Подвох в другом: если сертификат получен через &lt;code&gt;certonly&lt;/code&gt;, nginx никто не просит перечитать новый файл, и он отдаёт старый до следующего рестарта. Узнаёшь об этом примерно через 90 дней.&lt;/p&gt;
&lt;h2&gt;Решение&lt;/h2&gt;
&lt;p&gt;Кладём скрипт в папку deploy-хуков. Certbot запускает всё, что в ней лежит, после каждого успешного продления, и только тогда.&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;Проверка &lt;code&gt;nginx -t&lt;/code&gt; здесь не для красоты: если где-то лежит сломанный конфиг, reload всё равно упадёт, а так хук хотя бы не сделает хуже.&lt;/p&gt;
&lt;h2&gt;Как проверить, не дожидаясь трёх месяцев&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;При dry run certbot пропускает deploy-хуки, если не передать &lt;code&gt;--run-deploy-hooks&lt;/code&gt;, поэтому флаг и нужен.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Старый монитор, который перестаёт ловить сигнал</title>
    <link href="https://home.tigran.manukyan.dev/ru/notes/monitor-that-forgets-to-listen.html"/>
    <id>https://home.tigran.manukyan.dev/ru/notes/monitor-that-forgets-to-listen.html</id>
    <published>2026-10-10T12:00:00Z</published>
    <updated>2026-10-10T12:00:00Z</updated>
    <summary>Старый DVI-монитор, который остаётся чёрным после загрузки или долгого пропадания сигнала, можно разбудить, переключив вход по DDC/CI (VCP 0x60) на VGA и обратно на DVI утилитой NirSoft ControlMyMonitor из задачи планировщика при входе в систему.</summary>
    <category term="монитор"/>
    <category term="DDC/CI"/>
    <category term="Windows"/>
    <category term="железо"/>
    <content type="html">
&lt;h1&gt;Старый монитор, который перестаёт ловить сигнал&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-10-10 · железо, windows&lt;/p&gt;
&lt;p&gt;Мой второй экран — 22-дюймовая панель с наработкой около 35&amp;nbsp;000 часов, подключённая по DVI. После каждой загрузки он оставался чёрным с включённой подсветкой. Windows его видела, кабель был в порядке, переподключение помогало. Надоело это очень быстро.&lt;/p&gt;
&lt;h2&gt;Сужаем круг&lt;/h2&gt;
&lt;p&gt;Монитор отвечает по DDC/CI, так что можно спросить у него самого, что происходит. В зависшем состоянии он сообщал правильный вход (DVI) и частоту вертикальной развёртки 0 (VCP &lt;code&gt;AE&lt;/code&gt;), то есть просто не слушал сигнал.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Воспроизводится так: Win+P → «PC screen only» на 40 секунд, потом «Extend». Разрыв в секунду (Win+Ctrl+Shift+B) проблему не вызывает.&lt;/li&gt;
&lt;li&gt;Повторный выбор DVI ничего не даёт. Перезапуск сигнала со стороны Windows — тоже.&lt;/li&gt;
&lt;li&gt;Переключение входа на VGA и обратно на DVI будит его каждый раз.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Обходное решение&lt;/h2&gt;
&lt;p&gt;ControlMyMonitor от NirSoft умеет писать VCP-коды из командной строки. VCP &lt;code&gt;60&lt;/code&gt; — источник сигнала; у этого монитора 1 — VGA, 3 — 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;Пока монитор переключается, он игнорирует запись, поэтому настоящий скрипт повторяет команду, пока чтение &lt;code&gt;60&lt;/code&gt; не подтвердит значение, а потом проверяет, что &lt;code&gt;AE&lt;/code&gt; больше не 0.&lt;/p&gt;
&lt;p&gt;Скрипт запускается задачей планировщика при входе в систему, плюс второй триггер через минуту — на всякий случай. 26 загрузок подряд монитор зависал, и каждый раз скрипт будил его с первой попытки.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Ловушки PowerShell 5.1, которые вылезают только при запуске от админа</title>
    <link href="https://home.tigran.manukyan.dev/ru/notes/powershell-51-traps.html"/>
    <id>https://home.tigran.manukyan.dev/ru/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: UTF-8 без BOM читается как ANSI, неудачный каст [xml] не останавливает скрипт, $null превращается в пустую строку в .NET-вызовах, возврат через унарную запятую ломает пайплайн, имена переменных не различают регистр, а коды выхода внешних программ надо проверять.</summary>
    <category term="PowerShell"/>
    <category term="Windows"/>
    <category term="скрипты"/>
    <content type="html">
&lt;h1&gt;Ловушки PowerShell 5.1, которые вылезают только при запуске от админа&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-09-23 · windows, powershell&lt;/p&gt;
&lt;p&gt;Написал скрипт для чистки системы и гонял его в режиме dry run без прав администратора. Все настоящие баги вылезли при запуске от админа. Вот те, что стоит запомнить.&lt;/p&gt;
&lt;h2&gt;Держите .ps1 в чистом ASCII&lt;/h2&gt;
&lt;p&gt;Windows PowerShell 5.1 читает UTF-8 без BOM в системной ANSI-кодировке (на русской Windows это cp1251). Один не-ASCII символ в регулярке превратился в кракозябры, и шаблон перестал совпадать.&lt;/p&gt;
&lt;h2&gt;Неудачный каст не останавливает скрипт&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;[xml]$x = $text&lt;/code&gt; на кривых данных пишет ошибку и спокойно идёт дальше с пустым &lt;code&gt;$x&lt;/code&gt;. Причём в текст ошибки попадает весь вход, а это могут быть мегабайты красного текста. Используйте &lt;code&gt;XmlDocument.LoadXml&lt;/code&gt; в &lt;code&gt;try/catch&lt;/code&gt; и обрезайте сообщение.&lt;/p&gt;
&lt;h2&gt;$null превращается в &quot;&quot; в .NET-вызовах&lt;/h2&gt;
&lt;p&gt;Если передать &lt;code&gt;$null&lt;/code&gt; в .NET-параметр типа &lt;code&gt;string&lt;/code&gt;, уйдёт пустая строка. Когда API действительно нужен NULL, передавайте &lt;code&gt;[NullString]::Value&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Унарная запятая и пайплайн&lt;/h2&gt;
&lt;p&gt;Функция, которая заканчивается на &lt;code&gt;return ,$list&lt;/code&gt;, отдаёт список одним объектом. Тогда &lt;code&gt;Get-Things | Where-Object { ... }&lt;/code&gt; фильтрует весь массив как один элемент. Сначала присвойте результат переменной, а уже потом отправляйте в пайплайн.&lt;/p&gt;
&lt;h2&gt;Имена переменных не различают регистр&lt;/h2&gt;
&lt;p&gt;Локальная &lt;code&gt;$folders = @()&lt;/code&gt; внутри функции перекрыла &lt;code&gt;$Folders&lt;/code&gt; уровня скрипта. Цикл прошёл по пустому массиву, и скрипт отчитался об успехе. Давайте спискам из конфига имена, которые ни с чем не пересекутся.&lt;/p&gt;
&lt;h2&gt;Защитные проверки должны быть fail-closed&lt;/h2&gt;
&lt;p&gt;Начинайте с &lt;code&gt;$blocked = $true&lt;/code&gt; и сбрасывайте флаг, только когда проверка полностью прошла. Моя первая версия делала наоборот, и ошибка парсинга пропустила бы скрипт дальше.&lt;/p&gt;
&lt;h2&gt;Проверяйте коды выхода внешних программ&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pnputil /export-driver&lt;/code&gt; падает, если целевой папки нет. Вывод уходил в &lt;code&gt;Out-Null&lt;/code&gt;, так что этого никто не заметил, и следующая строка удалила пакет без бэкапа. Проверяйте &lt;code&gt;$LASTEXITCODE&lt;/code&gt; &lt;em&gt;и&lt;/em&gt; то, что файл действительно появился.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Restic на второй диск: скучно, зато работает</title>
    <link href="https://home.tigran.manukyan.dev/ru/notes/restic-systemd-timer.html"/>
    <id>https://home.tigran.manukyan.dev/ru/notes/restic-systemd-timer.html</id>
    <published>2026-08-30T12:00:00Z</published>
    <updated>2026-08-30T12:00:00Z</updated>
    <summary>Бэкапы restic по systemd-таймеру (OnCalendar, RandomizedDelaySec, Persistent=true), очистка старых снапшотов через restic forget --prune, репозиторий и пароль в EnvironmentFile и пробное восстановление раз в месяц.</summary>
    <category term="restic"/>
    <category term="бэкапы"/>
    <category term="systemd"/>
    <category term="Linux"/>
    <content type="html">
&lt;h1&gt;Restic на второй диск: скучно, зато работает&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-08-30 · бэкапы&lt;/p&gt;
&lt;p&gt;Перепробовал несколько инструментов для бэкапов с веб-интерфейсом и в итоге вернулся к restic и systemd-таймеру. Работает только тот бэкап, который запускается без моего участия.&lt;/p&gt;
&lt;h2&gt;Сервис&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; лежат &lt;code&gt;RESTIC_REPOSITORY&lt;/code&gt; и &lt;code&gt;RESTIC_PASSWORD_FILE&lt;/code&gt;. Права на файл — &lt;code&gt;chmod 600&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Таймер&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; запускает пропущенный бэкап при следующей загрузке. Это важно для машины, которая в 3:30 бывает выключена.&lt;/p&gt;
&lt;h2&gt;То, что обычно пропускают&lt;/h2&gt;
&lt;p&gt;Раз в месяц стоит восстановить случайный файл куда-нибудь в сторону и открыть его. &lt;code&gt;restic check --read-data-subset=5%&lt;/code&gt; тоже полезен, но он не покажет, получится ли найти нужный файл, когда он действительно понадобится.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Битая память, одна общая DLL и badmemorylist</title>
    <link href="https://home.tigran.manukyan.dev/ru/notes/bad-ram-badmemorylist.html"/>
    <id>https://home.tigran.manukyan.dev/ru/notes/bad-ram-badmemorylist.html</id>
    <published>2026-08-14T12:00:00Z</published>
    <updated>2026-08-14T12:00:00Z</updated>
    <summary>Случайные синие экраны Windows без единой ошибки WHEA оказались одним битым чипом на DDR4 без ECC. Одинаковые падения ILLEGAL_INSTRUCTION в одной общей DLL в разных процессах указали на плохую физическую страницу, MemTest86 это подтвердил, а bcdedit /set {badmemory} badmemorylist исключил эти страницы.</summary>
    <category term="Windows"/>
    <category term="BSOD"/>
    <category term="оперативная память"/>
    <category term="MemTest86"/>
    <category term="bcdedit"/>
    <content type="html">
&lt;h1&gt;Битая память, одна общая DLL и badmemorylist&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-08-14 · windows, железо&lt;/p&gt;
&lt;p&gt;Два месяца случайных синих экранов, и у каждого свой stop-код: битые PTE, CRITICAL_PROCESS_DIED, порча LIST_ENTRY, page fault&#x27;ы. Плюс несколько жёстких перезагрузок вообще без bugcheck. И ни одной ошибки WHEA.&lt;/p&gt;
&lt;h2&gt;Два неверных предположения&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;«Событий WHEA нет — значит, не память».&lt;/strong&gt; У обычной памяти без ECC нет обнаружения ошибок. Перевёрнутый бит до WHEA просто не доходит.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;«Падения идут пачками, а не нарастают постепенно — значит, не железо».&lt;/strong&gt; Одна битая ячейка даёт пачку падений, как только на неё попадает активно используемая страница, и затихает, когда страница переезжает.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Что выдало проблему&lt;/h2&gt;
&lt;p&gt;Три не связанных между собой сервиса упали с &lt;code&gt;ILLEGAL_INSTRUCTION&lt;/code&gt; по одному и тому же смещению в одной и той же общей DLL, а хэш DLL на диске сходился. Windows отображает одну физическую копию кода DLL во все процессы. Если этот фрейм битый, каждый процесс декодирует один и тот же мусор. После перезагрузки страница переехала, и падения прекратились.&lt;/p&gt;
&lt;h2&gt;Подтверждение&lt;/h2&gt;
&lt;p&gt;Windows Memory Diagnostic (mdsched) нашёл ошибки уже на первом проходе. MemTest86 насчитал 3278 ошибок за 4 прохода даже на медленной штатной частоте. На тест row hammer пришлось только 12%, так что дело не в пограничных таймингах. 148 битых адресов легли в 16 страниц тремя кластерами, и сбоили всегда одни и те же два бита. Это один чип.&lt;/p&gt;
&lt;h2&gt;Огораживаем битые страницы&lt;/h2&gt;
&lt;p&gt;Windows умеет не использовать конкретные номера физических страниц (PFN):&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;Каждый кластер я расширил до блока в 16 страниц, всего вышло 48. Потеря, которую не заметишь. Две вещи, которые надо знать:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/set&lt;/code&gt; заменяет список целиком. Чтобы потом добавить страницы, заново перечислите и все старые.&lt;/li&gt;
&lt;li&gt;Запишите, где лежат кластеры. Если следующее падение придётся на адрес вне их, чип деградирует, и одного списка уже мало. У меня так и вышло через два месяца, так что новая планка уже в списке покупок.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Обновление редактора, которое удаляет редактор</title>
    <link href="https://home.tigran.manukyan.dev/ru/notes/editor-update-deletes-editor.html"/>
    <id>https://home.tigran.manukyan.dev/ru/notes/editor-update-deletes-editor.html</id>
    <published>2026-07-27T12:00:00Z</published>
    <updated>2026-07-27T12:00:00Z</updated>
    <summary>Cursor и другие редакторы на базе VS Code под Windows могут удалить сами себя при обновлении, если stdio MCP-сервер ещё работает на встроенном в редактор node.exe. Чтобы восстановиться, перенесите подготовленную сборку из папки _, а перед обновлением убивайте эти процессы.</summary>
    <category term="Cursor"/>
    <category term="VS Code"/>
    <category term="MCP"/>
    <category term="Windows"/>
    <content type="html">
&lt;h1&gt;Обновление редактора, которое удаляет редактор&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-07-27 · windows, инструменты&lt;/p&gt;
&lt;p&gt;Дважды за месяц автообновление под Windows оставило меня вообще без редактора. Ярлык вёл в пустоту, а лог обновления рапортовал об успехе.&lt;/p&gt;
&lt;h2&gt;Что происходит&lt;/h2&gt;
&lt;p&gt;Редактор (Cursor, но апдейтер достался ему от VS Code) складывает новую сборку в подпапку &lt;code&gt;_&lt;/code&gt;. Затем апдейтер удаляет старую установку &lt;em&gt;до того&lt;/em&gt;, как переносит новую на место. А перед началом проверяет только, не запущен ли основной &lt;code&gt;.exe&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;При запуске stdio MCP-серверов редактор ставит свой встроенный &lt;code&gt;node.exe&lt;/code&gt; первым в PATH. Поэтому &lt;code&gt;npx some-mcp-server&lt;/code&gt; работает на node редактора, и этот процесс живёт и после закрытия окна. Удаление натыкается на заблокированный файл, после нескольких попыток сдаётся и оставляет наполовину удалённую установку, а новая сборка так и лежит в &lt;code&gt;_&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Как восстановить&lt;/h2&gt;
&lt;p&gt;Переустанавливать не нужно. Удалите выпотрошенную папку &lt;code&gt;resources&lt;/code&gt;, перенесите всё из &lt;code&gt;_&lt;/code&gt; на уровень выше и удалите &lt;code&gt;_&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Как не допустить&lt;/h2&gt;
&lt;p&gt;Я отключил автообновление (&lt;code&gt;&quot;update.mode&quot;: &quot;none&quot;&lt;/code&gt;) и перед ручным обновлением проверяю, не остались ли висеть процессы:&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;Выключить сервер в настройках MCP недостаточно: у одного сервера дочерние процессы пережили выключение. Верьте списку процессов, а не интерфейсу. MCP-серверы по HTTP локально ничего не запускают, так что важны только stdio.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Папка cache — не всегда кэш</title>
    <link href="https://home.tigran.manukyan.dev/ru/notes/cache-folders-are-not-always-cache.html"/>
    <id>https://home.tigran.manukyan.dev/ru/notes/cache-folders-are-not-always-cache.html</id>
    <published>2026-07-09T12:00:00Z</published>
    <updated>2026-07-09T12:00:00Z</updated>
    <summary>Папки cache или depots внутри данных приложения могут хранить установленную сборку: удаление C:\ProgramData\LGHUB\depots сломало Logitech G HUB. Перед удалением проверьте манифесты приложения и по возможности пользуйтесь его собственной очисткой.</summary>
    <category term="Windows"/>
    <category term="очистка диска"/>
    <category term="Logitech G HUB"/>
    <content type="html">
&lt;h1&gt;Папка cache — не всегда кэш&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-07-09 · windows, уборка&lt;/p&gt;
&lt;p&gt;Системный SSD почти забился, и я пошёл по &lt;code&gt;C:\ProgramData&lt;/code&gt; искать большие папки, которые выглядели как мусор. У Logitech G HUB нашлись гигабайты в &lt;code&gt;depots&lt;/code&gt; и &lt;code&gt;cache&lt;/code&gt;. Похоже на старые загрузки, так что я их удалил. G HUB перестал работать.&lt;/p&gt;
&lt;h2&gt;Чем на самом деле была &lt;code&gt;depots&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Лежащий рядом &lt;code&gt;current.json&lt;/code&gt; перечислял depot установленной сборки со статусом &lt;code&gt;&quot;PRESENT&quot;&lt;/code&gt;. Приложение работает прямо из этой папки и проверяет её. Там лежала текущая версия, а не остатки старых. Помогла переустановка поверх.&lt;/p&gt;
&lt;h2&gt;Правило, которого я теперь держусь&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Внутри папки данных приложения название вроде cache, depots или downloads ничего не доказывает. Сначала поищите манифесты или файлы версий, которые на неё ссылаются.&lt;/li&gt;
&lt;li&gt;Лучше воспользоваться встроенной очисткой приложения или его деинсталлятором.&lt;/li&gt;
&lt;li&gt;Если непонятно, записывайте папку в «может потребовать переустановки», а не в «безопасно».&lt;/li&gt;
&lt;li&gt;Обычные кэши по-прежнему можно чистить смело: npm, pip, кэши шейдеров, &lt;code&gt;%TEMP%&lt;/code&gt;. Они пересобираются сами, так задумано.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Замерьте DNS-резолверы, прежде чем выбирать</title>
    <link href="https://home.tigran.manukyan.dev/ru/notes/measuring-dns-resolvers.html"/>
    <id>https://home.tigran.manukyan.dev/ru/notes/measuring-dns-resolvers.html</id>
    <published>2026-06-21T12:00:00Z</published>
    <updated>2026-06-21T12:00:00Z</updated>
    <summary>Скорость публичного DNS зависит от маршрутов вашего провайдера. Замерьте 1.1.1.1, 8.8.8.8 и 9.9.9.9 из своей сети через dig со случайными, некэшированными именами и сравнивайте медиану и худший результат.</summary>
    <category term="DNS"/>
    <category term="сеть"/>
    <category term="dig"/>
    <content type="html">
&lt;h1&gt;Замерьте DNS-резолверы, прежде чем выбирать&lt;/h1&gt;
&lt;p class=&quot;meta&quot;&gt;2026-06-21 · сеть&lt;/p&gt;
&lt;p&gt;Любой рейтинг «самых быстрых DNS» измерен из чужой сети. Anycast-маршрутизация зависит от пиринга вашего провайдера, так что резолвер, до которого у автора рейтинга 5 мс, у вас может оказаться в 60 мс.&lt;/p&gt;
&lt;h2&gt;Быстрый цикл&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;Случайное имя не попадает в кэш резолвера, поэтому замеряется полный резолв, а не ответ из кэша. Запустите в разное время дня: вечерние цифры обычно самые честные.&lt;/p&gt;
&lt;h2&gt;На что смотреть&lt;/h2&gt;
&lt;p&gt;Смотрите на медиану и худший результат, а не на лучший. Резолвер, который стабильно держит 8 мс, лучше того, что скачет от 3 до 90 мс.&lt;/p&gt;
</content>
  </entry>
</feed>
