Основные причины
Прежний редактор загружал рабочий файл в родительский каталог цели,
куда учетная запись SSH в /etc или /var часто не имеет права записи. Переименование
через SFTP не давало ни авторизованного пути через sudo, ни проверки конфликта с исходным
состоянием, ни проверки метаданных безопасности, ни надежной семантики перезаписи.
Обработчик ошибок удалял временную копию и тем самым уничтожал материал для восстановления
даже тогда, когда завершение было неопределенным.
Ранее статус сеанса показывал только состояние соединения. Индикатор терминала не получал ни измерения ICMP, ни измерения отклика аутентифицированного SSH.
Протокол сохранения
- Фиксированная вспомогательная программа SSH открывает обычный файл с единственной жесткой ссылкой,
используя операции относительно дескриптора и
O_NOFOLLOWдля каждого компонента пути. Она фиксирует содержимое, хеш SHA-256, устройство/inode, UID, GID, все биты прав доступа, наносекундные времена изменения содержимого и статуса, а также все читаемые расширенные атрибуты. Core проверяет хеш и декодирует строгий UTF-8 с сохранением BOM. Каждый документ получает непрозрачныйeditId, привязанный к своему плагину, сеансу терминала и идентичности сервера. - Непривилегированная подготовительная операция определяет домашний каталог аутентифицированной учетной записи
через
pwd.getpwuid(os.getuid()), создает$HOME/ch-j-sm/с режимом0700и создает закрытый эксклюзивный журнал восстановления. Небезопасный домашний каталог, рабочая область в виде символической ссылки или рабочая область другой учетной записи отклоняются. - Core загружает через SFTP скрытый случайный 128-битный рабочий файл
.tmpс эксклюзивным созданием (wx), режимом0600и шестидесятисекундным тайм-аутом канала/загрузки. Журнал содержит цель, исходное состояние и ожидаемый хеш рабочей копии; учетных данных он не содержит. - Весь этап завершения выполняется одним вызовом SSH exec, при необходимости через существующий исполнитель sudo. Вспомогательная программа безопасно открывает закрытую рабочую копию, вычисляет ее хеш и сбрасывает на диск. Она блокирует исходный inode, проверяет его исходное состояние, создает эксклюзивный файл назначения рядом с целью, записывает все содержимое, применяет и проверяет метаданные, еще раз проверяет оригинал и сбрасывает замену на диск.
os.replaceв том же каталоге атомарно заменяет цель. Родительский каталог сбрасывается на диск, после чего проверяются итоговый путь, хеш, inode и метаданные. Исходный путь остается доступным до замены. Рабочий файл никогда не перемещается между файловыми системами и никогда не удаляется на этапе завершения.- После того как Core получает проверенный успех, отдельная очистка повторно проверяет итоговое
содержимое и метаданные безопасности и только затем удаляет рабочий файл и журнал.
Сбой очистки все равно сообщается как подтвержденное сохранение с
recoveryAvailable: true.
Неудачная загрузка, неудачный sudo, конфликт, ошибка метаданных или неудачная замена сохраняют оригинал и весь подготовленный материал. Неполные загрузки помечаются как таковые и не могут быть восстановлены как полный отредактированный документ. Потеря связи SSH, отсутствие кода завершения, тайм-ауты и неожиданные результаты завершения сообщаются как неизвестные, но никогда как сохраненные. Core блокирует повторное сохранение документа с неопределенным результатом; автоматического повтора замены нет.
Метаданные, привилегии и границы безопасности
Вспомогательная программа сохраняет и проверяет UID, GID и все биты прав POSIX, включая биты исполнения, setuid, setgid и sticky. Расширенные атрибуты Linux воспроизводятся байт в байт, включая POSIX ACL, capabilities и метки SELinux. Владелец и содержимое файла устанавливаются до атрибутов безопасности, поскольку запись/chown могут сбросить capabilities или специальные биты прав. Унаследованные атрибуты файла назначения, которых нет у оригинала, удаляются. Время изменения отражает новую запись и не копируется с оригинала.
Нечитаемый или непредставимый атрибут прерывает операцию; метаданные никогда не отбрасываются молча. Подписи IMA/EVM явно отклоняются, поскольку копирование подписей целостности, привязанных к содержимому/inode, сделало бы их недействительными. Символические ссылки в пути цели или домашнего каталога, файлы с несколькими жесткими ссылками и необычные файлы отклоняются. Не используется ни перезапись на месте, ни более слабый резервный вариант перезаписи через SFTP.
Обычные сохранения с правом записи выполняются от имени учетной записи SSH. Защищенные сохранения требуют явного
{ sudo: true }; пароль sudo необязателен для sudo без пароля. Пароли
проверяются и передаются только через stdin канала, но никогда в команде, журнале,
рабочем файле, логе или удаленной ошибке. Сеансы SSH от root используют существующий прямой
исполнитель root. Неудачная авторизация оставляет оригинал и рабочую копию нетронутыми.
Все сохранения, выполняемые от root (прямой SSH от root или sudo), дополнительно отклоняют каталоги
цели/предков, принадлежащие другим учетным записям, доступные им на запись или имеющие ACL доступа,
вместо того чтобы доверять привилегированному пути, контролируемому другой учетной записью.
Фиксированная вспомогательная программа запускается с python3 -I -B: она игнорирует пользовательские пути Python,
рабочий каталог и пользовательские site packages и не записывает байт-код. Пути и аргументы JSON
экранируются для оболочки в Core. Плагины никогда не получают произвольный SSH exec или
дескрипторы SFTP. Существующие песочница, CSP, разрешения манифеста, проверка ключа хоста
и проверка обновлений продолжают действовать.
Plugin API и оставшаяся интеграция Monaco
Отдельно распространяемый интерфейс Monaco/File Manager отсутствует в этом checkout. Его интеграцию в UI, запрос sudo, обработку несохраненного буфера и переводы здесь не удалось реализовать или протестировать. Встроенным исходником плагина является плагин Hash & Checksum. Никакой замены исходников File Manager не придумывалось.
Существующие методы остаются доступными. Дополнительные методы/параметры предоставляют preload в песочнице и среда выполнения с проверкой разрешений:
const opened = await chjPlugin.files.readText(sessionId, path);
// For a protected read, explicitly pass { sudo: true, sudoPassword }.
const result = await chjPlugin.files.saveText(sessionId, path, editor.getValue(), {
editId: opened.editId,
sudo: true, // Include only after the user authorizes elevation.
sudoPassword // Omit for passwordless sudo; never persist this value.
});
if (result.ok && result.value.status === "saved") {
// Mark this exact submitted editor version as saved. Later edits stay dirty.
} else {
// Keep the Monaco model dirty. Translate result.error.code/status.
// result.error.recoveryId/recoveryPath identify retained working material.
}
await chjPlugin.files.closeText(opened.editId);
const recovery = await chjPlugin.files.listRecovery(sessionId);
const copy = await chjPlugin.files.readRecovery(sessionId, recoveryId);
// Review copy.status and copy.text; open a fresh document before another save.
await chjPlugin.files.cleanupRecovery(sessionId, recoveryId);
// Cleanup rejects anything whose target is not currently confirmed.
Методы восстановления принимают необязательный последний аргумент { sudo: true, sudoPassword },
если проверка цели требует повышения прав. Они сохраняют рабочую область аутентифицированного
пользователя даже при выполнении через sudo. Журналы переживают перезапуск приложения и повторное
подключение SSH; восстановление никогда не повторяет завершение автоматически. Перед явным сохранением
сравните восстановленный буфер с заново открытой целью.
files.read защищает чтение, проверку и освобождение дескрипторов. files.write защищает
сохранение и очистку восстановления. writeText для существующих плагинов по-прежнему отклоняет промис при сбое;
saveText возвращает структурированную оболочку { ok, value/error }, поскольку
Electron отбрасывает пользовательские свойства отклоненных объектов Error. Устаревший вызов writeText с тремя аргументами
может использовать исходное состояние единственного открытого документа. Несколько активных документов
для одного пути требуют editId; неоднозначные устаревшие вызовы безопасно завершаются ошибкой, вместо того чтобы
выбрать исходное состояние другого документа. Плагинам следует освобождать неиспользуемые дескрипторы.
Внешний интерфейс должен сразу показывать сохранение, при сбое оставлять буферы несохраненными, предлагать sudo только после ошибок прав/аутентификации, показывать конфликты без перезаписи и предлагать восстановление после потери соединения или неизвестного результата. Он должен переводить эти состояния на чешский, немецкий и английский. Предлагаемые тексты состояний:
| Состояние | Чешский | Немецкий | Английский |
|---|---|---|---|
| Сохранение | Ukládání… | Wird gespeichert… | Saving… |
| Подтверждено | Uloženo | Gespeichert | Saved |
| Сбой | Uložení selhalo | Speichern fehlgeschlagen | Save failed |
| Требуется sudo | Vyžadováno oprávnění sudo | sudo-Autorisierung erforderlich | Sudo authorization required |
| Конфликт | Původní soubor byl změněn | Originaldatei wurde geändert | Original file changed |
| Отключено | Spojení bylo přerušeno | Verbindung unterbrochen | Connection lost |
| Неизвестно | Výsledek uložení není znám | Speicherergebnis unbekannt | Save result unknown |
| Восстановление | Obnova je dostupná | Wiederherstellung verfügbar | Recovery available |
Определения задержки и жизненный цикл
Ping — задержка, сообщаемая реальным ответом ICMP echo на стороне клиента
от разрешенного IP-адреса, который использует SSH. Core асинхронно запускает системную программу ping
с фиксированным массивом аргументов, без оболочки и без запуска с правами администратора.
Аргументы для Linux, macOS (включая ping6) и Windows выбираются отдельно.
Заблокированный ICMP, отсутствующая программа, тайм-аут, некорректный вывод или ответ Windows
только с верхней границей, например <1ms, дают недоступное значение, а не выдуманное число.
SSH RTT — монотонная длительность performance.now() фиксированного запроса exec true
через существующий аутентифицированный клиент SSH. Она включает установку канала,
обработку на сервере и доставку ответа; это не чистое сетевое RTT и не длительность
входа по SSH. Переведенная подсказка индикатора объясняет это различие.
Мониторинг начинается сразу после подключения терминала. Пробы выполняются параллельно и асинхронно с пятисекундным тайм-аутом запроса/процесса. Следующее измерение планируется через двенадцать секунд после завершения предыдущего, поэтому они не перекрываются. Отключение прерывает ICMP, сбрасывает таймеры, отменяет незавершенную работу SSH и очищает значения. Проверки идентичности записи не позволяют прежнему соединению публиковать значения после переподключения. Сбой ICMP никогда не меняет состояние соединения SSH.
Задержка передается внутри существующего события state / данных IPC ssh:state:
latency: { pingMs: 24.1, sshRttMs: 31.25, measuredAt: "…" }
Недоступные значения равны null. Компактный индикатор терминала использует миллисекунды,
локализованный текст недоступности и подписи на чешском/немецком/английском. При отключении он скрывается.
Дополнительные возможности выполнения в renderer или запросы IPC не потребовались.
Проверка и эксплуатационные ограничения
- Запуск из
app/:npm ci, затемnpm test. - Итоговый локальный результат: 179 тестов; 163 пройдено, 0 провалено, 16 пропущено. Все пропуски
— это сценарии файловой системы Linux на данном хосте macOS.
npm ci, проверка синтаксиса Python иgit diff --checkзавершились успешно. - Тесты покрывают сохранение/восстановление в Core, протокол sudo и скрытие паролей, реальное экранирование враждебных путей для оболочки POSIX, конфликты, загрузки, неудачную очистку, неопределенные результаты, параллельные документы, разрешения IPC и жизненный цикл задержки.
- Тесты файловой системы Linux запускают поставляемую точку входа на Python с закрытым
тестовым поиском домашнего каталога: владелец/режим/xattrs, POSIX ACL, символические/жесткие ссылки,
FIFO, сбои загрузки и метаданных, сбои замены, гонки на цели, неизвестные
результаты после замены и восстановление. Подготовка между файловыми системами использует
/dev/shmи другую файловую систему, если она доступна. На хостах без Linux эти тесты пропускаются; отсутствие поддержки ACL/отдельной файловой системы пропускается явно. - Добавленное задание GitHub Actions для Linux запускает весь набор и повторяет сценарии файловой системы от root. Этот workflow из данного рабочего дерева не запускался.
- Локальная проверка выполнялась на macOS. Синтаксис исходников Python проверялся напрямую
исполняемым файлом Python из Command Line Tools. Реальная smoke-проверка ICMP через loopback в macOS
вернула
0.071 ms. Локально не тестировались ни реальный удаленный сервер SSH, ни среды выполнения Windows и Linux, ни развертывание SELinux, ни внешний интерфейс Monaco. - Удаленное редактирование текста теперь требует Linux и Python 3 с поддержкой файловой системы/xattr относительно дескриптора. Отсутствие интерпретатора приводит к явной ошибке. Этот Core намеренно отказывается работать с небезопасными метаданными, ссылками и конфигурациями каталогов.
- Блокировка носит рекомендательный характер: несотрудничающие процессы все еще могут конкурировать в узком интервале между последней проверкой исходного состояния и атомарным переименованием. Linux не предоставляет общей атомарной операции сравнения хеша содержимого и замены. Проверки обнаруживают изменения до этого интервала; сохраненные рабочие копии позволяют восстановление после него.
- Данные восстановления остаются в удаленной учетной записи до подтвержденной очистки. Неполные, конфликтные и непроверяемые журналы никогда не удаляются молча. Нет автоматического истечения срока файлов восстановления и нет параметра принудительной перезаписи.
npm ciсообщил о 17 находках аудита зависимостей (9 средних, 8 высоких). Это изменение не меняет зависимости и модель доверия к обновлениям приложения.
Пользователь просил сохранить изменения в существующем рабочем дереве, сейчас это
feature/hash-checksum-plugin; переключения ветки и посторонней очистки не было.
Файлы, измененные в этой задаче
src/main/files/remoteFileService.jssrc/main/files/remoteEditor.py(новый)src/main/sessions/sessionManager.jssrc/main/sessions/latencyMonitor.js(новый)src/main/plugins/pluginRuntime.jssrc/preload/pluginPreload.jssrc/renderer/app.jssrc/renderer/index.htmlsrc/renderer/styles.csssrc/renderer/i18n.jstest/remoteFileService.test.jstest/sessionManager.test.jstest/pluginRuntime.test.jstest/remoteEditor.test.js(новый)test/remoteEditorProtocol.test.js(новый)test/remoteEditorLinux.test.js(новый)test/fixtures/remoteEditorLinux.py(новый)test/latencyMonitor.test.js(новый)README.mddocs/remote-editor-latency.md(этот файл)../.github/workflows/remote-editor-tests.yml(новый)