Plank help · updated 2026-09-17

Запуск команд на машине

Что реально установлено на машине рабочего пространства, а чего нет: имя интерпретатора, где лежат пакеты Python, каких утилит не хватает и куда можно писать временные файлы.

Agents: fetch the raw markdown of this page at /ru/help/running-commands.md

Запуск команд на машине

Здесь собраны факты о машине рабочего пространства, из-за которых чаще всего теряется ход. Каждый наблюдался как реальная неудачная первая попытка. Прочитайте перед первым вызовом bash в рабочем пространстве и считайте это важнее любых предположений, перенесённых из другого окружения.

О том, что машина собой представляет — где работает, что сохраняется, как устроен вход в сервисы, — см. Среда вашего ассистента.

Что есть, а чего нет

ПредположениеКак на самом деле
pythonНе существует. Есть только python3. Голый python падает с command not found.
Пакеты Python стоят глобальноДополнительные пакеты ставятся в пользовательский site-packages (~/.local/lib/python3.X/site-packages) командой pip install --user — это домашняя папка, которая у общего командного чата не та же, что у личного.
PYTHONNOUSERSITE=1 безобиденОн ломает все доустановленные пакеты, включая все библиотеки Google. Никогда его не задавайте.
file, rg, java, xz доступныНичего из этого не установлено. Для разбора файлов используйте python3, вместо rggrep (или инструмент grep).
/tmp/opencode доступен на записьОн принадлежит root, а вы работаете под обычным пользователем. Пишите во временные файлы вида /tmp/<имя> или, лучше, прямо в рабочее пространство.

Предустановлено и на это можно опираться: python3openpyxl и requests в системном пути), node, curl, grep, sed, awk, tar, gzip, а также набор инструментов для документов, описанный ниже.

Пакет, поставленный через pip install --user, ведёт себя не так, как предустановленный. openpyxl и requests лежат в системном пути и переживают всё. Всё, что вы доустановили сами, включая клиентские библиотеки Google, лежит в пользовательском site-packages и исчезает в тот момент, когда задан PYTHONNOUSERSITE. Эта разница незаметна ровно до того момента, когда становится заметной.

Проверяйте свои документы, посмотрев на них

Файл .pptx, .docx или .xlsx можно превратить в PDF, а затем в картинку — поэтому смотрите на собранный документ, а не предполагайте, что он получился правильно. Текст, вылезший за границы блока, заголовок, перенесённый на три строки, диаграмма, закрывшая подпись, — ничего из этого не видно в файле, который вы записали, и всё это видно на картинке.

# 1. конвертация (обе переменные окружения обязательны, см. ниже)
SAL_USE_VCLPLUGIN=svp soffice -env:UserInstallation=file:///tmp/lo \
  --headless --convert-to pdf deck.pptx --outdir /tmp
# 2. растрируем страницу, которую можно открыть и рассмотреть
pdftoppm -png -r 110 /tmp/deck.pdf /tmp/page
  • Обе переменные обязательны. Просто soffice падает с «User installation could not be completed»; -env:UserInstallation даёт ему профиль, доступный на запись, а SAL_USE_VCLPLUGIN=svp выбирает headless-бэкенд. Дисплея к машине не подключено.
  • Запускайте по одному файлу за раз. Два процесса soffice с общим каталогом профиля будут мешать друг другу; если запуски всё же пересекаются, задайте каждому свой путь -env:UserInstallation.
  • Также доступны: pdftotext (проверить, что текст действительно попал в файл), markitdown (выгрузить собранный документ обратно в markdown — удобно, чтобы поймать забытый текст-заглушку), pandoc, wkhtmltopdf.

Для HTML используйте Chromium, а не wkhtmltopdf. wkhtmltopdf — это форк WebKit 2012 года: flexbox и grid поддержаны частично, веб-шрифты работают ненадёжно, поэтому страница, которая в браузере выглядит правильно, может отрендериться сломанной. chrome-headless-shell установлен и печатает в том размере страницы, который задан правилом @page самого документа:

