Files

8 lines
22 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": 335,
"slug": "editorial-2018-09-mechanism-windows-dev-env",
"title": "Windows: почему команда запускает не тот бинарник",
"excerpt": "Разбираем разрешение команд в PowerShell: приоритет alias, функций, cmdlet и приложений, поиск через PATH и PATHEXT, а также отдельную диагностику кодировки консоли и файлов.",
"contentHtml": "<p><strong>Сцена:</strong> разработчик открывает PowerShell на Windows-машине перед сборкой проекта и выполняет <code>node --version</code>. В одном окне он видит новую версию, в другом — старую; русское сообщение об ошибке к тому же выглядит как набор символов. Первая мысль — ещё раз поправить <code>PATH</code> и перезапустить сборку.</p>\n<p>Такой шаг может скрыть причину. Имя <code>node</code> в PowerShell не обязано означать один конкретный файл: это может быть alias, функция, cmdlet, скрипт или внешнее приложение. А нечитаемый вывод относится к другому слою — кодовой странице и кодировке обмена. Разделим наблюдения, чтобы не исправить не тот <code>node.exe</code>, не повредить текстовый файл и не записать разовую гипотезу в системные настройки.</p>\n<h2>Сценарий: два окна, один проект, разные ответы</h2>\n<p>В первом окне команда была запущена после установки новой версии Node.js. Во втором остался старый терминал, открытый до изменения переменных среды. Разработчик сравнил только строку версии и решил, что сборщик «иногда выбирает случайный бинарник». Это предположение звучит правдоподобно, но пока не отвечает на два вопроса: какой объект PowerShell выбрал по имени и какой процесс унаследовал окружение.</p>\n<p>В 2018 году на Windows чаще использовался Windows PowerShell 5.1; те же базовые команды работают и в PowerShell 7.x, но значения по умолчанию для кодировки и поведение отдельных cmdlet могут различаться. Поэтому версия оболочки — часть наблюдения, а не деталь, которую можно опустить.</p>\n<h2>Как PowerShell разрешает имя команды</h2>\n<p>Если в команде нет пути и в текущей сессии есть несколько одноимённых команд, PowerShell применяет порядок приоритета: alias, функция, cmdlet, внешнее исполняемое приложение. Для внешнего приложения используются каталоги из <code>$env:PATH</code>, а на Windows расширения без явного суффикса сопоставляются с перечнем <code>$env:PATHEXT</code>. Путь, указанный явно, обходит это разрешение: PowerShell запускает файл по указанному адресу.</p>\n<p>Это важная граница: <code>where.exe</code> перечисляет подходящие файлы в текущем каталоге и <code>PATH</code>, но не видит функцию или alias профиля PowerShell. Поэтому результат <code>where.exe node</code> — инвентаризация файлов, а результат <code>Get-Command node -All</code> — список команд в порядке исполнения. Они дополняют друг друга.</p>\n<figure><img src='/assets/editorial/2018/windows-dev-env-resolution-2018.svg' alt='Схема разрешения команды в PowerShell: alias, функция, cmdlet или приложение; для приложения проверяются PATH и PATHEXT, а кодовая страница диагностируется отдельно' /><figcaption>Одно имя распадается на две проверки: сначала выясняется выбранный объект и его файл, потом — как консоль и программа обменялись текстом.</figcaption></figure>\n<h2>Первый снимок: не запускаем предположение</h2>\n<p>Снимок нужно сделать в той консоли, где проявился сбой. Команда <code>-All</code> показывает скрытые совпадения и сохраняет порядок приоритета. Для <code>where</code> указываем суффикс <code>.exe</code>: иначе PowerShell может трактовать это имя как alias <code>Where-Object</code>.</p>\n<pre><code>$version = $PSVersionTable.PSVersion.ToString()\nWrite-Host ('PowerShell=' + $version)\nWrite-Host ('PID=' + $PID)\n\nGet-Command -Name node -All |\n Select-Object CommandType, Name, Source, Definition, Version |\n Format-Table -AutoSize\n\nwhere.exe node\nWrite-Host ('PATH=' + $env:Path)\nWrite-Host ('PATHEXT=' + $env:PATHEXT)</code></pre>\n<p>Если первым оказался <code>Function</code> или <code>Alias</code>, проверка версии не доказывает, что был запущен файл. Если первым оказался <code>Application</code>, поле <code>Definition</code> показывает путь, который следует сопоставить с ожиданием проекта. Несколько строк в <code>where.exe</code> означают, что на диске есть несколько кандидатов; они ещё не доказывают, какой из них выполнился.</p>\n<h2>Воспроизводимая проверка приложения</h2>\n<p>Не всякая команда принимает аргумент <code>--version</code>, поэтому сначала фиксируем тип объекта, а версию запрашиваем только у найденного приложения. В примере ниже действие не меняет окружение и возвращает код завершения отдельно от текста.</p>\n<pre><code>$commands = @(Get-Command -Name node -All -ErrorAction SilentlyContinue)\n$chosen = $commands | Select-Object -First 1\n\nif (-not $chosen) {\n throw 'Команда node не найдена в этой сессии'\n}\n\nWrite-Host ('CommandType=' + $chosen.CommandType)\nWrite-Host ('Definition=' + $chosen.Definition)\n\nif ($chosen.CommandType -eq 'Application') {\n &amp; $chosen.Definition --version\n Write-Host ('exitCode=' + $LASTEXITCODE)\n} else {\n Write-Host 'Это не Application: аргумент --version здесь не выполнялся'\n}</code></pre>\n<p>Успешный код возврата означает только то, что конкретный запуск завершился с этим кодом. Он не подтверждает правильность версии, совместимость зависимостей или успешную сборку. Для проекта запишите commit, каталог запуска, полную команду и следующий симптом, который нужно проверить.</p>\n<h2>Что именно наследует новый процесс</h2>\n<p>Переменные среды имеют область <code>Process</code>. Сеанс PowerShell получает её от родительского процесса, а дочерние программы наследуют копию при запуске. Присваивание <code>$env:Path = ...</code> меняет текущий процесс и программы, которые будут созданы из него; оно не переписывает системную или пользовательскую область Windows.</p>\n<p>Отсюда объясняется разница между окнами. Настройка, сделанная через интерфейс Windows, не превращает уже открытый терминал в новый процесс. Старое окно продолжает жить со своим снимком окружения. Откройте новый процесс или запускайте проверку в явно заданном окружении, иначе сравниваются разные входные данные.</p>\n<div class='table-scroll'><table><thead><tr><th scope='col'>Наблюдение</th><th scope='col'>Что оно доказывает</th><th scope='col'>Чего оно не доказывает</th></tr></thead><tbody><tr><td><code>Get-Command node -All</code></td><td>Какие команды доступны в сессии и в каком порядке они разрешаются</td><td>Что каждый найденный файл запускался</td></tr><tr><td><code>where.exe node</code></td><td>Какие файлы найдены в текущем каталоге и <code>PATH</code></td><td>Что alias или функция не перекрывает приложение</td></tr><tr><td><code>$env:Path</code></td><td>Какой PATH видит текущий процесс</td><td>Какой PATH был у уже запущенного сборщика</td></tr><tr><td><code>node --version</code></td><td>Строку, которую вернул данный вызов</td><td>Причину падения сборки, DLL, права и lock-файл</td></tr><tr><code>chcp</code> и <code>OutputEncoding</code></td><td>Часть состояния консоли и обмена текстом</td><td>Кодировку каждого сохранённого файла</td></tr></tbody></table></div>\n<h2>Безопасный эксперимент с PATH</h2>\n<p>Гипотеза должна быть узкой: «нужный каталог не является первым кандидатом для этого процесса». Проверяем её временно и с восстановлением исходного значения. Каталог в примере условный: перед запуском убедитесь, что в нём действительно лежит требуемый файл.</p>\n<pre><code>$oldPath = $env:Path\n$requiredDir = 'C:\\Tools\\node-20'\n\ntry {\n $nodePath = Join-Path $requiredDir 'node.exe'\n if (-not (Test-Path -LiteralPath $nodePath -PathType Leaf)) {\n throw ('Файл не найден: ' + $nodePath)\n }\n\n $env:Path = $requiredDir + ';' + $oldPath\n $after = Get-Command -Name node -All -ErrorAction SilentlyContinue\n $after | Select-Object CommandType, Definition, Version | Format-Table -AutoSize\n &amp; $nodePath --version\n Write-Host ('exitCode=' + $LASTEXITCODE)\n} finally {\n $env:Path = $oldPath\n}</code></pre>\n<p>Здесь бинарник вызывается абсолютным путем, поэтому результат эксперимента не зависит от повторного разрешения имени. Если требуемая версия сработала, повторите исходную команду в том же временном процессе. Если симптом остался, гипотеза о порядке <code>PATH</code> не подтверждена: не добавляйте каталог в системную область, ищите следующую причину.</p>\n<h2>Поворот расследования: путь совпал, текст — нет</h2>\n<p>В исходной сцене версия могла совпасть в обоих окнах, а отличаться только русская ошибка. Это возвращает нас к другой ветке. <code>chcp</code> показывает активную кодовую страницу консоли. <code>[Console]::OutputEncoding</code> описывает вывод .NET, а автоматическая переменная <code>$OutputEncoding</code> относится к обмену PowerShell с внешними программами. Кодировка файла, который читает или пишет утилита, — отдельный вопрос.</p>\n<pre><code>&amp; cmd.exe /d /c chcp\n[Console]::InputEncoding.WebName\n[Console]::OutputEncoding.WebName\n$OutputEncoding.WebName\n\n# Снимок версии и платформы для журнала проверки\n$PSVersionTable | Format-List PSVersion, PSEdition, OS</code></pre>\n<p>Не делайте вывод о файле по виду текста в терминале. В Windows PowerShell 5.1 значения по умолчанию для записи, перенаправления и чтения без BOM не совпадают во всех cmdlet; в PowerShell 7.x значения по умолчанию и параметры кодировки иные. Если ошибка относится к файлу, укажите кодировку в конкретном cmdlet или настройке программы и сохраните байтовый снимок. Если она относится к обмену с внешней командой, проверяйте <code>$OutputEncoding</code> и контракт этой команды.</p>\n<p><code>chcp 65001</code> — эксперимент, а не универсальный ремонт. По документации Windows новые процессы используют назначенную кодовую страницу, тогда как уже запущенные программы могут продолжать использовать исходную. После изменения зафиксируйте новый процесс и повторите ровно тот же ввод; не меняйте одновременно кодировку файла, профиль и системный <code>PATH</code>.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class='table-scroll'><table><thead><tr><th scope='col'>Симптом</th><th scope='col'>Рабочая гипотеза</th><th scope='col'>Проверка</th><th scope='col'>Ограниченное действие</th></tr></thead><tbody><tr><td>Версии <code>node</code> различаются в двух окнах</td><td>Разные снимки процесса или каталог в <code>PATH</code></td><td>Сравнить <code>PID</code>, <code>$env:Path</code>, <code>Get-Command -All</code></td><td>Открыть новый процесс; временно добавить проверенный каталог</td></tr><tr><td><code>where.exe</code> показывает файл, но выполняется другая команда</td><td>Alias или функция перекрывает приложение</td><td><code>Get-Command node -All</code>, поле <code>CommandType</code></td><td>Устранить конфликт в профиле или вызвать проверенный путь</td></tr><tr><td>Версия совпадает, русское сообщение искажено</td><td>Различается кодовая страница, OutputEncoding или кодировка файла</td><td><code>chcp</code>, свойства <code>Console</code>, байты файла</td><td>Менять один слой и повторять тот же тест</td></tr><tr><td>Временный PATH не изменил результат</td><td>Сборка использует другой процесс, lock-файл, DLL или сеть</td><td>Полный лог и окружение запуска сборщика</td><td>Прекратить правку PATH и перейти к следующей гипотезе</td></tr><tr><td>Смена кодовой страницы не помогла</td><td>Проблема в чтении/записи файла или API программы</td><td>Определить байты и настройку конкретного инструмента</td><td>Не включать глобальную кодировку без контракта</td></tr></tbody></table></div>\n<h2>Порядок проверки</h2>\n<ol><li>Сохраните каталог запуска, commit, полную команду, PID, версию PowerShell и текст ошибки.</li><li>В той же сессии выполните <code>Get-Command node -All</code> и <code>where.exe node</code>; запишите <code>Definition</code> первого кандидата.</li><li>Откройте PowerShell с <code>-NoProfile</code> и повторите снимок, чтобы отделить профиль от окружения.</li><li>Сравните <code>$env:Path</code> и <code>$env:PATHEXT</code> в старом и новом процессах.</li><li>Проверьте узкую гипотезу временным <code>PATH</code> или абсолютным путем, восстановив окружение через <code>finally</code>.</li><li>Повторите исходную команду и запишите версию, код возврата, следующий симптом и полный лог.</li><li>Если путь уже доказан, перейдите к <code>chcp</code>, <code>Console</code>, <code>$OutputEncoding</code> и кодировке конкретного файла.</li><li>Постоянную настройку меняйте только после подтвержденного эксперимента; зафиксируйте требование в README или скрипте проекта.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Даже одинаковые путь и версия не делают два окружения идентичными. На сборку влияют разрядность, DLL, права, антивирус, прокси, кеш, локальные файлы, политика выполнения и lock-файл. Не отключайте защиту и не переустанавливайте Windows, пока наблюдение не указывает на такую причину.</p>\n<p>Профиль PowerShell может добавлять функции и alias, а сборщик может запускаться не из интерактивного окна — например, через отдельный процесс CI или IDE. Поэтому «в терминале работает» не означает «тот же объект запущен в сборке». Перед выводом сравните родителя процесса, рабочий каталог, переменные среды и точную команду.</p>\n<h2>Критерий завершения диагностики</h2>\n<p>Расследование закончено, когда можно показать четыре независимых факта: какой тип команды выбран, какой файл напечатал проверенную версию, какой процесс получил нужный <code>PATH</code> и какая кодировка относится к проблемному тексту. В финальной проверке новый процесс запускает тот же commit и исходную команду, получает ожидаемую версию и не возвращает исходную ошибку. Совпала только строка версии — значит, причина ещё не доказана.</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_command_precedence?view=powershell-7.6' target='_blank' rel='noopener noreferrer'>Microsoft Learn: порядок разрешения команд PowerShell</a></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 и параметр -All</a></li><li><a href='https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-7.6' target='_blank' rel='noopener noreferrer'>Microsoft Learn: области переменных среды, PATH и PATHEXT</a></li><li><a href='https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/where' target='_blank' rel='noopener noreferrer'>Microsoft Learn: команда where</a></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></li><li><a href='https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_character_encoding?view=powershell-7.6' target='_blank' rel='noopener noreferrer'>Microsoft Learn: кодировка в Windows PowerShell и PowerShell 7.x</a></li></ul>"
}