CH-J Server Managerserver management over SSH
Menu
Published source

CH-J Server Manager

Browse directories and files for a specific application release.

Download source ZIP
CH-J Proprietary Software License 1.14

Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.

13,4 KB · 236 linesDownload file
1# Remote editor saving and latency monitoring
3## Root causes
5The previous editor uploaded its working file into the target's parent directory,
6which often cannot be written by the SSH account under `/etc` or `/var`. SFTP
7rename supplied no authorized sudo path, baseline conflict check, security
8metadata verification or reliable overwrite semantics. Its error handler deleted
9the temporary copy, removing recovery material even when completion was uncertain.
11Session status previously exposed only the connection state. There was no ICMP or
12authenticated SSH response measurement feeding the terminal indicator.
14## Save protocol
161. A fixed SSH helper opens a regular, single-link file using descriptor-relative
17 operations and `O_NOFOLLOW` for every path component. It captures the content,
18 SHA-256 hash, device/inode, UID, GID, complete permission bits, nanosecond
19 modification/change times and all readable extended attributes. Core verifies
20 the hash and decodes strict UTF-8, preserving a BOM. Each document gets an
21 opaque `editId`, scoped to its plugin, terminal session and server identity.
222. An unprivileged preparation operation resolves the authenticated account's
23 home through `pwd.getpwuid(os.getuid())`, creates `$HOME/ch-j-sm/` with mode
24 `0700`, and creates a private, exclusive recovery journal. An unsafe home,
25 symlink workspace, or workspace owned by another account is rejected.
263. Core uploads a hidden, random 128-bit `.tmp` working file through SFTP with
27 exclusive creation (`wx`), mode `0600` and a sixty-second channel/upload timeout.
28 The journal contains the target,
29 original baseline and expected working hash; it contains no credentials.
304. **One SSH exec performs the entire finalization**, optionally through the
31 existing sudo executor. The helper safely opens and hashes the private working
32 copy and flushes it. It locks the original inode, checks its baseline, creates
33 an exclusive destination file beside the target, writes all content, applies
34 and verifies metadata, checks the original again, and flushes the replacement.
355. A same-directory `os.replace` atomically replaces the target. The parent
36 directory is flushed and the final path, hash, inode and metadata are verified.
37 The original path remains available until replacement. The working file is
38 never moved across filesystems and is never removed by finalization.
396. After Core receives a verified success, a separate cleanup rechecks the final
40 content and security metadata before removing the working file and journal.
41 Cleanup failure still reports a confirmed save with `recoveryAvailable: true`.
43A failed upload, failed sudo, conflict, metadata error or failed replacement keeps
44the original and any staged material. Incomplete uploads are identified as such
45and cannot be recovered as a complete edited document. Lost SSH communication,
46missing exit status, timeouts and unexpected finalization results are reported as
47**unknown**, never as saved. Core blocks another save on an uncertain document;
48there is no automatic replacement retry.
50## Metadata, privileges and safety boundaries
52The helper preserves and verifies UID, GID and all POSIX permission bits, including
53executable, setuid, setgid and sticky bits. It reproduces Linux extended attributes
54byte for byte, including POSIX ACLs, capabilities and SELinux labels. Ownership
55and file content are set before security attributes, because writes/chown can
56clear capabilities or special permission bits. Inherited destination attributes
57that are absent on the original are removed. Modification time reflects the new
58write and is not copied from the original.
60An unreadable or unrepresentable attribute aborts the operation; metadata is not
61silently dropped. IMA/EVM signatures are explicitly rejected because copying
62content/inode-bound integrity signatures would invalidate them. Symlinks in the
63target or home path, hard-linked files and nonregular files are rejected. No
64in-place or weaker SFTP overwrite fallback is used.
66Ordinary writable saves run as the SSH account. Protected saves require explicit
67`{ sudo: true }`; a sudo password is optional for passwordless sudo. Passwords are
68validated and passed only on channel stdin, never inside the command, journal,
69working file, logger or remote error. Root SSH sessions use the existing direct
70root executor. Failed authorization leaves the original and working copy intact.
71All saves executed as root (direct root SSH or sudo) also reject target
72directories/ancestors owned or writable by other accounts, or carrying an access
73ACL, rather than trusting a privileged pathname controlled by another account.
75The fixed helper runs with `python3 -I -B`: it ignores user Python paths, the
76working directory and user site packages, and writes no bytecode. Paths and JSON
77arguments are shell-quoted in Core. Plugins never receive arbitrary SSH exec or
78SFTP handles. Existing sandbox, CSP, manifest permissions, host-key verification
79and update verification remain in use.
81## Plugin API and remaining Monaco integration
83**The separately distributed Monaco/File Manager UI is absent from this
84checkout. Its UI integration, sudo prompt, dirty-buffer handling and translations
85could not be implemented or tested here.** The bundled plugin source is the Hash
86& Checksum plugin. No substitute File Manager source was invented.
88The existing methods remain available. Additive methods/options are exposed by
89the sandboxed preload and permission-checked runtime:
91```js
92const opened = await chjPlugin.files.readText(sessionId, path);
93// For a protected read, explicitly pass { sudo: true, sudoPassword }.
95const result = await chjPlugin.files.saveText(sessionId, path, editor.getValue(), {
96 editId: opened.editId,
97 sudo: true, // Include only after the user authorizes elevation.
98 sudoPassword // Omit for passwordless sudo; never persist this value.
99});
101if (result.ok && result.value.status === "saved") {
102 // Mark this exact submitted editor version as saved. Later edits stay dirty.
103} else {
104 // Keep the Monaco model dirty. Translate result.error.code/status.
105 // result.error.recoveryId/recoveryPath identify retained working material.
108await chjPlugin.files.closeText(opened.editId);
109const recovery = await chjPlugin.files.listRecovery(sessionId);
110const copy = await chjPlugin.files.readRecovery(sessionId, recoveryId);
111// Review copy.status and copy.text; open a fresh document before another save.
112await chjPlugin.files.cleanupRecovery(sessionId, recoveryId);
113// Cleanup rejects anything whose target is not currently confirmed.
114```
116Recovery methods accept an optional final `{ sudo: true, sudoPassword }` argument
117when inspecting a target requires elevation. They retain the authenticated user's
118workspace even when executed through sudo. Journals survive app restart and SSH
119reconnect; recovery never automatically repeats finalization. Review a recovered
120buffer against a freshly opened target before explicitly saving it.
122`files.read` guards reads, inspection and handle release. `files.write` guards
123saves and recovery cleanup. `writeText` still rejects on failure for existing
124plugins; `saveText` returns a structured `{ ok, value/error }` envelope because
125Electron drops custom properties on rejected Error objects. A legacy three-argument
126`writeText` can use the baseline of a single open document. Multiple live documents
127for the same path require `editId`; ambiguous legacy calls fail safely instead of
128choosing another document's baseline. Plugins should release unused handles.
130The external UI must display saving immediately, keep buffers dirty on failure,
131offer sudo only after permission/authentication failures, show conflicts without
132overwriting, and offer recovery after connection loss or an unknown outcome. It
133must translate these states in Czech, German and English. Suggested state copy:
135| State | Czech | German | English |
136| --- | --- | --- | --- |
137| Saving | Ukládání… | Wird gespeichert… | Saving… |
138| Confirmed | Uloženo | Gespeichert | Saved |
139| Failure | Uložení selhalo | Speichern fehlgeschlagen | Save failed |
140| Sudo required | Vyžadováno oprávnění sudo | sudo-Autorisierung erforderlich | Sudo authorization required |
141| Conflict | Původní soubor byl změněn | Originaldatei wurde geändert | Original file changed |
142| Disconnected | Spojení bylo přerušeno | Verbindung unterbrochen | Connection lost |
143| Unknown | Výsledek uložení není znám | Speicherergebnis unbekannt | Save result unknown |
144| Recovery | Obnova je dostupná | Wiederherstellung verfügbar | Recovery available |
146## Latency definitions and lifecycle
148**Ping** is the latency reported by an actual client-side ICMP echo reply to the
149resolved IP used by SSH. Core invokes the platform ping executable asynchronously
150with a fixed argument array, without a shell or administrator startup requirement.
151Linux, macOS (including `ping6`) and Windows arguments are selected separately.
152Blocked ICMP, a missing executable, a timeout, invalid output or an upper-bound-only
153Windows reply such as `<1ms` produce an unavailable value, not an invented number.
155**SSH RTT** is the monotonic `performance.now()` duration of a fixed `true` exec
156request over the existing authenticated SSH client. It includes channel setup,
157server processing and response delivery; it is not pure network RTT or SSH login
158duration. The translated indicator tooltip explains this distinction.
160Monitoring starts as soon as the terminal connects. Probes run concurrently and
161asynchronously, with a five-second request/process timeout. The next sample is
162scheduled twelve seconds after the previous sample completes, preventing overlap.
163Disconnect aborts ICMP, clears timers, cancels pending SSH work and clears values.
164Record identity checks prevent a prior connection from publishing after reconnect.
165ICMP failure never changes SSH connection state.
167Latency travels inside the existing `state` event / `ssh:state` IPC payload:
169```js
170latency: { pingMs: 24.1, sshRttMs: 31.25, measuredAt: "…" }
171```
173Unavailable numbers are `null`. The compact terminal indicator uses milliseconds,
174localized unavailable text and Czech/German/English labels. It hides on disconnect.
175No additional renderer execution capability or IPC request was needed.
177## Validation and operational limits
179- Run from `app/`: `npm ci`, then `npm test`.
180- Final local result: **179 tests; 163 passed, 0 failed, 16 skipped**. All skips
181 are the Linux filesystem cases on this macOS host. `npm ci`, Python syntax
182 compilation and `git diff --check` completed successfully.
183- The tests cover Core saves/recovery, sudo protocol and password redaction,
184 real POSIX shell quoting of hostile paths, conflicts, uploads, failed cleanup,
185 uncertain outcomes, concurrent documents, IPC permissions and latency lifecycle.
186- Linux filesystem tests exercise the shipped Python entry point, using a private
187 test account-home lookup: ownership/mode/xattrs, POSIX ACLs, symlinks/hardlinks,
188 FIFOs, upload and metadata failures, replacement failures, target races, unknown
189 post-replacement outcomes and recovery. Cross-filesystem staging uses `/dev/shm`
190 and another filesystem when available. These tests skip on non-Linux hosts;
191 missing ACL/separate-filesystem facilities are explicitly skipped.
192- The added Linux GitHub Actions job runs the complete suite and repeats filesystem
193 cases as root. This workflow has not been dispatched from this working tree.
194- Local validation was on macOS. Python source syntax was checked with the direct
195 Command Line Tools Python executable. A real macOS loopback ICMP smoke check
196 returned `0.071 ms`. No live remote SSH server, Windows runtime, Linux runtime,
197 SELinux deployment or external Monaco UI was tested locally.
198- Remote text editing now requires Linux and Python 3 with descriptor-relative
199 filesystem/xattr support. A missing interpreter fails explicitly. This Core
200 intentionally refuses unsafe metadata, links and directory configurations.
201- Locking is advisory: noncooperating processes can still race in the narrow
202 interval between the last baseline check and atomic rename. Linux supplies no
203 general atomic content-hash compare-and-replace operation. Checks detect changes
204 before that interval; retained working copies support recovery afterward.
205- Recovery data remains on the remote account until a confirmed cleanup. Incomplete,
206 conflicted and uninspectable journals are never silently discarded. There is no
207 automatic recovery-file expiration and no forced overwrite option.
208- `npm ci` reported 17 dependency audit findings (9 moderate, 8 high). This change
209 does not change dependencies or the application's update trust model.
211The user requested retaining changes in the existing working tree, currently
212`feature/hash-checksum-plugin`; no branch switch or unrelated cleanup was performed.
214## Files changed for this task
216- `src/main/files/remoteFileService.js`
217- `src/main/files/remoteEditor.py` (new)
218- `src/main/sessions/sessionManager.js`
219- `src/main/sessions/latencyMonitor.js` (new)
220- `src/main/plugins/pluginRuntime.js`
221- `src/preload/pluginPreload.js`
222- `src/renderer/app.js`
223- `src/renderer/index.html`
224- `src/renderer/styles.css`
225- `src/renderer/i18n.js`
226- `test/remoteFileService.test.js`
227- `test/sessionManager.test.js`
228- `test/pluginRuntime.test.js`
229- `test/remoteEditor.test.js` (new)
230- `test/remoteEditorProtocol.test.js` (new)
231- `test/remoteEditorLinux.test.js` (new)
232- `test/fixtures/remoteEditorLinux.py` (new)
233- `test/latencyMonitor.test.js` (new)
234- `README.md`
235- `docs/remote-editor-latency.md` (this file)
236- `../.github/workflows/remote-editor-tests.yml` (new)

SHA-256: 818223294a09fb06331673639d6d545897d96faff6f209aa2cfb2845d83b1e9d

Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0