diff --git a/TODO.org b/TODO.org index d3b237b5..a936aa1d 100644 --- a/TODO.org +++ b/TODO.org @@ -1465,11 +1465,14 @@ in a merged build that process is the program, the compiler and the listener at once. It reported as a connection refusal on a path that plainly existed, which misdirected two investigations. -** TODO The daemon leaves its temp directory behind -About seven megabytes a session, and nothing removes it. Two deliberate non-goals -when it is fixed: not on a crash, because the directory is the post-mortem, and -never another session's directory, because a stale pid is not proof of anything. -The agent socket goes with it. +** DONE The daemon leaves its temp directory behind +CLOSED: [2026-09-25] +A session that ends cleanly — =close=, or the editor gone past the grace — +removes =flan-dev-= (program, modules, agent socket) and its own +=Build.workdir=. Kept on a crash: the accept loop raising, or a two-process +child killed by a signal. Only the two paths named for this pid are touched; +nothing sweeps other sessions' directories. =flan build= and =flan run= still +leave an empty =flan-= each; that is not this entry. ** DONE (agent/start) takes no argument, and binds before main CLOSED: [2026-09-20] @@ -2010,9 +2013,12 @@ go through one helper. The CLI loads through its own path and checks with a different entry point, so this is not a matter of calling the test module from the binary — closing it means the pipeline moving into the library. -** TODO Build.executable returns only its output path -The daemon recovers the host's IR file by recomputing the working directory. One -line away: return the path rather than recomputing it. +** DONE Build.executable returns only its output path +CLOSED: [2026-09-25] +It returns the output path and, under =keep=, the path of the IR or assembly +it kept; without =keep= that file is gone and the second half is =None=. The +two-process daemon moves the host's IR from the path it is given and no longer +recomputes =Build.workdir=. ** TODO The 2MB OFL font is not vendored One example wants a font that is OFL and redistributable; it says on screen when diff --git a/lib/build.ml b/lib/build.ml index 308363e4..c86fc27b 100644 --- a/lib/build.ml +++ b/lib/build.ml @@ -729,7 +729,11 @@ let compile_c ~opts ?tflags ?(warn = []) ~src ~name () = (* [csrcs] and [lflags] come from the imported packages (see [Load]): the C shim a package binds through, and the arguments needed to link the library - it binds to. *) + it binds to. + + Returns the output path and, when [opts.keep] asked for it, the path of the + IR (or, under [x86], the assembly) the build was made from. Without [keep] + that file is removed before this returns, so there is no path to give. *) let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = []) (p : Tast.program) ~out = (* The JS dialect leaves here, before anything that assumes a clang. Its @@ -749,7 +753,7 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = []) if opts.x86 then failwith "js: --x86 and --target=js are two different backends — pick one"; write out (Js.program ~checks:opts.checks p); - out + (out, None) end else (* A dev build is the REPL's, and the REPL reaches a running process through @@ -943,8 +947,11 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = []) let code = Sys.command cmd in if code <> 0 then failwith (Printf.sprintf "%s failed (exit %d); the IR is at %s" clang code ll); - if not opts.keep then (try Sys.remove ll with Sys_error _ -> ()); - out + if opts.keep then (out, Some ll) + else begin + (try Sys.remove ll with Sys_error _ -> ()); + (out, None) + end (* ── The dev path: one function into a loadable object ──────────────── *) diff --git a/lib/dev.ml b/lib/dev.ml index 693575a2..7a46ee28 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -64,6 +64,10 @@ type t = { again ([eval]) and when a re-run is accepted ([rerun]), so the next park is a new one and gets the whole sentence. *) mutable park_noted : bool; + (* How the two-process daemon's child ended, once [liveness] has reaped it. + A signal here is a crash, and a crash keeps [dir] on disk: see + [remove_session_dirs]. *) + mutable died : Unix.process_status option; } (* The program's stdout is a pipe into this process, so that an editor can see @@ -597,7 +601,7 @@ let liveness t = Some (match Unix.waitpid [ Unix.WNOHANG ] child with | 0, _ -> true - | _ -> false + | _, status -> t.died <- Some status; false | exception Unix.Unix_error _ -> false) in liveness_of ~child_alive ~finished:t.finished ~program:(Program.state ()) @@ -4241,6 +4245,29 @@ let accept_loop ?grace t ls = in go () +(* A session that ended cleanly takes its directories with it: [t.dir], which + holds the program, every module it was sent and the agent's socket, and + [Build.workdir], which holds what the builds left there. Both are named for + this process's pid, so both are this session's and no other's. Nothing looks + further than those two paths — a stale pid in another directory's name is + not proof that its session is over. + + Called only on the way out of a clean end. A crash leaves both where they + are, because they are what there is to look at afterwards: the IR the + program was built from and the modules it was running. *) +let remove_session_dirs t = + let rec remove path = + match (Unix.lstat path).Unix.st_kind with + | Unix.S_DIR -> + Array.iter (fun n -> remove (Filename.concat path n)) + (try Sys.readdir path with Sys_error _ -> [||]); + (try Unix.rmdir path with Unix.Unix_error _ -> ()) + | _ -> (try Unix.unlink path with Unix.Unix_error _ -> ()) + | exception Unix.Unix_error _ -> () + in + remove t.dir; + remove (Build.workdir ()) + (* [debug] is off by default, which keeps [flan dev] exactly what it was: a -O2 host and -O2 modules. It is opt-in rather than always-on because a debug build is an -O0 build — [llvm.dbg.declare] describes an alloca and mem2reg @@ -4266,28 +4293,26 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () = actually given, not a second emission of it, which is the difference between showing what the process was built from and showing what it probably was. [Build.executable] leaves it in its own working directory - under the module's basename; it is moved here so that nothing else in this - process can reuse the name. *) + and says where; it is moved here so that nothing else in this process can + reuse the name. *) (* The host and the modules are one decision. DWARF in a redefinition is only half a debuggable dev loop: lldb re-resolves a *name* breakpoint against each module as it loads either way, but a breakpoint set on a line in the .flan buffer needs a line table on both sides — the host's to fire before the first C-c C-c, the module's to follow the reload. *) - ignore - (Build.executable - ~opts:{ Build.default with Build.dev = true; Build.keep = true; - Build.debug; Build.x86 } - ~csrcs:l.Load.csrcs ~lflags:l.Load.lflags session.Session.host ~out:exe); + let _, kept = + Build.executable + ~opts:{ Build.default with Build.dev = true; Build.keep = true; + Build.debug; Build.x86 } + ~csrcs:l.Load.csrcs ~lflags:l.Load.lflags session.Session.host ~out:exe + in (* Host and modules are chosen together, which is the whole licence: an [--x86] host gets [--x86] modules because one flag set both, and the source [Build.executable] kept is assembly rather than IR. *) let host_ll = Filename.concat dir (if x86 then "host.s" else "host.ll") in - (try - Sys.rename - (Filename.concat (Build.workdir ()) - (Filename.basename exe ^ if x86 then ".s" else ".ll")) - host_ll - with Sys_error _ -> ()); + (match kept with + | Some src -> (try Sys.rename src host_ll with Sys_error _ -> ()) + | None -> ()); let agent = Filename.concat dir "agent.sock" in (* The program's source names some socket path; the daemon is the one that @@ -4337,7 +4362,7 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () = { session; child = Some child; agent; dir; stdout = rd; out = Buffer.create 4096; n = 0; gen = 0; owners = Hashtbl.create 32; host_ll; host_exe = exe; finished = false; agent_watch = None; - park_noted = false } + park_noted = false; died = None } in ignore_sigpipe (); (try Unix.unlink sock with Unix.Unix_error _ -> ()); @@ -4352,7 +4377,13 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () = (try Unix.close ls with Unix.Unix_error _ -> ()); (try Unix.close rd with Unix.Unix_error _ -> ()); (try Unix.unlink sock with Unix.Unix_error _ -> ())) - (fun () -> accept_loop t ls) + (fun () -> accept_loop t ls); + (* Here only when the loop returned: an exception out of it has already + left through the [finally]. A child killed by a signal is the crash that + keeps the directory. *) + match t.died with + | Some (Unix.WSIGNALED _) -> () + | _ -> remove_session_dirs t (* ── One process: the program and the compiler in the same binary ──── *) @@ -5172,7 +5203,7 @@ let merged_setup () = { session; child = None; agent; dir; stdout = rd; out = Buffer.create 4096; n = 0; gen = 0; owners = Hashtbl.create 32; host_ll; host_exe = exe; finished = false; agent_watch = None; - park_noted = false } + park_noted = false; died = None } in ignore_sigpipe (); (try Unix.unlink sock with Unix.Unix_error _ -> ()); @@ -5228,12 +5259,18 @@ let merged_serve () = this process. See [agent_check] for where the sentence is said now, and [eval] for what a delivery to such a program honestly reports. *) t.agent_watch <- Some (Unix.gettimeofday () +. 10.); - (match accept_loop t ls with - | () -> () - | exception e -> - Printf.eprintf "flan dev: %s\n%!" (Printexc.to_string e)); + let clean = + match accept_loop t ls with + | () -> true + | exception e -> + Printf.eprintf "flan dev: %s\n%!" (Printexc.to_string e); + false + in (try Unix.close ls with Unix.Unix_error _ -> ()); (try Unix.unlink sock with Unix.Unix_error _ -> ()); + (* The program is this process, so a program that crashed never gets here; + the one end that does and is not clean is the loop raising. *) + if clean then remove_session_dirs t; (* [close] from the editor ends the session, and so now does an editor that stopped being there; in one process either means the program too — which is what the daemon did by killing its child. [_exit] for the loader-lock