{ "index": 335, "slug": "editorial-2018-09-mechanism-windows-dev-env", "title": "Windows: почему команда запускает не тот бинарник", "excerpt": "Разбираем разрешение команд в PowerShell: приоритет alias, функций, cmdlet и приложений, поиск через PATH и PATHEXT, а также отдельную диагностику кодировки консоли и файлов.", "contentHtml": "
Сцена: разработчик открывает PowerShell на Windows-машине перед сборкой проекта и выполняет node --version. В одном окне он видит новую версию, в другом — старую; русское сообщение об ошибке к тому же выглядит как набор символов. Первая мысль — ещё раз поправить PATH и перезапустить сборку.
Такой шаг может скрыть причину. Имя node в PowerShell не обязано означать один конкретный файл: это может быть alias, функция, cmdlet, скрипт или внешнее приложение. А нечитаемый вывод относится к другому слою — кодовой странице и кодировке обмена. Разделим наблюдения, чтобы не исправить не тот node.exe, не повредить текстовый файл и не записать разовую гипотезу в системные настройки.
В первом окне команда была запущена после установки новой версии Node.js. Во втором остался старый терминал, открытый до изменения переменных среды. Разработчик сравнил только строку версии и решил, что сборщик «иногда выбирает случайный бинарник». Это предположение звучит правдоподобно, но пока не отвечает на два вопроса: какой объект PowerShell выбрал по имени и какой процесс унаследовал окружение.
\nВ 2018 году на Windows чаще использовался Windows PowerShell 5.1; те же базовые команды работают и в PowerShell 7.x, но значения по умолчанию для кодировки и поведение отдельных cmdlet могут различаться. Поэтому версия оболочки — часть наблюдения, а не деталь, которую можно опустить.
\nЕсли в команде нет пути и в текущей сессии есть несколько одноимённых команд, PowerShell применяет порядок приоритета: alias, функция, cmdlet, внешнее исполняемое приложение. Для внешнего приложения используются каталоги из $env:PATH, а на Windows расширения без явного суффикса сопоставляются с перечнем $env:PATHEXT. Путь, указанный явно, обходит это разрешение: PowerShell запускает файл по указанному адресу.
Это важная граница: where.exe перечисляет подходящие файлы в текущем каталоге и PATH, но не видит функцию или alias профиля PowerShell. Поэтому результат where.exe node — инвентаризация файлов, а результат Get-Command node -All — список команд в порядке исполнения. Они дополняют друг друга.
Снимок нужно сделать в той консоли, где проявился сбой. Команда -All показывает скрытые совпадения и сохраняет порядок приоритета. Для where указываем суффикс .exe: иначе PowerShell может трактовать это имя как alias Where-Object.
$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)\nЕсли первым оказался Function или Alias, проверка версии не доказывает, что был запущен файл. Если первым оказался Application, поле Definition показывает путь, который следует сопоставить с ожиданием проекта. Несколько строк в where.exe означают, что на диске есть несколько кандидатов; они ещё не доказывают, какой из них выполнился.
Не всякая команда принимает аргумент --version, поэтому сначала фиксируем тип объекта, а версию запрашиваем только у найденного приложения. В примере ниже действие не меняет окружение и возвращает код завершения отдельно от текста.
$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}\nУспешный код возврата означает только то, что конкретный запуск завершился с этим кодом. Он не подтверждает правильность версии, совместимость зависимостей или успешную сборку. Для проекта запишите commit, каталог запуска, полную команду и следующий симптом, который нужно проверить.
\nПеременные среды имеют область Process. Сеанс PowerShell получает её от родительского процесса, а дочерние программы наследуют копию при запуске. Присваивание $env:Path = ... меняет текущий процесс и программы, которые будут созданы из него; оно не переписывает системную или пользовательскую область Windows.
Отсюда объясняется разница между окнами. Настройка, сделанная через интерфейс Windows, не превращает уже открытый терминал в новый процесс. Старое окно продолжает жить со своим снимком окружения. Откройте новый процесс или запускайте проверку в явно заданном окружении, иначе сравниваются разные входные данные.
\n| Наблюдение | Что оно доказывает | Чего оно не доказывает |
|---|---|---|
Get-Command node -All | Какие команды доступны в сессии и в каком порядке они разрешаются | Что каждый найденный файл запускался |
where.exe node | Какие файлы найдены в текущем каталоге и PATH | Что alias или функция не перекрывает приложение |
$env:Path | Какой PATH видит текущий процесс | Какой PATH был у уже запущенного сборщика |
node --version | Строку, которую вернул данный вызов | Причину падения сборки, DLL, права и lock-файл |
| Часть состояния консоли и обмена текстом | Кодировку каждого сохранённого файла |
Гипотеза должна быть узкой: «нужный каталог не является первым кандидатом для этого процесса». Проверяем её временно и с восстановлением исходного значения. Каталог в примере условный: перед запуском убедитесь, что в нём действительно лежит требуемый файл.
\n$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}\nЗдесь бинарник вызывается абсолютным путем, поэтому результат эксперимента не зависит от повторного разрешения имени. Если требуемая версия сработала, повторите исходную команду в том же временном процессе. Если симптом остался, гипотеза о порядке PATH не подтверждена: не добавляйте каталог в системную область, ищите следующую причину.
В исходной сцене версия могла совпасть в обоих окнах, а отличаться только русская ошибка. Это возвращает нас к другой ветке. chcp показывает активную кодовую страницу консоли. [Console]::OutputEncoding описывает вывод .NET, а автоматическая переменная $OutputEncoding относится к обмену PowerShell с внешними программами. Кодировка файла, который читает или пишет утилита, — отдельный вопрос.
& 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\nНе делайте вывод о файле по виду текста в терминале. В Windows PowerShell 5.1 значения по умолчанию для записи, перенаправления и чтения без BOM не совпадают во всех cmdlet; в PowerShell 7.x значения по умолчанию и параметры кодировки иные. Если ошибка относится к файлу, укажите кодировку в конкретном cmdlet или настройке программы и сохраните байтовый снимок. Если она относится к обмену с внешней командой, проверяйте $OutputEncoding и контракт этой команды.
chcp 65001 — эксперимент, а не универсальный ремонт. По документации Windows новые процессы используют назначенную кодовую страницу, тогда как уже запущенные программы могут продолжать использовать исходную. После изменения зафиксируйте новый процесс и повторите ровно тот же ввод; не меняйте одновременно кодировку файла, профиль и системный PATH.
| Симптом | Рабочая гипотеза | Проверка | Ограниченное действие |
|---|---|---|---|
Версии node различаются в двух окнах | Разные снимки процесса или каталог в PATH | Сравнить PID, $env:Path, Get-Command -All | Открыть новый процесс; временно добавить проверенный каталог |
where.exe показывает файл, но выполняется другая команда | Alias или функция перекрывает приложение | Get-Command node -All, поле CommandType | Устранить конфликт в профиле или вызвать проверенный путь |
| Версия совпадает, русское сообщение искажено | Различается кодовая страница, OutputEncoding или кодировка файла | chcp, свойства Console, байты файла | Менять один слой и повторять тот же тест |
| Временный PATH не изменил результат | Сборка использует другой процесс, lock-файл, DLL или сеть | Полный лог и окружение запуска сборщика | Прекратить правку PATH и перейти к следующей гипотезе |
| Смена кодовой страницы не помогла | Проблема в чтении/записи файла или API программы | Определить байты и настройку конкретного инструмента | Не включать глобальную кодировку без контракта |
Get-Command node -All и where.exe node; запишите Definition первого кандидата.-NoProfile и повторите снимок, чтобы отделить профиль от окружения.$env:Path и $env:PATHEXT в старом и новом процессах.PATH или абсолютным путем, восстановив окружение через finally.chcp, Console, $OutputEncoding и кодировке конкретного файла.Даже одинаковые путь и версия не делают два окружения идентичными. На сборку влияют разрядность, DLL, права, антивирус, прокси, кеш, локальные файлы, политика выполнения и lock-файл. Не отключайте защиту и не переустанавливайте Windows, пока наблюдение не указывает на такую причину.
\nПрофиль PowerShell может добавлять функции и alias, а сборщик может запускаться не из интерактивного окна — например, через отдельный процесс CI или IDE. Поэтому «в терминале работает» не означает «тот же объект запущен в сборке». Перед выводом сравните родителя процесса, рабочий каталог, переменные среды и точную команду.
\nРасследование закончено, когда можно показать четыре независимых факта: какой тип команды выбран, какой файл напечатал проверенную версию, какой процесс получил нужный PATH и какая кодировка относится к проблемному тексту. В финальной проверке новый процесс запускает тот же commit и исходную команду, получает ожидаемую версию и не возвращает исходную ошибку. Совпала только строка версии — значит, причина ещё не доказана.