From 781880a95ef55e50cd3c6ccd8abbbbea72693cd7 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Fri, 4 Sep 2026 00:38:15 +0300 Subject: [PATCH] Editorial: revise article 335 for 10/10 quality --- editorial/agent-rewrites/335.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 ради разовой проверки.

\n

Имя команды не равно конкретному файлу. PowerShell учитывает команды текущей сессии, затем ищет приложения через переменные среды. Кодировка вывода живёт рядом, но не управляет выбором бинарника. Эти слои надо проверять раздельно.

\n

Тезис: сначала докажите, что именно запустилось

\n

Первый вопрос диагностики: какой объект получил это имя в текущем процессе? Им может оказаться alias, функция, cmdlet, скрипт или приложение. Только последний вариант связывает команду с файлом на диске.

\n

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

\n

Механизм процесса и наследование

\n

Каждый процесс получает собственный набор переменных среды. Дочерний процесс наследует копию от родителя. Если в PowerShell выполнить $env:Path = ..., изменится текущая сессия и программы, запущенные из неё. Системные настройки от этого не меняются.

\n

Изменение в окне настроек Windows не обновляет уже открытый терминал. Новая консоль получит новое значение, старая продолжит работать со старым. Поэтому после правки нужно открыть новый процесс. Иначе проверка сравнивает ожидание с устаревшим снимком.

\n
Схема выбора команды в PowerShell: процесс получает PATH и PATHEXT, Get-Command показывает команды, where.exe ищет файлы, а кодовая страница проверяется отдельно
Путь к бинарнику и отображение текста — две ветви одной диагностики. Совпадение версии не доказывает, что консоль правильно прочитала файл.
\n

Get-Command и where.exe отвечают на разные вопросы

\n

Get-Command -Name node -All показывает все найденные команды в порядке приоритета PowerShell. Колонка CommandType отделяет приложение от функции, alias, cmdlet и скрипта. Первый элемент — кандидат, который оболочка выберет при обычном вводе имени.

\n

where.exe node ищет файлы в текущем каталоге и каталогах из PATH. Он полезен для инвентаризации физических файлов, но не видит функцию из профиля PowerShell и не описывает полный приоритет оболочки. Поэтому одна команда не заменяет другую.

\n
$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 подходит не каждой утилите. Для такой команды укажите документированный аргумент.

\n

Проверка гипотезы через процессный PATH

\n

Безопасная гипотеза звучит так: проект запускает старый файл, потому что он раньше нужного каталога в PATH. Её проверяют в текущем процессе, не меняя системные настройки.

\n
$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. Если версия и исходная ошибка не изменились, гипотеза не подтверждена. Окно можно закрыть без отката постоянных настроек.

\n

Кодовая страница — отдельная ветвь

\n

chcp показывает активную кодовую страницу консоли. [Console]::OutputEncoding.WebName показывает настройку вывода .NET. Кодировка сохранённого файла — третья сущность. Она может не совпадать ни с одной из двух.

\n

Нечитаемый текст только в одной консоли не доказывает неправильный бинарник. Сначала зафиксируйте кодовую страницу, настройку вывода и байты конкретного файла. Не переключайте системный язык и не добавляйте 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

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

\n
СимптомПричинаПроверкаДействие
node --version показывает старую версиюДругой кандидат стоит раньше в PATHGet-Command node -All и where.exe nodeВременно поставить документированный каталог первым
where.exe показывает файлы, но запускается функцияПрофиль перекрывает приложениеCommandType и запуск с -NoProfileПроверить профиль, не менять PATH наугад
Русский вывод нечитаем только в одной консолиРазличается кодовая страница или OutputEncodingchcp и свойства ConsoleСравнить новый процесс и кодировку файла
После правки результат прежнийРаботает старый процессНовая сессия без профиляПовторить исходную команду в новом окне
Версия совпала, но сборка падаетПричина в lock-файле, правах, DLL или сетиПолный лог следующей ошибкиПрекратить правку PATH
\n

Порядок действий

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

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

\n

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

\n

chcp 65001 не является универсальным решением. Старые программы могут читать ввод и писать вывод по собственным правилам. Если смена кодовой страницы не меняет симптом, вернитесь к байтам файла и API чтения. Если временный PATH не меняет сборку, перестаньте редактировать PATH.

\n

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

\n

Диагностика завершена, когда можно показать, какой объект выбран, какой файл напечатал проверенную версию и какая кодировка относится к проблемному тексту. После исправления новый PowerShell повторяет исходную команду с тем же commit, получает требуемую версию и не возвращает исходную ошибку. Если совпала только версия, готовность не достигнута.

\n

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

" + "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

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

" }