# Lưu trong trình soạn thảo từ xa và giám sát độ trễ ## Nguyên nhân gốc rễ Trình soạn thảo trước đây tải tệp làm việc lên thư mục cha của tệp đích, nơi mà tài khoản SSH thường không có quyền ghi trong `/etc` hoặc `/var`. Thao tác đổi tên qua SFTP không cung cấp đường dẫn sudo được ủy quyền, kiểm tra xung đột so với trạng thái ban đầu, xác minh siêu dữ liệu bảo mật hay ngữ nghĩa ghi đè đáng tin cậy. Trình xử lý lỗi của nó xóa bản sao tạm thời, làm mất dữ liệu khôi phục ngay cả khi chưa chắc chắn thao tác đã hoàn tất. Trạng thái phiên trước đây chỉ hiển thị trạng thái kết nối. Không có phép đo ICMP hay phản hồi SSH đã xác thực nào cung cấp dữ liệu cho chỉ báo của terminal. ## Giao thức lưu 1. Một helper SSH cố định mở tệp thông thường chỉ có một liên kết bằng các thao tác tương đối theo descriptor và `O_NOFOLLOW` cho mọi thành phần của đường dẫn. Helper ghi nhận nội dung, hash SHA-256, thiết bị/inode, UID, GID, toàn bộ bit quyền, thời gian sửa đổi/thay đổi tính bằng nano giây và mọi thuộc tính mở rộng có thể đọc. Core xác minh hash và giải mã UTF-8 nghiêm ngặt, giữ nguyên BOM. Mỗi tài liệu nhận một `editId` không minh bạch, gắn với plugin, phiên terminal và danh tính máy chủ của nó. 2. Một thao tác chuẩn bị không đặc quyền phân giải thư mục home của tài khoản đã xác thực qua `pwd.getpwuid(os.getuid())`, tạo `$HOME/ch-j-sm/` với chế độ `0700` và tạo một nhật ký khôi phục riêng tư, độc quyền. Thư mục home không an toàn, không gian làm việc là liên kết tượng trưng hoặc không gian làm việc thuộc tài khoản khác đều bị từ chối. 3. Core tải lên qua SFTP một tệp làm việc `.tmp` ẩn có tên ngẫu nhiên 128 bit với chế độ tạo độc quyền (`wx`), chế độ `0600` và thời gian chờ kênh/tải lên sáu mươi giây. Nhật ký chứa tệp đích, trạng thái ban đầu gốc và hash mong đợi của tệp làm việc; nhật ký không chứa thông tin xác thực. 4. **Một lần thực thi SSH thực hiện toàn bộ bước hoàn tất**, tùy chọn thông qua bộ thực thi sudo hiện có. Helper mở an toàn và tính hash của bản sao làm việc riêng tư, rồi đẩy (flush) nó xuống đĩa. Helper khóa inode gốc, kiểm tra trạng thái ban đầu của nó, tạo một tệp đích độc quyền bên cạnh tệp đích, ghi toàn bộ nội dung, áp dụng và xác minh siêu dữ liệu, kiểm tra lại tệp gốc một lần nữa và đẩy tệp thay thế xuống đĩa. 5. `os.replace` trong cùng thư mục thay thế tệp đích một cách nguyên tử. Thư mục cha được đẩy xuống đĩa, sau đó đường dẫn cuối cùng, hash, inode và siêu dữ liệu được xác minh. Đường dẫn gốc vẫn khả dụng cho tới khi thay thế. Tệp làm việc không bao giờ bị di chuyển giữa các hệ thống tệp và không bao giờ bị bước hoàn tất xóa. 6. Sau khi Core nhận được kết quả thành công đã xác minh, một bước dọn dẹp riêng kiểm tra lại nội dung cuối cùng và siêu dữ liệu bảo mật trước khi xóa tệp làm việc và nhật ký. Nếu dọn dẹp thất bại, việc lưu vẫn được báo là đã xác nhận với `recoveryAvailable: true`. Tải lên thất bại, sudo thất bại, xung đột, lỗi siêu dữ liệu hoặc thay thế thất bại đều giữ nguyên tệp gốc và mọi dữ liệu đã chuẩn bị. Các lần tải lên chưa hoàn tất được đánh dấu như vậy và không thể khôi phục thành một tài liệu đã chỉnh sửa hoàn chỉnh. Mất liên lạc SSH, thiếu mã thoát, hết thời gian chờ và kết quả hoàn tất bất ngờ được báo là **không xác định**, không bao giờ là đã lưu. Core chặn lần lưu tiếp theo đối với tài liệu có kết quả không chắc chắn; không có cơ chế tự động thử thay thế lại. ## Siêu dữ liệu, đặc quyền và ranh giới an toàn Helper giữ nguyên và xác minh UID, GID và mọi bit quyền POSIX, bao gồm bit thực thi, setuid, setgid và sticky. Helper tái tạo từng byte các thuộc tính mở rộng của Linux, bao gồm ACL POSIX, capabilities và nhãn SELinux. Quyền sở hữu và nội dung tệp được thiết lập trước các thuộc tính bảo mật, vì thao tác ghi/chown có thể xóa capabilities hoặc các bit quyền đặc biệt. Các thuộc tính kế thừa ở tệp đích nhưng không có trên tệp gốc sẽ bị xóa. Thời gian sửa đổi phản ánh lần ghi mới và không được sao chép từ tệp gốc. Thuộc tính không đọc được hoặc không biểu diễn được sẽ hủy thao tác; siêu dữ liệu không bị âm thầm bỏ qua. Chữ ký IMA/EVM bị từ chối rõ ràng, vì sao chép chữ ký toàn vẹn gắn với nội dung/inode sẽ làm chúng mất hiệu lực. Liên kết tượng trưng trong đường dẫn đích hoặc home, tệp có liên kết cứng và tệp không thông thường đều bị từ chối. Không có cơ chế dự phòng ghi đè tại chỗ hay ghi đè SFTP kém an toàn hơn. Các lần lưu thông thường có quyền ghi chạy dưới tài khoản SSH. Lưu tệp được bảo vệ yêu cầu `{ sudo: true }` rõ ràng; mật khẩu sudo là tùy chọn đối với sudo không cần mật khẩu. Mật khẩu được xác thực và chỉ được truyền qua stdin của kênh, không bao giờ nằm trong lệnh, nhật ký, tệp làm việc, logger hay lỗi từ xa. Phiên SSH root dùng bộ thực thi root trực tiếp hiện có. Ủy quyền thất bại sẽ giữ nguyên tệp gốc và bản sao làm việc. Mọi lần lưu thực thi dưới quyền root (SSH root trực tiếp hoặc sudo) cũng từ chối các thư mục đích/thư mục tổ tiên thuộc sở hữu hoặc có thể ghi bởi tài khoản khác, hoặc có ACL truy cập, thay vì tin tưởng một đường dẫn đặc quyền do tài khoản khác kiểm soát. Helper cố định chạy với `python3 -I -B`: nó bỏ qua các đường dẫn Python của người dùng, thư mục làm việc và site packages của người dùng, đồng thời không ghi bytecode. Đường dẫn và đối số JSON được Core đặt trong dấu nháy shell. Plugin không bao giờ nhận được quyền thực thi SSH tùy ý hay handle SFTP. Sandbox, CSP, quyền trong manifest, xác minh khóa máy chủ và xác minh cập nhật hiện có vẫn được sử dụng. ## Plugin API và phần tích hợp Monaco còn lại **Giao diện Monaco/File Manager được phân phối riêng không có trong bản checkout này. Phần tích hợp giao diện, lời nhắc sudo, xử lý bộ đệm chưa lưu và bản dịch của nó không thể được triển khai hay thử nghiệm tại đây.** Mã nguồn plugin đi kèm là plugin Hash & Checksum. Không có mã nguồn File Manager thay thế nào được tự tạo ra. Các phương thức hiện có vẫn khả dụng. Các phương thức/tùy chọn bổ sung được cung cấp bởi preload trong sandbox và runtime có kiểm tra quyền: ```js 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. ``` Các phương thức khôi phục chấp nhận đối số cuối tùy chọn `{ sudo: true, sudoPassword }` khi việc kiểm tra tệp đích cần nâng quyền. Chúng vẫn giữ không gian làm việc của người dùng đã xác thực ngay cả khi thực thi qua sudo. Nhật ký vẫn tồn tại sau khi khởi động lại ứng dụng và kết nối lại SSH; việc khôi phục không bao giờ tự động lặp lại bước hoàn tất. Hãy đối chiếu bộ đệm đã khôi phục với tệp đích vừa được mở lại trước khi lưu nó một cách rõ ràng. `files.read` bảo vệ việc đọc, kiểm tra và giải phóng handle. `files.write` bảo vệ việc lưu và dọn dẹp khôi phục. `writeText` vẫn từ chối (reject) khi thất bại đối với các plugin hiện có; `saveText` trả về một phong bì có cấu trúc `{ ok, value/error }` vì Electron bỏ các thuộc tính tùy chỉnh trên đối tượng Error bị từ chối. `writeText` kiểu cũ với ba đối số có thể dùng trạng thái ban đầu của một tài liệu duy nhất đang mở. Nhiều tài liệu đang mở cho cùng một đường dẫn yêu cầu `editId`; các lệnh gọi kiểu cũ không rõ ràng sẽ thất bại an toàn thay vì chọn trạng thái ban đầu của tài liệu khác. Plugin nên giải phóng các handle không dùng. Giao diện bên ngoài phải hiển thị trạng thái đang lưu ngay lập tức, giữ bộ đệm ở trạng thái chưa lưu khi thất bại, chỉ đề xuất sudo sau lỗi quyền/xác thực, hiển thị xung đột mà không ghi đè, và đề xuất khôi phục sau khi mất kết nối hoặc khi kết quả không xác định. Giao diện phải dịch các trạng thái này sang tiếng Séc, tiếng Đức và tiếng Anh. Văn bản trạng thái đề xuất: | Trạng thái | Tiếng Séc | Tiếng Đức | Tiếng Anh | | --- | --- | --- | --- | | Đang lưu | Ukládání… | Wird gespeichert… | Saving… | | Đã xác nhận | Uloženo | Gespeichert | Saved | | Thất bại | Uložení selhalo | Speichern fehlgeschlagen | Save failed | | Cần sudo | Vyžadováno oprávnění sudo | sudo-Autorisierung erforderlich | Sudo authorization required | | Xung đột | Původní soubor byl změněn | Originaldatei wurde geändert | Original file changed | | Mất kết nối | Spojení bylo přerušeno | Verbindung unterbrochen | Connection lost | | Không xác định | Výsledek uložení není znám | Speicherergebnis unbekannt | Save result unknown | | Khôi phục | Obnova je dostupná | Wiederherstellung verfügbar | Recovery available | ## Định nghĩa độ trễ và vòng đời **Ping** là độ trễ được báo cáo bởi một phản hồi ICMP echo thực tế phía máy khách tới IP đã phân giải mà SSH sử dụng. Core gọi tệp thực thi ping của nền tảng một cách bất đồng bộ với mảng đối số cố định, không dùng shell và không yêu cầu khởi chạy với quyền quản trị viên. Đối số cho Linux, macOS (bao gồm `ping6`) và Windows được chọn riêng. ICMP bị chặn, thiếu tệp thực thi, hết thời gian chờ, đầu ra không hợp lệ hoặc phản hồi Windows chỉ có cận trên như `<1ms` đều cho giá trị không khả dụng, không phải một con số bịa ra. **SSH RTT** là khoảng thời gian đơn điệu `performance.now()` của một yêu cầu exec `true` cố định qua trình khách SSH đã xác thực hiện có. Giá trị này bao gồm thiết lập kênh, xử lý phía máy chủ và truyền phản hồi; nó không phải RTT mạng thuần túy hay thời gian đăng nhập SSH. Chú giải (tooltip) đã dịch của chỉ báo giải thích sự khác biệt này. Việc giám sát bắt đầu ngay khi terminal kết nối. Các phép thăm dò chạy đồng thời và bất đồng bộ, với thời gian chờ yêu cầu/tiến trình là năm giây. Mẫu tiếp theo được lên lịch mười hai giây sau khi mẫu trước hoàn tất, tránh chồng chéo. Ngắt kết nối sẽ hủy ICMP, xóa bộ hẹn giờ, hủy các tác vụ SSH đang chờ và xóa các giá trị. Kiểm tra danh tính bản ghi ngăn kết nối trước công bố giá trị sau khi kết nối lại. Lỗi ICMP không bao giờ thay đổi trạng thái kết nối SSH. Độ trễ được truyền trong payload IPC của sự kiện `state` / `ssh:state` hiện có: ```js latency: { pingMs: 24.1, sshRttMs: 31.25, measuredAt: "…" } ``` Các số không khả dụng có giá trị `null`. Chỉ báo terminal gọn nhẹ dùng mili giây, văn bản không khả dụng đã bản địa hóa và nhãn tiếng Séc/Đức/Anh. Chỉ báo bị ẩn khi ngắt kết nối. Không cần thêm khả năng thực thi cho renderer hay yêu cầu IPC nào. ## Xác thực và giới hạn vận hành - Chạy từ `app/`: `npm ci`, sau đó `npm test`. - Kết quả cục bộ cuối cùng: **179 kiểm thử; 163 đạt, 0 thất bại, 16 bỏ qua**. Tất cả các trường hợp bỏ qua là kiểm thử hệ thống tệp Linux trên máy macOS này. `npm ci`, biên dịch cú pháp Python và `git diff --check` đều hoàn tất thành công. - Các kiểm thử bao quát việc lưu/khôi phục của Core, giao thức sudo và che giấu mật khẩu, đặt dấu nháy shell POSIX thực tế cho các đường dẫn độc hại, xung đột, tải lên, dọn dẹp thất bại, kết quả không chắc chắn, tài liệu đồng thời, quyền IPC và vòng đời đo độ trễ. - Kiểm thử hệ thống tệp Linux chạy điểm vào Python được phát hành, dùng cơ chế tra cứu home của tài khoản thử nghiệm riêng: quyền sở hữu/chế độ/xattrs, ACL POSIX, liên kết tượng trưng/liên kết cứng, FIFO, lỗi tải lên và siêu dữ liệu, lỗi thay thế, tranh chấp trên tệp đích, kết quả không xác định sau khi thay thế và khôi phục. Việc chuẩn bị giữa các hệ thống tệp dùng `/dev/shm` và một hệ thống tệp khác nếu có. Các kiểm thử này bị bỏ qua trên máy không phải Linux; thiếu ACL/hệ thống tệp riêng biệt được bỏ qua một cách rõ ràng. - Job GitHub Actions cho Linux được thêm vào chạy toàn bộ bộ kiểm thử và lặp lại các trường hợp hệ thống tệp dưới quyền root. Workflow này chưa được kích hoạt từ working tree này. - Việc xác thực cục bộ được thực hiện trên macOS. Cú pháp mã nguồn Python được kiểm tra trực tiếp bằng tệp thực thi Python của Command Line Tools. Một kiểm thử nhanh ICMP loopback thực tế trên macOS trả về `0.071 ms`. Không có máy chủ SSH từ xa đang chạy, runtime Windows, runtime Linux, môi trường SELinux hay giao diện Monaco bên ngoài nào được thử nghiệm cục bộ. - Chỉnh sửa văn bản từ xa hiện yêu cầu Linux và Python 3 có hỗ trợ hệ thống tệp/xattr tương đối theo descriptor. Thiếu trình thông dịch sẽ thất bại một cách rõ ràng. Core này cố ý từ chối siêu dữ liệu, liên kết và cấu hình thư mục không an toàn. - Cơ chế khóa chỉ mang tính tư vấn: các tiến trình không hợp tác vẫn có thể tranh chấp trong khoảng thời gian ngắn giữa lần kiểm tra trạng thái ban đầu cuối cùng và thao tác đổi tên nguyên tử. Linux không cung cấp thao tác chung so sánh hash nội dung rồi thay thế một cách nguyên tử. Các bước kiểm tra phát hiện thay đổi trước khoảng thời gian đó; bản sao làm việc được giữ lại hỗ trợ khôi phục sau đó. - Dữ liệu khôi phục vẫn nằm trên tài khoản từ xa cho tới khi dọn dẹp được xác nhận. Các nhật ký chưa hoàn tất, có xung đột và không thể kiểm tra không bao giờ bị âm thầm loại bỏ. Không có cơ chế tự động hết hạn tệp khôi phục và không có tùy chọn ghi đè cưỡng bức. - `npm ci` báo cáo 17 phát hiện kiểm tra phụ thuộc (9 mức trung bình, 8 mức cao). Thay đổi này không thay đổi các phụ thuộc hay mô hình tin cậy cập nhật của ứng dụng. Người dùng yêu cầu giữ các thay đổi trong working tree hiện có, hiện là `feature/hash-checksum-plugin`; không có việc chuyển nhánh hay dọn dẹp không liên quan nào được thực hiện. ## Các tệp đã thay đổi cho tác vụ này - `src/main/files/remoteFileService.js` - `src/main/files/remoteEditor.py` (mới) - `src/main/sessions/sessionManager.js` - `src/main/sessions/latencyMonitor.js` (mới) - `src/main/plugins/pluginRuntime.js` - `src/preload/pluginPreload.js` - `src/renderer/app.js` - `src/renderer/index.html` - `src/renderer/styles.css` - `src/renderer/i18n.js` - `test/remoteFileService.test.js` - `test/sessionManager.test.js` - `test/pluginRuntime.test.js` - `test/remoteEditor.test.js` (mới) - `test/remoteEditorProtocol.test.js` (mới) - `test/remoteEditorLinux.test.js` (mới) - `test/fixtures/remoteEditorLinux.py` (mới) - `test/latencyMonitor.test.js` (mới) - `README.md` - `docs/remote-editor-latency.md` (tệp này) - `../.github/workflows/remote-editor-tests.yml` (mới)