chrome-headless-shell --no-sandbox --disable-gpu --no-pdf-header-footer \
  --user-data-dir=/tmp/chrome-$$ --print-to-pdf=/tmp/out.pdf /abs/path/page.html
  • --no-sandbox обязателен — контейнер не даёт user namespaces, которые нужны собственной песочнице Chromium, и без этого флага браузер завершится, не загрузив страницу.
  • Каждому запуску — свой --user-data-dir. Два процесса Chromium с общим профилем блокируют его, и второй ничего не запишет.
  • Размер страницы задаёт документ, а не команда. Флага для этого нет: пропишите @page { size: 1280px 720px; margin: 0 } в CSS, иначе получите US Letter.
  • Никогда не используйте repeating-linear-gradient, repeating-radial-gradient и conic-gradient на странице, которую собираетесь печатать. Градиент в PDF — это переход цвета вдоль прямой или окружности, поэтому linear-gradient и radial-gradient переводятся напрямую и ничего не стоят. Эти три не переводятся, и Chromium заменяет их шейдингом, цвет которого вычисляет PostScript-программа, — её читалка выполняет для каждого пикселя. Preview на macOS и iOS растрирует всю ячейку паттерна до отсечения, поэтому цена определяется размером блока, на котором висит градиент, а не тем, сколько его видно. Замеренная колода из двенадцати слайдов открывалась 43 секунды, из них 26 — титульный слайд, и 27 секунд из 43 ушли на градиент, который был полностью отсечён и не нарисовал ни одного пикселя. До конца отрисовки страница остаётся пустой, поэтому это выглядит как битый файл, а не как медленный. Для сетки в волосяную линию или для полос используйте повторяющийся SVG в background-image (data:-URI с одной плиткой) — он остаётся векторным и рисуется мгновенно; для развёртки используйте linear-gradient под углом.
  • Проверьте напечатанный PDF — в исходнике этого не видно: python3 -c "import re,sys;print(len(re.findall(rb'/ShadingType\s+1\b',open(sys.argv[1],'rb').read())))" out.pdf. Любое значение больше 0 означает, что страница будет открываться медленно. plank_deck_qa.py делает эту проверку сам и заваливает колоду по ней.

Шрифты, если в документе есть русский или казахский текст. Используйте Arial, Calibri, Times New Roman, Courier New или Inter — для каждого установлена замена с полным покрытием кириллицы, включая Әә Ғғ Ққ Ңң Өө Ұұ Үү Һһ Іі. Избегайте Cambria: замены для неё здесь нет, поэтому она незаметно подменяется посторонним начертанием, и вёрстка, которую видите вы, — не та, что получит пользователь. Если на растрированной странице текст выглядит как квадратики или интервалы явно неправильные, меняйте шрифт, а не отправляйте файл как есть.

Node-пакеты для сборки документов уже стоят глобально. pptxgenjs, sharp, react, react-dom и react-icons установлены на уровне образа, а NODE_PATH задан, так что require('pptxgenjs') работает откуда угодно. Не ставьте их через npm install в рабочее пространство: это медленно, создаёт тысячи файлов на сетевом хранилище и не нужно.

Две привычки, которые экономят больше всего ходов

  • Гасите лишний вывод максимально узко. Библиотеки Google на каждом запуске печатают FutureWarning про версию Python. Заглушайте его через PYTHONWARNINGS=ignore — и никогда через PYTHONNOUSERSITE, который «убирает предупреждение» вместе с самой библиотекой.
  • Пишите файл скрипта вместо длинного однострочника python3 -c. Однострочники с вложенными кавычками — вторая по частоте причина падений здесь: оболочка превращает их в SyntaxError или разбивает на посторонние команды. Файл к тому же можно запустить повторно, а с заголовком @plank-integration он появится в боковой панели пользователя. См. Скрипты рабочего пространства и боковая панель.

Оболочка неинтерактивна — команда, ждущая ввода, вешает ход

К машине не подключён терминал, и ответить на запрос некому. Команда, которая останавливается и что-то спрашивает — код входа, учётные данные, подтверждение [y/N], пейджер в ожидании нажатия клавиши, — не получает ответа и висит, пока ход не завершат принудительно (примерно через 30 минут), а пользователь видит это как проваленный ход. Считайте любую интерактивную команду убийцей хода.

  • Никогда не запускайте интерактивный входgcloud auth login, az login, клиент базы данных, спрашивающий пароль. Для OAuth используйте релей входа: сгенерируйте URL, дайте пользователю подтвердить его в своём браузере и завершите в следующем ходе, когда он вставит обратно URL перенаправления. Не держите процесс живым в ожидании.
  • Обычные виновники заранее настроены падать быстро, а не висеть. Образ задаёт CLOUDSDK_CORE_DISABLE_PROMPTS=1 (gcloud не спрашивает), GIT_TERMINAL_PROMPT=0 (git не запрашивает учётные данные) и PAGER=cat (без пейджера). Когда что-то из этого падает с ошибкой вместо зависания — прочитайте ошибку и не запускайте ту же интерактивную команду снова. Но не полагайтесь на то, что это перехватит любой инструмент: для всего остального всё равно берите явный неинтерактивный флаг ниже.
  • Для всего остального включайте неинтерактивный режим сами: передайте флаг инструмента (-y, --yes, --no-input, --non-interactive) или перенаправьте ввод из пустоты через < /dev/null. Если задачу действительно нельзя выполнить без интерактивного ввода, честно скажите об этом, а не вешайте ход.

