8 lines
22 KiB
JSON
8 lines
22 KiB
JSON
{
|
||
"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 & $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 & $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>& 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>"
|
||
}
|