Skip to content

files: open files the way node's fs does - #405

Open
PTR-inc wants to merge 3 commits into
Ylianst:masterfrom
PTR-inc:fs-open-shared
Open

PTR-inc wants to merge 3 commits into
Ylianst:masterfrom
PTR-inc:fs-open-shared

Conversation

@PTR-inc

@PTR-inc PTR-inc commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

All fileopens now go through a shared function which checks the mode and opens files like Node does:
opens every file binary, shared for read, write and delete, non-inheritable by child processes and accept only r(ead), w(rite), a(ppend), +(read and write), x(fail if exists) for mode.

POSIX supports this out of the box (Node's fs is modelled after POSIX after all), except non-inheritable, which is the fcntl(FD_CLOEXEC) after the fopen.
'rx' is refused with EINVAL on every platform, as Node doesn't have that mode.
On windows, _wfopen_s and _wfsopen() lack FILE_SHARE_DELETE, so to circumvent that and get the Node functionality,
the handle is made with CreateFileW (which does accept all share flags), get a filedescriptor with _open_osfhandle() and put into FILE* f afterwards.

This should fix most sharing violation issues on windows.

#edit 10-09: add datastore fix
This is related to the fileopen fix. By using the new ILibFile_Open function for opening the datastore (the local .db), it no longer is passed on to child precesses. If the agent restarted and a chil process was still running, holding a lock on the.db, this would cause the meshagent to open an empty, cache-only .db and create an new certificate. And a new id on the meshserver side.

Details
  • 🧠 I used LLMs/AI in this contribution and reviewed all generated content.
    I understand that I am responsible for and able to explain every line of code I submit.
  • 🛠️ I have self-reviewed my code and self-tested it against a MeshCentral server to ensure it works as expected.
  • 🖥️ My change compiles on every platform it affects (Windows / Linux / macOS / FreeBSD), and I have considered
    the impact on platforms and architectures I could not test.
  • 📦 If I changed JavaScript modules under modules/, I re-embedded them so the compiled-in copies in
    microscript/ILibDuktape_Polyfills.c match (the agent runs the embedded copies, not the files on disk).
  • 🤖 I ran the agent self-test where appropriate (see "Self Test" in readme.md).
  • 📄 Documentation updates are included (if applicable), e.g. the .msh options table in readme.md.
  • 🧰 Updates to vendored dependencies (OpenSSL, zlib, ...) are listed and explained.
  • ⚠️ CI passes and is green (Windows / Linux / macOS / FreeBSD builds and CodeQL).

Testing

Tested on linux x86/x64 and windows x86/x64

Powershell test as mentioned in #7832 works, I can open edit, download and copy the file.

#edit 10-09: datastore tests done - push new core, service restart, force agent update on linux/windows.

_wfopen_s is fixed at _SH_SECURE, so every file the agent wrote stayed locked
against all readers for the life of the FILE*, and any handle whose mode lacked
an N was inherited by every spawned child.

ILibFile_Open() replaces the all open sites. It uses CreateFileW plus
_open_osfhandle and _fdopen on Windows, because _wfopen_s() and _wfsopen() cannot
grant FILE_SHARE_DELETE.

Always binary, never inherited, and the mode letters r w a + x b N are checked
on both platforms.

The .db keeps its own open, one writer with readers allowed,
which is what the datastore needs.
For 'a' modes the handle gets FILE_APPEND_DATA without FILE_WRITE_DATA, so the
kernel places every write at the end and two appending processes cannot
interleave, where the CRT's seek-then-write leaves a window. Access masks move
to FILE_GENERIC_READ/WRITE, since the swap needs the specific rights.
The Windows path of the datastore open already used the same CreateFileW,
_open_osfhandle and _fdopen sequence that ILibFile_Open uses, and
the POSIX path used plain fopen. Both now go through ILibFile_Open,
first with rb+ and, if the file does not exist, with wb+x.

Only one process may write, readers are allowed.
This is done with a non-blocking exclusive lock.
Flock() on POSIX and LockFileEx() on windows on one byte beyond the end of the file,
so that readers are never blocked.

The .db is also no longer passed on to child processes.
Before, a child that was still running after the agent had stopped
kept the lock on the .db, so the next agent start could not open it.
It would open an empty, cache-only .db and create a new certificate,
which appears as a new device on the server.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant