diff --git a/editorial/agent-rewrites/335.json b/editorial/agent-rewrites/335.json index e81b700..6f7e136 100644 --- a/editorial/agent-rewrites/335.json +++ b/editorial/agent-rewrites/335.json @@ -1,7 +1,7 @@ { "index": 335, "slug": "editorial-2018-09-mechanism-windows-dev-env", - "title": "Windows: почему одна команда запускает не тот бинарник", - "excerpt": "PowerShell выбирает команду из нескольких слоёв: профиля, alias, функций, PATH и PATHEXT. Разбираем, как увидеть фактический выбор, отделить кодировку консоли и проверить гипотезу без глобальной правки системы.", - "contentHtml": "
Симптом: в одной консоли node --version показывает ожидаемую версию, а в другой — старую. Иногда команда отрабатывает, но русское сообщение превращается в нечитаемые символы. Цена ошибки — не только одна неудачная сборка. Можно исправить не тот node.exe, повредить текстовый файл или навсегда засорить системный PATH ради разовой проверки.
Имя команды не равно конкретному файлу. PowerShell учитывает команды текущей сессии, затем ищет приложения через переменные среды. Кодировка вывода живёт рядом, но не управляет выбором бинарника. Эти слои надо проверять раздельно.
\nПервый вопрос диагностики: какой объект получил это имя в текущем процессе? Им может оказаться alias, функция, cmdlet, скрипт или приложение. Только последний вариант связывает команду с файлом на диске.
\nPATH задаёт каталоги для поиска исполняемых файлов. В Windows каталоги разделяет точка с запятой. PATHEXT перечисляет расширения, которые оболочка считает исполняемыми. Поэтому команда без расширения может привести к tool.exe или tool.cmd. До поиска в PATH PowerShell может выбрать функцию или alias с тем же именем.
Каждый процесс получает собственный набор переменных среды. Дочерний процесс наследует копию от родителя. Если в PowerShell выполнить $env:Path = ..., изменится текущая сессия и программы, запущенные из неё. Системные настройки от этого не меняются.
Изменение в окне настроек Windows не обновляет уже открытый терминал. Новая консоль получит новое значение, старая продолжит работать со старым. Поэтому после правки нужно открыть новый процесс. Иначе проверка сравнивает ожидание с устаревшим снимком.
\nGet-Command -Name node -All показывает все найденные команды в порядке приоритета PowerShell. Колонка CommandType отделяет приложение от функции, alias, cmdlet и скрипта. Первый элемент — кандидат, который оболочка выберет при обычном вводе имени.
where.exe node ищет файлы в текущем каталоге и каталогах из PATH. Он полезен для инвентаризации физических файлов, но не видит функцию из профиля PowerShell и не описывает полный приоритет оболочки. Поэтому одна команда не заменяет другую.
$names = @('node', 'npm', 'php', 'git')\nforeach ($name in $names) {\n Write-Host ('== ' + $name + ' ==')\n Get-Command -Name $name -All -ErrorAction SilentlyContinue |\n Select-Object CommandType, Name, Version, Source, Definition |\n Format-Table -AutoSize\n where.exe $name 2>$null\n if (Get-Command -Name $name -ErrorAction SilentlyContinue) {\n & $name --version\n Write-Host ('exitCode=' + $LASTEXITCODE)\n }\n}\nУчебный пример показывает способ наблюдения, а не production-результат. В реальном проекте список должен соответствовать README и lock-файлам. Аргумент --version подходит не каждой утилите. Для такой команды укажите документированный аргумент.
Безопасная гипотеза звучит так: проект запускает старый файл, потому что он раньше нужного каталога в PATH. Её проверяют в текущем процессе, не меняя системные настройки.
$requiredTools = 'C:\\Tools\\node-20'\nif (-not (Test-Path (Join-Path $requiredTools 'node.exe'))) {\n throw ('Нет node.exe: ' + $requiredTools)\n}\n$env:Path = $requiredTools + ';' + $env:Path\nGet-Command node -All | Select-Object CommandType, Definition, Version\nnode --version\n# Затем повторяется исходная команда проекта.\nПуть в примере условный. Его нельзя копировать без требования проекта и проверки файла. Команды меняют только текущий PowerShell. Если версия и исходная ошибка не изменились, гипотеза не подтверждена. Окно можно закрыть без отката постоянных настроек.
\nchcp показывает активную кодовую страницу консоли. [Console]::OutputEncoding.WebName показывает настройку вывода .NET. Кодировка сохранённого файла — третья сущность. Она может не совпадать ни с одной из двух.
Нечитаемый текст только в одной консоли не доказывает неправильный бинарник. Сначала зафиксируйте кодовую страницу, настройку вывода и байты конкретного файла. Не переключайте системный язык и не добавляйте UTF-8 в глобальные настройки до проверки. Старое приложение может ожидать другую кодировку.
\n& cmd.exe /d /c chcp\n[Console]::OutputEncoding.WebName\n[Text.Encoding]::Default.WebName\n$env:Path = 'C:\\Tools\\node-20;' + $env:Path\nGet-Command node -All\nnode --version\nЭти команды дают снимок процесса и консоли. Они не исправляют кодировку файла и не доказывают совместимость всей сборки. Если проблема относится к файлу, откройте его байты или настройку конкретной программы. Если к выводу — повторите запуск в новом процессе.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
node --version показывает старую версию | Другой кандидат стоит раньше в PATH | Get-Command node -All и where.exe node | Временно поставить документированный каталог первым |
where.exe показывает файлы, но запускается функция | Профиль перекрывает приложение | CommandType и запуск с -NoProfile | Проверить профиль, не менять PATH наугад |
| Русский вывод нечитаем только в одной консоли | Различается кодовая страница или OutputEncoding | chcp и свойства Console | Сравнить новый процесс и кодировку файла |
| После правки результат прежний | Работает старый процесс | Новая сессия без профиля | Повторить исходную команду в новом окне |
| Версия совпала, но сборка падает | Причина в lock-файле, правах, DLL или сети | Полный лог следующей ошибки | Прекратить правку PATH |
Get-Command -All, where.exe и команду версии.-NoProfile и повторите наблюдения.$env:Path текущего процесса.chcp, OutputEncoding и кодировку файла.Совпадение команды, пути и версии не делает машины одинаковыми. На результат влияют разрядность, DLL, права, антивирус, прокси, кеш, локальные файлы и политика выполнения. Не отключайте защиту и не переустанавливайте Windows, пока отдельное наблюдение не укажет на такую причину.
\nchcp 65001 не является универсальным решением. Старые программы могут читать ввод и писать вывод по собственным правилам. Если смена кодовой страницы не меняет симптом, вернитесь к байтам файла и API чтения. Если временный PATH не меняет сборку, перестаньте редактировать PATH.
Диагностика завершена, когда можно показать, какой объект выбран, какой файл напечатал проверенную версию и какая кодировка относится к проблемному тексту. После исправления новый PowerShell повторяет исходную команду с тем же commit, получает требуемую версию и не возвращает исходную ошибку. Если совпала только версия, готовность не достигнута.
\nСцена: разработчик открывает 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 и исходную команду, получает ожидаемую версию и не возвращает исходную ошибку. Совпала только строка версии — значит, причина ещё не доказана.