Files

8 lines
24 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 336,
"slug": "editorial-2018-09-practice-windows-dev-env",
"title": "Windows-окружение без гадания: как найти настоящий бинарник и сравнить запуск",
"excerpt": "Если одна Windows-машина запускает проект, а другая не находит команду или выбирает другую версию, сначала снимите процессное окружение. Разбираем PATH, профиль PowerShell, кодовую страницу и обратимую проверку без глобальной переустановки.",
"contentHtml": "<p>Симптом знаком: на одном компьютере <code>npm run build</code> проходит, а на другом команда не находится, запускает старый Node или печатает нечитаемый лог. Код и commit совпадают. Разработчик видит только имя <code>node</code>, но Windows выбирает конкретный файл из конкретного процесса. Цена ошибки — потерянные часы и испорченные доказательства. Если сразу переустановить Node или переписать системный <code>PATH</code>, изменятся несколько условий сразу. После этого трудно понять, что действительно помогло.</p>\n<p>Тезис простой: сравнивать нужно не список установленных программ, а путь запуска команды. В него входят процессные переменные, порядок каталогов в <code>PATH</code>, тип найденной команды, фактический путь к файлу, вывод версии и состояние консоли. Такой снимок не делает машины одинаковыми. Он сужает причину до наблюдаемого различия, которое можно проверить одним обратимым действием.</p>\n<h2>Механизм: имя команды не равно файлу</h2>\n<p>PowerShell ищет команды в текущей сессии. Результатом может быть alias, функция, cmdlet, скрипт или приложение. Для приложения поиск использует переменную <code>PATH</code> и расширения из <code>PATHEXT</code>. Первый найденный вариант становится тем, что запустит команда. Поэтому строка «Node установлен» не отвечает на вопрос «какой <code>node.exe</code> запустился сейчас?».</p>\n<p>Переменные среды имеют область процесса, пользователя и компьютера. Новая PowerShell-сессия получает значения от родительского процесса. Уже открытое окно не обязано увидеть изменение, сделанное в настройках Windows. Если добавить каталог в системный <code>PATH</code>, а затем повторить команду в старом окне, проверка может измерить старое состояние. Для диагностики сначала используйте новый процесс, а постоянную настройку не меняйте.</p>\n<p>Кодовая страница и кодировка обмена с внешней программой — отдельные слои. Команда <code>chcp</code> показывает активную кодовую страницу консоли, а <code>$OutputEncoding</code> задаёт кодировку, с которой PowerShell обменивается данными с native-программами. Ни одна из них не выбирает бинарник. Если путь и версия совпадают, нечитаемый вывод не доказывает проблему <code>PATH</code>. Кодировка файла — ещё один вопрос.</p>\n<figure><img src=\"/assets/editorial/2018/windows-dev-env-snapshot-2018.svg\" alt=\"Схема снимка Windows-окружения: PowerShell передаёт дочернему процессу PATH и PATHEXT, затем фиксируются найденный бинарник, версия и кодовая страница\" loading=\"lazy\" /><figcaption>Снимок фиксирует путь запуска команды. Он не является копией компьютера и не должен содержать секреты.</figcaption></figure>\n<h2>Минимальный снимок</h2>\n<p>Снимок должен отвечать на четыре вопроса: какая PowerShell выполняет команду; какие каталоги участвуют в поиске; какие кандидаты возвращает <code>Get-Command -All</code>; что сообщает сам инструмент. Отдельно запишите кодовую страницу, кодировку обмена и способ записи файла. Не собирайте весь <code>Env:</code>: в нём могут оказаться токены, прокси, внутренние адреса и персональные пути.</p>\n<table><caption>Что фиксировать при сравнении двух запусков</caption><thead><tr><th scope=\"col\">Поле</th><th scope=\"col\">Проверка</th><th scope=\"col\">Что объясняет</th><th scope=\"col\">Ограничение</th></tr></thead><tbody><tr><td>Версия и редакция PowerShell</td><td><code>$PSVersionTable.PSVersion</code>, <code>$PSVersionTable.PSEdition</code></td><td>Доступные команды и различия поведения оболочки</td><td>Не доказывает версию проекта</td></tr><tr><td>Порядок <code>PATH</code></td><td><code>$env:Path -split ';'</code></td><td>Какая папка может дать первый бинарник</td><td>Сам по себе путь не говорит, что файл исправен</td></tr><tr><td>Кандидаты команды</td><td><code>Get-Command node -All</code></td><td>Alias, функцию и приложения по одному имени</td><td>Не проверяет зависимости приложения</td></tr><tr><td>Фактическая версия</td><td><code>node --version</code></td><td>Что ответил запущенный инструмент</td><td>Не заменяет lock-файл и требования проекта</td></tr><tr><td>Состояние консоли и обмена</td><td><code>cmd /c chcp</code>, <code>$OutputEncoding.WebName</code>, <code>[Console]::OutputEncoding.WebName</code></td><td>Причину различий в консоли и при обмене с native-программой</td><td>Не меняет кодировку файлов и редактора</td></tr></tbody></table>\n<h2>Пример сбора отчёта</h2>\n<p>Ниже — ограниченный учебный скрипт для PowerShell 5.1. Он собирает выбранные поля и версии четырёх команд. Пример не устанавливает инструменты, не исправляет <code>PATH</code> и не создаёт рабочие данные. Замените список команд на требования конкретного проекта. Если проект использует только PHP, отсутствие Node в отчёте нормально.</p>\n<pre><code># Capture-Environment.ps1\n$ErrorActionPreference = 'Stop'\n$toolChecks = @(\n @{ name = 'node'; arguments = @('--version') },\n @{ name = 'npm'; arguments = @('--version') },\n @{ name = 'php'; arguments = @('--version') },\n @{ name = 'git'; arguments = @('--version') }\n)\n\nfunction Get-Candidates($name) {\n @(Get-Command -Name $name -All -ErrorAction SilentlyContinue |\n ForEach-Object {\n [ordered]@{\n type = $_.CommandType.ToString()\n name = $_.Name\n definition = $_.Definition\n source = $_.Source\n version = if ($_.Version) { $_.Version.ToString() } else { $null }\n }\n })\n}\n\nfunction Get-VersionOutput($name, $arguments) {\n $command = Get-Command -Name $name -ErrorAction SilentlyContinue\n if (-not $command) { return @('NOT FOUND') }\n if ($command.CommandType -ne 'Application') {\n return @('SKIPPED: ' + $command.CommandType.ToString() + ' ' + $command.Definition)\n }\n try {\n $lines = @(&amp; $command.Source @arguments 2&gt;&amp;1 | Select-Object -First 3 |\n ForEach-Object { $_.ToString() })\n return @($lines + ('exitCode=' + $LASTEXITCODE))\n } catch {\n return @('FAILED: ' + $_.Exception.Message)\n }\n}\n\n$report = [ordered]@{\n formatVersion = 1\n capturedAt = (Get-Date).ToString('o')\n powershell = [ordered]@{\n version = $PSVersionTable.PSVersion.ToString()\n edition = $PSVersionTable.PSEdition\n }\n console = [ordered]@{\n codePage = ((cmd.exe /d /c chcp) -join ' ').Trim()\n nativeOutputEncoding = $OutputEncoding.WebName\n consoleOutputEncoding = [Console]::OutputEncoding.WebName\n }\n environment = [ordered]@{\n pathEntries = @($env:Path -split ';' | Where-Object { $_ })\n pathext = $env:PATHEXT\n }\n commands = @($toolChecks | ForEach-Object {\n [ordered]@{\n name = $_.name\n candidates = Get-Candidates $_.name\n versionOutput = Get-VersionOutput $_.name $_.arguments\n }\n })\n}\n\n$report | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath '.\\environment-snapshot.json' -Encoding UTF8</code></pre>\n<p>Запускайте его из каталога проекта без профиля, чтобы пользовательская функция или alias не скрыли реальную картину. Для версии 5.1 это также сохраняет UTF-8 с BOM, поэтому файл безопаснее читать теми инструментами, которые умеют распознавать BOM:</p>\n<pre><code>powershell.exe -NoProfile -File .\\tools\\Capture-Environment.ps1\nGet-Content .\\environment-snapshot.json -Raw | ConvertFrom-Json | Format-List\nGet-Command node -All | Select-Object CommandType, Name, Version, Definition</code></pre>\n<p>Перед отправкой JSON удалите имя пользователя, внутренние каталоги, URL прокси и любые значения, которые относятся к доступу. Сохраните исходный файл только там, где это разрешено. Если отчёт нужен для сравнения, полезнее заменить часть пути на <code>&lt;USER&gt;</code>, чем публиковать личные данные. Не добавляйте снимок в репозиторий, пока не проверили его содержимое.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Диагностическая карта для Windows-окружения</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Команда не найдена</td><td>Каталог отсутствует в процессном <code>PATH</code> или программа не установлена</td><td><code>$env:Path -split ';'</code>, <code>Get-Command name -All</code></td><td>Сверить требование проекта и проверить одобренную установку; не дописывать путь наугад</td></tr><tr><td>Запускается не та версия</td><td>Старая папка стоит раньше в <code>PATH</code></td><td>Сравнить первый <code>Definition</code>, весь список кандидатов и <code>name --version</code></td><td>В новом окне временно проверить документированный каталог в начале процессного <code>$env:Path</code></td></tr><tr><td>Вместо приложения найдена функция или alias</td><td>Профиль PowerShell подменяет имя</td><td>Сравнить обычный запуск с <code>powershell.exe -NoProfile</code></td><td>Использовать явный путь или исправить профиль после подтверждения причины</td></tr><tr><td>После изменения ничего не поменялось</td><td>Команда выполняется в старом процессе</td><td>Снять отчёт из новой сессии без профиля</td><td>Повторить одну исходную команду; не делать вывод о неработающем исправлении по старому окну</td></tr><tr><td>Лог нечитаем</td><td>Различаются кодовая страница или кодировка вывода</td><td><code>chcp</code>, <code>OutputEncoding</code> и способ записи файла</td><td>Проверить консоль, обмен с native-программой и файл раздельно; не менять <code>PATH</code></td></tr><tr><td>Путь и версия совпадают, сборка всё ещё падает</td><td>Причина находится в зависимостях, правах, сети или конфигурации</td><td>Lock-файл, полный лог, права каталога и следующий симптом</td><td>Прекратить правку окружения и продолжить расследование по новому факту</td></tr></tbody></table>\n<h2>Сравнение двух машин</h2>\n<p>Сначала сделайте входы сравнимыми: один commit, одна команда, один рабочий каталог и одинаковый lock-файл. На обеих машинах сохраните очищенные снимки. Затем сравните только поля, относящиеся к запуску. В учебном примере машина A первой находит <code>C:\\Tools\\node-6\\node.exe</code>, а машина B — <code>C:\\Program Files\\nodejs\\node.exe</code>. Это доказывает различие поиска, но не доказывает, какая версия нужна проекту. Требование должно прийти из README, lock-файла или официальной документации проекта.</p>\n<pre><code>function Get-SnapshotLines($path) {\n $s = Get-Content $path -Raw | ConvertFrom-Json\n $lines = @(\n 'powershell=' + $s.powershell.version\n 'codePage=' + $s.console.codePage\n 'nativeOutputEncoding=' + $s.console.nativeOutputEncoding\n 'consoleOutputEncoding=' + $s.console.consoleOutputEncoding\n 'pathext=' + $s.environment.pathext\n )\n foreach ($entry in $s.environment.pathEntries) { $lines += 'path=' + $entry }\n foreach ($tool in $s.commands) {\n $first = @($tool.candidates | Select-Object -First 1)\n if ($first.Count -eq 0) { $lines += $tool.name + '=NOT FOUND'; continue }\n $lines += ('{0}={1}|{2}|{3}' -f $tool.name, $first[0].type, $first[0].definition, $first[0].version)\n }\n return $lines\n}\nCompare-Object (Get-SnapshotLines '.\\machine-a.json') (Get-SnapshotLines '.\\machine-b.json')</code></pre>\n<p><code>Compare-Object</code> показывает различающиеся строки. Он не ставит диагноз. Если отличаются десять каталогов, выберите тот, который связан с исходной ошибкой, и проверьте его отдельно. Сначала сравните первый кандидат и версию. Только затем смотрите кодовую страницу или второстепенные различия.</p>\n<h2>Обратимая проверка PATH</h2>\n<p>Допустим, документация проекта требует Node из конкретного каталога, а снимок показывает старый бинарник. В новом PowerShell добавьте проверенный каталог только в процессную переменную. Учебный путь ниже условный. Его нельзя копировать в рабочую машину без подтверждения версии и расположения файла.</p>\n<pre><code>$requiredNode = 'C:\\Tools\\node-8'\nif (-not (Test-Path (Join-Path $requiredNode 'node.exe'))) {\n throw 'node.exe not found: ' + $requiredNode\n}\n$env:Path = $requiredNode + ';' + $env:Path\nGet-Command node -All | Select-Object CommandType, Definition\nnode --version\nnpm run build</code></pre>\n<p>Если версия изменилась и исходная ошибка исчезла, гипотеза о порядке <code>PATH</code> получила подтверждение в этой сессии. Это ещё не разрешение менять системные настройки. Сначала зафиксируйте требование проекта и способ установки. Если результат не изменился, верните внимание к логу. Не оставляйте случайный каталог в постоянном <code>PATH</code> и не скачивайте исполняемый файл из непроверенного источника.</p>\n<h2>Порядок действий</h2>\n<ol><li>Запишите commit, рабочий каталог, команду и полный текст ошибки до любых изменений.</li><li>Назовите инструменты, которые действительно нужны проекту, и их требуемые версии.</li><li>Снимите выбранные поля из обычной PowerShell-сессии.</li><li>Повторите снимок в новом окне с <code>-NoProfile</code>, если есть подозрение на alias, функцию или профиль.</li><li>Сравните порядок <code>PATH</code>, <code>PATHEXT</code>, кандидатов команды, путь первого приложения и результат <code>--version</code>.</li><li>Выберите одно отличие, которое прямо связано с ошибкой. Не исправляйте остальные различия одновременно.</li><li>Проверьте гипотезу в новом процессе через временный <code>$env:Path</code> или другой обратимый шаг.</li><li>Повторите исходную команду и сохраните результат вместе с новым снимком.</li><li>Только после подтверждения обновите README или согласованный установочный механизм. Закройте временную сессию и убедитесь, что глобальные настройки не изменились.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Одинаковый снимок не гарантирует одинаковый запуск. На результат влияют разрядность, DLL, права каталога, антивирус, прокси, сертификаты, кеши, редактор, локальные файлы и сетевой доступ. Снимок не видит все эти причины. Он только помогает исключить подмену команды и ошибку поиска.</p>\n<p>Если команда не найдена, не добавляйте в <code>PATH</code> первую папку из загрузок. Сначала проверьте официальную инструкцию проекта, наличие нужного файла и архитектуру. Если путь и версия совпали, не продолжайте менять <code>PATH</code> ради другого симптома. Если отчёт содержит секрет, удалите его из копии до публикации и сообщите владельцу доступа по принятому каналу.</p>\n<p>Если новый сеанс с нужным каталогом не меняет исходную ошибку, отрицательный результат важен. Он исключает одну гипотезу. Верните постоянные настройки в исходное состояние, не скрывайте неудачную проверку и исследуйте следующий наблюдаемый факт: лог пакетного менеджера, права, сертификат или конфигурацию проекта.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Разбор готов, когда другой разработчик без устного объяснения может назвать исходную команду и ошибку, показать очищенный снимок, указать первый найденный бинарник, сопоставить его с требуемой версией и повторить обратимую проверку в новом сеансе. Должно быть видно, что изменилось и что не изменилось. Если остаётся только фраза «у меня работает», диагностика не закончена.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1\" target=\"_blank\" rel=\"noopener noreferrer\">Microsoft Learn: about_Environment_Variables</a> — области процесса, пользователя и компьютера, наследование переменных дочерними процессами, <code>PATH</code> и <code>PATHEXT</code>.</li><li><a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-command?view=powershell-7.6\" target=\"_blank\" rel=\"noopener noreferrer\">Microsoft Learn: Get-Command</a> — типы команд, поиск приложений в <code>PATH</code> и порядок выполнения при параметре <code>-All</code>.</li><li><a href=\"https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/chcp\" target=\"_blank\" rel=\"noopener noreferrer\">Microsoft Learn: chcp</a> — проверка активной кодовой страницы консоли Windows и граница действия для уже запущенных программ.</li><li><a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_character_encoding?view=powershell-7.5\" target=\"_blank\" rel=\"noopener noreferrer\">Microsoft Learn: about_Character_Encoding</a> — различие между <code>$OutputEncoding</code>, кодировкой консоли и кодировкой файлов.</li><li><a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7.5\" target=\"_blank\" rel=\"noopener noreferrer\">Microsoft Learn: about_Profiles</a> — профиль как startup script и причина запускать диагностический снимок через <code>-NoProfile</code>.</li></ul>"
}