import gleam/erlang/process import gleam/otp/actor import gleam/otp/supervision import gleam/result import mala import simplifile // --- Public API --- /// A reference to the process file manager. This can be used to register /// ownership of file paths using the `register` function. /// pub opaque type ProcessFileManager { ProcessFileManager( table: mala.BagTable(process.Pid, String), subject: process.Subject(Message), ) } /// Create a new process file manager. Typically you should use the /// `supervised` function instead of this one, adding it to your application's /// supervision tree. /// pub fn start() -> actor.StartResult(ProcessFileManager) { actor.new_with_initialiser(1000, initialise) |> actor.on_message(handle_message) |> actor.start } /// A supervision specification for a new process file manager. /// pub fn supervised() -> supervision.ChildSpecification(ProcessFileManager) { supervision.worker(start) } /// Register a file system path as being owned by the process that calls this /// function, so when the process exits the process file manager will delete /// any file or directory at the path. If there is no file at that location /// then nothing happens. /// /// The first time a process calls this function they will be registered by the /// manager process, but subsequent calls work directly with ETS and do not /// produce any work for the manager. /// pub fn register( manager: ProcessFileManager, ownership_of new_path: String, ) -> Result(Nil, Nil) { let owner = process.self() let table = manager.table use already_registered <- result.try(mala.has_key(table, owner)) case already_registered { // If this process is already registered then all that needs to be done is // to insert the new file path into the table so it will be deleted later. True -> mala.insert(table, owner, new_path) // If this is a new process then it has to ask the manager process to // register it. False -> Ok(actor.call(manager.subject, 1000, Registration(new_path, _))) } } // --- Actor implementation --- pub opaque type Message { /// A new process is registering itself as an owner of files. Registration(path: String, reply: process.Subject(Nil)) /// A file-owning process has exited, meaning any files it owns now need to /// be deleted to clean-up. MonitoredProcessDown(pid: process.Pid) } type State { State(table: mala.BagTable(process.Pid, String)) } fn handle_message( state: State, message: Message, ) -> actor.Next(State, Message) { case message { MonitoredProcessDown(pid:) -> handle_monitored_process_down(state, pid) Registration(path:, reply:) -> handle_registration(state, path, reply) } } fn handle_registration( state: State, path: String, reply: process.Subject(Nil), ) -> actor.Next(State, Message) { // Monitor the process, so we get informed later when the process exits. let assert Ok(pid) = process.subject_owner(reply) as "registee must be alive" let _monitor = process.monitor(pid) // Record this initial file in the table, both so it can be cleaned up and so // the code can tell that this process is already registered without going // via the manager actor again. let assert Ok(_) = mala.insert(state.table, pid, path) as "ETS table exists" // Inform the registee their registration is complete process.send(reply, Nil) // Onwards! actor.continue(state) } fn handle_monitored_process_down( state: State, pid: process.Pid, ) -> actor.Next(State, Message) { let table = state.table // Find and un-register all the files owned by this process let assert Ok(files) = mala.get(table, pid) as "ETS table exists" let assert Ok(_) = mala.delete_key(table, pid) as "ETS table exists" // Delete them all. This function does not return an error for absent paths, // so asserting panics only for when files exist but could not be deleted. let assert Ok(_) = simplifile.delete_all(files) as "deletion must succeed" // Let's go! actor.continue(state) } fn initialise( subject: process.Subject(Message), ) -> Result(actor.Initialised(State, Message, ProcessFileManager), b) { let table = mala.new() let handle = ProcessFileManager(table:, subject:) let selector = process.new_selector() |> process.select(subject) |> process.select_monitors(convert_down_message) actor.initialised(State(table)) |> actor.selecting(selector) |> actor.returning(handle) |> Ok } fn convert_down_message(down: process.Down) -> Message { let assert process.ProcessDown(pid:, ..) = down as "process_file manager actor should never be monitoring a port" MonitoredProcessDown(pid:) }