Как запускать долгую задачу в фоне

Задача на несколько минут — синхронизация, массовый импорт, долгая отрисовка — должна уходить в фон, чтобы ход оставался свободным и вы могли отвечать пользователю, пока она идёт. Чтобы это действительно случилось, нужны две вещи, и каждая из них уже ломалась по отдельности — с замерами.

1. Запуск — это отдельный вызов bash. Перед ним в строке ничего нет: ни cd … &&, ни … ;, ни другой команды. A && B & не отправляет в фон B — он отправляет туда весь список, в подоболочке, которая наследует канал вывода инструмента, и вызов не вернётся, пока этот канал не закроется. Замер на шестисекундной задаче: сама оболочка завершается через 0.012 с, а канал закрывается через 6.02 с — то есть вызов занимает все шесть секунд. В проде фоновая синхронизация, запущенная составным выражением, удерживала один вызов bash 616.327 с; тот же запуск отдельным вызовом возвращается за 0.284 с. Пишите абсолютные пути или передавайте параметр workdir инструменту оболочки — но не cd. Инструмент определяет рабочий каталог на каждый вызов, и его собственная инструкция велит не писать cd <каталог> && <команда>, а передавать workdir: cd из прошлого вызова в этот не переносится. (; перед запуском такую подоболочку не порождает и возвращается сразу — правило запрещает его всё равно: иначе приходится каждый раз выводить приоритет & и && в момент написания запуска, а одна всегда верная форма не стоит ничего. Запрет && и ; — про запуск: обычные команды, которые ничего не отправляют в фон, можно соединять как угодно. Правило про cd — нет: оно действует для любого вызова, потому что ни один вызов не наследует каталог предыдущего.)

2. Оба потока вывода идут в файл журнала. > job.log 2>&1. Задача, у которой stdout остался каналом инструмента, держит вызов открытым ровно так же — с & или без: замер показывает, что nohup sleep 6 & без перенаправления возвращается через 6.0 с.

mkdir -p /workspace/logs                # отдельным вызовом: перед запуском ничего быть не должно
nohup python3 -u /workspace/scripts/sync.py > /workspace/logs/sync.log 2>&1 &
echo "started pid $!"

Каталога /workspace/logs/ в новом рабочем пространстве нет — создайте его отдельным вызовом до запуска. Если каталога для перенаправления не существует, запуск не запишет вообще ничего, и вы будете опрашивать журнал, который никогда не появится.

-u здесь несущий, а не украшение. Когда stdout Python указывает на файл, он буферизуется блоками, поэтому задача, печатающая строку прогресса каждые 15 секунд, не пишет в журнал вообще ничего, пока не завершится или не наберёт ~8 КБ. Замер: через три секунды после старта задачи, печатающей раз в секунду, журнал пуст без -u и содержит три строки с ним. Пустой журнал неотличим от задачи, которая не запустилась, — именно его нашли пять из шести чтений журнала в том инциденте. Для не-Python используйте stdbuf -o0 -e0 <команда> — это снимает буферизацию с обоих потоков. Не -oL и не -eL: построчная буферизация сбрасывает по переводу строки, а команда, печатающая прогресс как \rprogress 42, его не пишет — с любым из этих флагов вывод остаётся в буфере ровно так же, как если бы stdbuf не было вовсе: замер даёт 0 байт за 3 секунды в обоих случаях против 44 с -o0. -o0 не дороже -oL: оба укладываются примерно в 285 мс на 200 000 строк, а разброс между прогонами шире разницы между ними.

Фоновая задача сообщает о прогрессе не реже чем раз в 15 секунд

