{ "index": 335, "slug": "editorial-2018-09-mechanism-windows-dev-env", "title": "Windows: почему команда запускает не тот бинарник", "excerpt": "Разбираем разрешение команд в PowerShell: приоритет alias, функций, cmdlet и приложений, поиск через PATH и PATHEXT, а также отдельную диагностику кодировки консоли и файлов.", "contentHtml": "

Сцена: разработчик открывает PowerShell на Windows-машине перед сборкой проекта и выполняет node --version. В одном окне он видит новую версию, в другом — старую; русское сообщение об ошибке к тому же выглядит как набор символов. Первая мысль — ещё раз поправить PATH и перезапустить сборку.

\n

Такой шаг может скрыть причину. Имя node в PowerShell не обязано означать один конкретный файл: это может быть alias, функция, cmdlet, скрипт или внешнее приложение. А нечитаемый вывод относится к другому слою — кодовой странице и кодировке обмена. Разделим наблюдения, чтобы не исправить не тот node.exe, не повредить текстовый файл и не записать разовую гипотезу в системные настройки.

\n

Сценарий: два окна, один проект, разные ответы

\n

В первом окне команда была запущена после установки новой версии Node.js. Во втором остался старый терминал, открытый до изменения переменных среды. Разработчик сравнил только строку версии и решил, что сборщик «иногда выбирает случайный бинарник». Это предположение звучит правдоподобно, но пока не отвечает на два вопроса: какой объект PowerShell выбрал по имени и какой процесс унаследовал окружение.

\n

В 2018 году на Windows чаще использовался Windows PowerShell 5.1; те же базовые команды работают и в PowerShell 7.x, но значения по умолчанию для кодировки и поведение отдельных cmdlet могут различаться. Поэтому версия оболочки — часть наблюдения, а не деталь, которую можно опустить.

\n

Как PowerShell разрешает имя команды

\n

Если в команде нет пути и в текущей сессии есть несколько одноимённых команд, PowerShell применяет порядок приоритета: alias, функция, cmdlet, внешнее исполняемое приложение. Для внешнего приложения используются каталоги из $env:PATH, а на Windows расширения без явного суффикса сопоставляются с перечнем $env:PATHEXT. Путь, указанный явно, обходит это разрешение: PowerShell запускает файл по указанному адресу.

\n

Это важная граница: where.exe перечисляет подходящие файлы в текущем каталоге и PATH, но не видит функцию или alias профиля PowerShell. Поэтому результат where.exe node — инвентаризация файлов, а результат Get-Command node -All — список команд в порядке исполнения. Они дополняют друг друга.

\n
Схема разрешения команды в PowerShell: alias, функция, cmdlet или приложение; для приложения проверяются PATH и PATHEXT, а кодовая страница диагностируется отдельно
Одно имя распадается на две проверки: сначала выясняется выбранный объект и его файл, потом — как консоль и программа обменялись текстом.
\n

Первый снимок: не запускаем предположение

\n

Снимок нужно сделать в той консоли, где проявился сбой. Команда -All показывает скрытые совпадения и сохраняет порядок приоритета. Для where указываем суффикс .exe: иначе PowerShell может трактовать это имя как alias Where-Object.

\n
$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 означают, что на диске есть несколько кандидатов; они ещё не доказывают, какой из них выполнился.

\n

Воспроизводимая проверка приложения

\n

Не всякая команда принимает аргумент --version, поэтому сначала фиксируем тип объекта, а версию запрашиваем только у найденного приложения. В примере ниже действие не меняет окружение и возвращает код завершения отдельно от текста.

\n
$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

Что именно наследует новый процесс

\n

Переменные среды имеют область Process. Сеанс PowerShell получает её от родительского процесса, а дочерние программы наследуют копию при запуске. Присваивание $env:Path = ... меняет текущий процесс и программы, которые будут созданы из него; оно не переписывает системную или пользовательскую область Windows.

\n

Отсюда объясняется разница между окнами. Настройка, сделанная через интерфейс Windows, не превращает уже открытый терминал в новый процесс. Старое окно продолжает жить со своим снимком окружения. Откройте новый процесс или запускайте проверку в явно заданном окружении, иначе сравниваются разные входные данные.

\n
chcp и OutputEncoding
НаблюдениеЧто оно доказываетЧего оно не доказывает
Get-Command node -AllКакие команды доступны в сессии и в каком порядке они разрешаютсяЧто каждый найденный файл запускался
where.exe nodeКакие файлы найдены в текущем каталоге и PATHЧто alias или функция не перекрывает приложение
$env:PathКакой PATH видит текущий процессКакой PATH был у уже запущенного сборщика
node --versionСтроку, которую вернул данный вызовПричину падения сборки, DLL, права и lock-файл
Часть состояния консоли и обмена текстомКодировку каждого сохранённого файла
\n

Безопасный эксперимент с PATH

\n

Гипотеза должна быть узкой: «нужный каталог не является первым кандидатом для этого процесса». Проверяем её временно и с восстановлением исходного значения. Каталог в примере условный: перед запуском убедитесь, что в нём действительно лежит требуемый файл.

\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 не подтверждена: не добавляйте каталог в системную область, ищите следующую причину.

\n

Поворот расследования: путь совпал, текст — нет

\n

В исходной сцене версия могла совпасть в обоих окнах, а отличаться только русская ошибка. Это возвращает нас к другой ветке. chcp показывает активную кодовую страницу консоли. [Console]::OutputEncoding описывает вывод .NET, а автоматическая переменная $OutputEncoding относится к обмену PowerShell с внешними программами. Кодировка файла, который читает или пишет утилита, — отдельный вопрос.

\n
& 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 и контракт этой команды.

\n

chcp 65001 — эксперимент, а не универсальный ремонт. По документации Windows новые процессы используют назначенную кодовую страницу, тогда как уже запущенные программы могут продолжать использовать исходную. После изменения зафиксируйте новый процесс и повторите ровно тот же ввод; не меняйте одновременно кодировку файла, профиль и системный PATH.

\n

Симптом → причина → проверка → действие

\n
СимптомРабочая гипотезаПроверкаОграниченное действие
Версии node различаются в двух окнахРазные снимки процесса или каталог в PATHСравнить PID, $env:Path, Get-Command -AllОткрыть новый процесс; временно добавить проверенный каталог
where.exe показывает файл, но выполняется другая командаAlias или функция перекрывает приложениеGet-Command node -All, поле CommandTypeУстранить конфликт в профиле или вызвать проверенный путь
Версия совпадает, русское сообщение искаженоРазличается кодовая страница, OutputEncoding или кодировка файлаchcp, свойства Console, байты файлаМенять один слой и повторять тот же тест
Временный PATH не изменил результатСборка использует другой процесс, lock-файл, DLL или сетьПолный лог и окружение запуска сборщикаПрекратить правку PATH и перейти к следующей гипотезе
Смена кодовой страницы не помоглаПроблема в чтении/записи файла или API программыОпределить байты и настройку конкретного инструментаНе включать глобальную кодировку без контракта
\n

Порядок проверки

\n
  1. Сохраните каталог запуска, commit, полную команду, PID, версию PowerShell и текст ошибки.
  2. В той же сессии выполните Get-Command node -All и where.exe node; запишите Definition первого кандидата.
  3. Откройте PowerShell с -NoProfile и повторите снимок, чтобы отделить профиль от окружения.
  4. Сравните $env:Path и $env:PATHEXT в старом и новом процессах.
  5. Проверьте узкую гипотезу временным PATH или абсолютным путем, восстановив окружение через finally.
  6. Повторите исходную команду и запишите версию, код возврата, следующий симптом и полный лог.
  7. Если путь уже доказан, перейдите к chcp, Console, $OutputEncoding и кодировке конкретного файла.
  8. Постоянную настройку меняйте только после подтвержденного эксперимента; зафиксируйте требование в README или скрипте проекта.
\n

Ограничения и отрицательный путь

\n

Даже одинаковые путь и версия не делают два окружения идентичными. На сборку влияют разрядность, DLL, права, антивирус, прокси, кеш, локальные файлы, политика выполнения и lock-файл. Не отключайте защиту и не переустанавливайте Windows, пока наблюдение не указывает на такую причину.

\n

Профиль PowerShell может добавлять функции и alias, а сборщик может запускаться не из интерактивного окна — например, через отдельный процесс CI или IDE. Поэтому «в терминале работает» не означает «тот же объект запущен в сборке». Перед выводом сравните родителя процесса, рабочий каталог, переменные среды и точную команду.

\n

Критерий завершения диагностики

\n

Расследование закончено, когда можно показать четыре независимых факта: какой тип команды выбран, какой файл напечатал проверенную версию, какой процесс получил нужный PATH и какая кодировка относится к проблемному тексту. В финальной проверке новый процесс запускает тот же commit и исходную команду, получает ожидаемую версию и не возвращает исходную ошибку. Совпала только строка версии — значит, причина ещё не доказана.

\n

Проверяемые источники

" }