Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions src/controller/controller.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ require("view.view")
require("util.string.string")
require("util.key")
local LANG = require("util.eval")
local FS = require("util.filesystem")

local messages = {
user_break = "BREAK into program",
Expand Down Expand Up @@ -451,6 +452,10 @@ Controller = {
local cfg = CC.cfg

local function quit()
--- flush pending writes before the process can exit
--- (spec 2.6): a graceful quit loses nothing. One
--- syscall; force-stop is covered by per-accept fsync
FS.sync()
if love.state.app_state == 'shutdown' then
return false
end
Expand All @@ -471,6 +476,30 @@ Controller = {
love.quit = quit
end,

--- Background durability net (spec 2.6): a child
--- leaving the app flushes pending writes. One syscall
--- on focus loss; does not cover a force-stop mid-edit
--- (per-accept fsync does).
--- @private
--- @param CC ConsoleController
set_love_focus = function(CC)
local function focus(f)
if not f then FS.sync() end
end
love.focus = focus

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this for? Do we have multi-app scenarios?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, probably just over-defensiveness that is typical for Claude. Should we remove it?

end,

--- Companion to focus: Android reports a backgrounded
--- window as not visible; flush there too.
--- @private
--- @param CC ConsoleController
set_love_visible = function(CC)
local function visible(v)
if not v then FS.sync() end
end
love.visible = visible
end,

----------------
--- public ---
----------------
Expand All @@ -493,10 +522,11 @@ Controller = {

--- SKIPPED joystick and gamepad support

--- intented to run as kiosk app
--- SKIPPED focus
--- intented to run as kiosk app; focus/visible are
--- wired only to flush pending writes on background
Controller.set_love_focus(CC)
Controller.set_love_visible(CC)
--- SKIPPED mousefocus
--- SKIPPED visible
--- SKIPPED resize
--- SKIPPED filedropped
--- SKIPPED directorydropped
Expand Down
58 changes: 58 additions & 0 deletions src/util/filesystem.lua
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,64 @@ if love and not TESTING then
return FS.read(path, true) or FS.read(path)
end

--- Durability helpers (bionic/glibc via LuaJIT FFI).
--- FS.write is async (see its contract below); the
--- editor accept path opts into durability with
--- FS.fsync, and lifecycle handlers use FS.sync as a
--- cheap whole-filesystem net.
local _durable = (function()
local ffi_ok, ffi = pcall(require, 'ffi')
if not ffi_ok then return nil end
pcall(ffi.cdef, [[
int open(const char* path, int flags);
int close(int fd);
int fsync(int fd);
void sync(void);
]])
local O_RDONLY = 0
return {
file = function(path)
local fd = ffi.C.open(path, O_RDONLY)
if fd < 0 then return false end
local r = ffi.C.fsync(fd)
ffi.C.close(fd)
return r == 0
end,
all = function() ffi.C.sync() end,
}
end)()

--- Flush one file's data through to stable storage.
--- The editor accept path calls this after a save
--- (spec 2.6 "written immediately"). Do NOT add it to
--- FS.write: bulk deploy/clone and the user-facing
--- writefile must stay async. Best-effort — returns
--- false when the platform lacks the syscall or the
--- path cannot be opened.
--- @param path string
--- @return boolean durable
function FS.fsync(path)
if not _durable then return false end
local ok, res = pcall(_durable.file, path)
return (ok and res) or false
end

--- Flush all pending writes filesystem-wide in one
--- syscall. Cheap broad net for background/quit; does
--- not cover a force-stop mid-edit (that is FS.fsync).
--- @return boolean ran
function FS.sync()
if not _durable then return false end
return pcall(_durable.all)
end

--- Write data to path, overwriting. Async by default:
--- the bytes reach the OS but are NOT flushed to stable
--- storage, so a power-cut or SIGKILL can lose them
--- while an (exfat dirsync) directory entry persists.
--- Callers needing durability opt in via FS.fsync(path)
--- after a successful write — the editor accept path
--- does; bulk deploy/clone and writefile do not.
--- @param path string
--- @param data string
--- @return boolean success
Expand Down
Loading