Пользователь спрашивает «работает?» задолго до конца задачи, и отвечаете вы по журналу. Значит, журнал должен что-то сказать в течение 15 секунд после запуска и дальше не реже чем раз в 15 секунд. Задача, которая печатает только в конце, невидима: в том инциденте это дало 319 секунд слепого опроса, пять пустых чтений журнала из шести и три сообщения пользователя «оно ещё идёт?», на которые нечем было ответить.

  • Скрипт, который вы написали сами: печатайте строку с отметкой времени на каждой границе шага и не реже раза в 15 секунд — и не забывайте -u, иначе она не выйдет из буфера. Это тот случай, которым вы управляете, и именно он важен: сделайте так, чтобы задача говорила сама.
  • Команда, которую вы изменить не можете: заставить её говорить нельзя — и не делайте вид, что можно. Без всякой обёртки вокруг неё честно наблюдаются две вещи:
    • размер журнала и время его изменения. Рост между двумя опросами — это движение; отсутствие роста не доказывает, что задача зависла, но именно об этом вы и сообщаете.
    • жив ли запущенный вами процесс. Запуск напечатал его pid, так что kill -0 <pid> отвечает на это одной командой.
kill -0 12345 2>/dev/null && echo "ещё работает" || echo "запущенный мной процесс завершился"
ls -l --time-style=+%H:%M:%S /workspace/logs/sync.log
  • Сообщайте о том, что наблюдали, а не об итоге, которого не видели. «Процесс работает, журнал вырос на 4 КБ за последнюю минуту» — честно. «Синхронизация завершилась» — утверждение, которое можно делать, только если об этом сказала сама задача в журнале. Команда, уходящая в собственную сессию, с точки зрения вашей оболочки завершается сразу, а работа продолжается, — поэтому исчезнувший pid означает запущенный мной процесс завершился, а не задача готова.
  • Дальше опрашивайте, а не гадайте. plank_wait на 15-30 секунд, затем tail -n 20 /workspace/logs/sync.log — и расскажите пользователю, что там на самом деле написано. plank_wait заканчивается в тот момент, когда пользователь пишет в чат, поэтому его сообщение доходит до вас за секунды.
  • Если журнал не появился вовсе, запуск не состоялся: перечитайте вывод самой команды запуска — оболочка сообщает о неудачном перенаправлении (например, об отсутствии /workspace/logs) прямо там.
  • Чего это не делает: задача от этого не заканчивается быстрее. Собственная фоновая синхронизация коннектора была замерена в 463-958 секунд и занимает ровно столько же. Меняется другое: вы можете отвечать, пока она идёт, а пользователь видит движение.

Учётные данные не попадают в вывод команды

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

  • Никогда не трассируйте команду, читающую файл с учётными данными. bash -x, set -x и sh -x печатают каждое присваивание, которое выполняют, — так что трассировка скрипта с source .env напечатает весь файл. Оболочка вас не остановит: машина намеренно не подменяет source, потому что это меняет поведение всех остальных подключаемых файлов. Здесь всё зависит от вас.
  • Читайте одно значение, а не файл. grep -m1 '^POSTHOG_PERSONAL_API_KEY=' .env | cut -d= -f2- в переменную — и дальше работайте с переменной. Никогда не делайте cat, echo, head или printf по файлу с учётными данными и не выводите переменную с ключом, чтобы проверить, задана ли она: проверяйте через [ -n "$VAR" ].
  • В командной строке ключам тоже не место. curl -H "Authorization: Bearer $TOKEN" — нормально; вставленный туда сам токен — нет, потому что команда показывается рядом со своим выводом.
  • Если что-то всё же просочилось, скажите об этом в том же ходе. Plank затирает значения, которые опознаёт как учётные данные, до того как они сохранятся, — на месте значения вы увидите [plank:secret-redacted]. Это подстраховка, а не разрешение: назовите пользователю, какой ключ засветился, чтобы он его перевыпустил, — действующий ключ остаётся утечкой.

Если чего-то действительно не хватает

Ставьте через pip install --user <пакет> для Python или npm install --prefix scripts/<provider> <пакет> для Node, чтобы пакет лежал рядом со скриптом, которому он нужен. Никогда не ставьте пакеты в .opencode/node_modules — это дерево плагинов образа, и оно сбрасывается из образа. Пакет из pip install --user лежит в домашней папке, которая у общего командного чата не та же, что у личного, поэтому для скрипта, который запускают чаты обоих видов, его, возможно, придётся поставить заново. Не тянитесь к apt-get: вы не root, да и системный пакет всё равно не переживёт пересоздание машины. Если задача действительно требует отсутствующей программы, честно скажите об этом, а не жгите ходы на обходные пути.