From 6e4b76a6d9cd8cb73fab4b11ae58be45ac46e7d0 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Mon, 4 Aug 2025 22:24:25 -0400 Subject: [PATCH] Unknown state [2025-08-04] --- hotswap/src.bak/bar.zig | 25 +++++ hotswap/src.bak/core.zig | 95 ++++++++++++++++ hotswap/src.bak/foo.zig | 25 +++++ hotswap/src.bak/main.zig | 222 +++++++++++++++++++++++++++++++++++++ hotswap/src/bar.zig | 28 ++--- hotswap/src/common.zig | 3 + hotswap/src/design.md | 42 +++++++ hotswap/src/foo.zig | 24 +--- hotswap/src/main.zig | 229 +++++---------------------------------- hotswap/src/mods.zig | 102 +++++++++++++++++ 10 files changed, 551 insertions(+), 244 deletions(-) create mode 100644 hotswap/src.bak/bar.zig create mode 100644 hotswap/src.bak/core.zig create mode 100644 hotswap/src.bak/foo.zig create mode 100644 hotswap/src.bak/main.zig create mode 100644 hotswap/src/common.zig create mode 100644 hotswap/src/design.md create mode 100644 hotswap/src/mods.zig diff --git a/hotswap/src.bak/bar.zig b/hotswap/src.bak/bar.zig new file mode 100644 index 0000000..3fca266 --- /dev/null +++ b/hotswap/src.bak/bar.zig @@ -0,0 +1,25 @@ +const std = @import("std"); +const core = @import("core.zig"); + +const Self = @This(); + +export const MODULE = core.module(Self); + +pub fn startup(alloc: std.mem.Allocator) !*Self { + std.log.debug("!! startup {s} !!", .{@typeName(@This())}); + return try alloc.create(Self); +} + +pub fn unload(_: *Self, _: std.mem.Allocator) ![]u8 { + std.log.debug("!! unload {s} !!", .{@typeName(@This())}); + return &.{}; +} + +pub fn reload(_: *Self, _: std.mem.Allocator, _: []u8) !void { + std.log.debug("!! reload {s} !!", .{@typeName(@This())}); +} + +pub fn shutdown(self: *Self, alloc: std.mem.Allocator) void { + std.log.debug("!! shutdown {s} !!", .{@typeName(@This())}); + alloc.destroy(self); +} diff --git a/hotswap/src.bak/core.zig b/hotswap/src.bak/core.zig new file mode 100644 index 0000000..db19cca --- /dev/null +++ b/hotswap/src.bak/core.zig @@ -0,0 +1,95 @@ +const std = @import("std"); + +// Notes: +// +// "init" is an overloaded term. Try not to use it. Instead, have hooks: +// - startup / shutdown. executed once each per program lifecycle. +// - load / unload. executed once per module load cycle. +// +// So the flow is: +// - startup. prepares long-lived state. +// - load. deserializes and prepares short-lived state. +// - unload. serializes and releases short-lived state. +// - shutdown. releases long-lived state. +// +// So when the hotplug system reloads the module, it performs an unload/load cycle. +// Normal operation without any reloads includes all four stages. +// +// Some considerations: +// - a module should be able to indicate specific failure: +// - startup (preparing state) +// - load (deserialization or preparing state) +// - unload (serialization) +// - a module should be able to indicate it cannot be reloaded. +// - serialization data might come from disk, or from a reload, or be missing. +// +// On reloading and rollback: +// - when loading a module, copy the dynlib to a versioned temporary file, and load that temporary file. +// - procedure for reloading a module: +// - current old version unload (and serialize) +// - attempt new version load (and deserialize) +// - if success: release old version and delete lib +// - if failure: +// - release new version and delete lib. +// - attempt old version load (and deserialize) (catch fatal error) +// - Configure signal handlers to always clean up the temporary files on errors like segfault? +// - temp files needs to gracefully handle multiple instances of the app running at once with shared hotloading. +// +// Things should be done in a thread-safe manner. Signal to a module that it should unload gracefully, and the +// module signal when it can safely be unloaded. + +pub fn Slot(Function: type) type { + const Arguments = std.meta.ArgsTuple(Function); + const Return = @typeInfo(Function).@"fn".return_type.?; + + return extern struct { + ptr: *const fn (*const Arguments, *Return) callconv(.c) void, + + pub fn wrap(impl: Function) @This() { + return .{ + .ptr = &struct { + pub fn cimpl(args: *const Arguments, ret: *Return) callconv(.c) void { + ret.* = @call(.auto, impl, args.*); + } + }.cimpl, + }; + } + + pub fn invoke(self: @This(), args: Arguments) Return { + var ret: Return = undefined; + self.ptr(&args, &ret); + return ret; + } + }; +} + +pub fn Module(State: type) type { + return extern struct { + pub const Error = std.mem.Allocator.Error; + + pub const StartupError = Error; + pub const UnloadError = Error; + pub const ReloadError = Error; + + const Startup = Slot(fn (std.mem.Allocator) StartupError!*State); + const Unload = Slot(fn (*State, std.mem.Allocator) UnloadError![]u8); + const Reload = Slot(fn (*State, std.mem.Allocator, []u8) ReloadError!void); + const Shutdown = Slot(fn (*State, std.mem.Allocator) void); + + startup: Startup, + unload: Unload, + reload: Reload, + shutdown: Shutdown, + }; +} + +pub fn module(State: type) Module(State) { + return .{ + .startup = Module(State).Startup.wrap(State.startup), + .unload = Module(State).Unload.wrap(State.unload), + .reload = Module(State).Reload.wrap(State.reload), + .shutdown = Module(State).Shutdown.wrap(State.shutdown), + }; +} + +pub const AnyModule = Module(anyopaque); diff --git a/hotswap/src.bak/foo.zig b/hotswap/src.bak/foo.zig new file mode 100644 index 0000000..3fca266 --- /dev/null +++ b/hotswap/src.bak/foo.zig @@ -0,0 +1,25 @@ +const std = @import("std"); +const core = @import("core.zig"); + +const Self = @This(); + +export const MODULE = core.module(Self); + +pub fn startup(alloc: std.mem.Allocator) !*Self { + std.log.debug("!! startup {s} !!", .{@typeName(@This())}); + return try alloc.create(Self); +} + +pub fn unload(_: *Self, _: std.mem.Allocator) ![]u8 { + std.log.debug("!! unload {s} !!", .{@typeName(@This())}); + return &.{}; +} + +pub fn reload(_: *Self, _: std.mem.Allocator, _: []u8) !void { + std.log.debug("!! reload {s} !!", .{@typeName(@This())}); +} + +pub fn shutdown(self: *Self, alloc: std.mem.Allocator) void { + std.log.debug("!! shutdown {s} !!", .{@typeName(@This())}); + alloc.destroy(self); +} diff --git a/hotswap/src.bak/main.zig b/hotswap/src.bak/main.zig new file mode 100644 index 0000000..c492e83 --- /dev/null +++ b/hotswap/src.bak/main.zig @@ -0,0 +1,222 @@ +const std = @import("std"); +const linux = std.os.linux; +const core = @import("core.zig"); + +const ModLib = struct { + path: []const u8, + lib: std.DynLib, + mod: *core.Module(anyopaque), + + fn open(realpath: []const u8, version: u32, alloc: std.mem.Allocator) !ModLib { + const basename = std.fs.path.basename(realpath); + const name = try std.fmt.allocPrint( + alloc, + ".{s}.{d}", + .{ basename, version }, + ); + defer alloc.free(name); + + const path = if (std.fs.path.dirname(realpath)) |dirname| + try std.fs.path.join(alloc, &.{ dirname, name }) + else + try alloc.dupe(u8, name); + errdefer alloc.free(path); + + try std.fs.copyFileAbsolute(realpath, path, .{}); + + var lib = try std.DynLib.open(path); + errdefer lib.close(); + + const mod = lib.lookup(*core.Module(anyopaque), "MODULE") orelse + return error.MissingModuleDefinition; + + return .{ + .path = path, + .lib = lib, + .mod = mod, + }; + } + + fn close(self: *ModLib, alloc: std.mem.Allocator) void { + self.lib.close(); + // std.fs.deleteFileAbsolute(self.path) catch std.log.warn("Failed to delete {s}", .{self.path}); + alloc.free(self.path); + } +}; + +const DynamicModule = struct { + gpa: std.heap.GeneralPurposeAllocator(.{}), + realpath: []const u8, + curr: ModLib, + state: *anyopaque, + next: u32, + + pub fn init(path: []const u8, alloc: std.mem.Allocator) !DynamicModule { + const realpath = try std.fs.cwd().realpathAlloc(alloc, path); + errdefer alloc.free(realpath); + + var gpa = std.heap.GeneralPurposeAllocator(.{}).init; + errdefer if (gpa.deinit() == .leak) { + std.log.warn("Module {s} leaked.", .{realpath}); + }; + + var self: DynamicModule = .{ + .gpa = gpa, + .realpath = realpath, + .curr = undefined, + .state = undefined, + .next = 0, + }; + + self.curr = try ModLib.open(realpath, self.next, alloc); + errdefer self.curr.close(alloc); + + self.state = try self.curr.mod.startup.invoke(.{self.gpa.allocator()}); + errdefer self.curr.mod.shutdown.invoke(.{ self.state, self.gpa.allocator() }); + + // todo deserialize from disk. + try self.curr.mod.reload.invoke(.{ self.state, self.gpa.allocator(), &.{} }); + + self.next += 1; + + return self; + } + + pub fn reload(self: *@This(), alloc: std.mem.Allocator) !void { + var next = try ModLib.open(self.realpath, self.next, alloc); + errdefer next.close(alloc); + + const data = try self.curr.mod.unload.invoke(.{ self.state, self.gpa.allocator() }); + errdefer self.curr.mod.reload.invoke(.{ self.state, self.gpa.allocator(), data }) catch + std.debug.panic("Failed to rollback to {s}", .{self.curr.path}); + + try next.mod.reload.invoke(.{ self.state, self.gpa.allocator(), data }); + + self.curr.close(alloc); + self.next += 1; + self.curr = next; + } + + pub fn deinit(self: *DynamicModule, alloc: std.mem.Allocator) void { + const data = self.curr.mod.unload.invoke(.{ self.state, self.gpa.allocator() }) catch + std.debug.panic("Failed to unload {s}", .{self.curr.path}); + + _ = data; // todo serialize to disk. + + self.curr.mod.shutdown.invoke(.{ self.state, self.gpa.allocator() }); + + self.curr.close(alloc); + + if (self.gpa.deinit() == .leak) { + std.log.warn("Module {s} leaked.", .{self.realpath}); + } + + alloc.free(self.realpath); + } +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}).init; + defer _ = gpa.deinit(); + const alloc = gpa.allocator(); + + const self = try std.fs.selfExeDirPathAlloc(alloc); + defer alloc.free(self); + std.log.debug("running from '{s}'", .{self}); + + var env = try std.process.getEnvMap(alloc); + defer env.deinit(); + + const mods_path = if (env.get("MODS_DIR")) |envvar| blk: { + break :blk try alloc.dupe(u8, envvar); + } else blk: { + const dir = try std.fs.selfExeDirPathAlloc(alloc); + defer alloc.free(dir); + break :blk try std.fs.path.resolve(alloc, &.{ dir, "..", "mods" }); + }; + defer alloc.free(mods_path); + + var mods: std.StringHashMapUnmanaged(DynamicModule) = .{}; + defer { + var it = mods.valueIterator(); + while (it.next()) |mod| { + mod.deinit(alloc); + } + mods.deinit(alloc); + } + + std.log.debug("loading mods in '{s}'", .{mods_path}); + { + var dir = try std.fs.openDirAbsolute(mods_path, .{ .iterate = true }); + defer dir.close(); + + var it = dir.iterate(); + while (try it.next()) |entry| { + const mod_path = try std.fs.path.resolve(alloc, &.{ mods_path, entry.name }); + defer alloc.free(mod_path); + + if (std.mem.startsWith(u8, std.fs.path.basename(mod_path), ".")) + continue; + + var mod = try DynamicModule.init(mod_path, alloc); + errdefer mod.deinit(alloc); + + try mods.putNoClobber(alloc, entry.name, mod); + } + } + + const fd: i32 = blk: { + const ret: isize = @bitCast(linux.inotify_init1(linux.IN.NONBLOCK)); + if (ret < 0) return error.inotify_init_error; + break :blk @intCast(ret); + }; + defer _ = linux.close(fd); + + const wd: i32 = blk: { + const pathz = try alloc.dupeZ(u8, mods_path); + defer alloc.free(pathz); + const ret: isize = @bitCast(linux.inotify_add_watch( + fd, + pathz, + linux.IN.MOVED_TO, + )); + if (ret < 0) return error.inotify_add_watch_error; + break :blk @intCast(ret); + }; + defer _ = linux.inotify_rm_watch(fd, wd); + + var fds = [_]linux.pollfd{ + .{ .fd = fd, .events = linux.POLL.IN, .revents = undefined }, + }; + + const eventbuf = try alloc.alloc(u8, 5 * (@sizeOf(linux.inotify_event) + linux.NAME_MAX)); + defer alloc.free(eventbuf); + + for (0..60 * 10) |_| { + const n = linux.poll(&fds, fds.len, std.time.ms_per_s); + + if (n > 0) { + const c = linux.read(fd, eventbuf.ptr, eventbuf.len); + + var i: usize = 0; + while (i < c) { + const event: *linux.inotify_event = @alignCast(@ptrCast(eventbuf.ptr + i)); + const name = event.getName().?; // Impossible as IN.MOVE_TO includes a name. + + if (mods.getPtr(name)) |mod| { + if (mod.reload(alloc)) |_| { + std.log.info("Reloaded {s}.", .{name}); + } else |_| { + std.log.err("Failed to reload {s}. Rolled back.", .{mod.realpath}); + } + } else { + // Ignore unloaded module. + } + + i += @sizeOf(linux.inotify_event) + event.len; + } + } + } + + std.log.debug("-" ** 80, .{}); +} diff --git a/hotswap/src/bar.zig b/hotswap/src/bar.zig index 3fca266..8dd3e95 100644 --- a/hotswap/src/bar.zig +++ b/hotswap/src/bar.zig @@ -1,25 +1,11 @@ const std = @import("std"); -const core = @import("core.zig"); +const common = @import("common.zig"); -const Self = @This(); - -export const MODULE = core.module(Self); - -pub fn startup(alloc: std.mem.Allocator) !*Self { - std.log.debug("!! startup {s} !!", .{@typeName(@This())}); - return try alloc.create(Self); +fn perform(action: common.Action) !void { + std.log.debug("{s} says {any}", .{ @typeName(@This()), action }); } -pub fn unload(_: *Self, _: std.mem.Allocator) ![]u8 { - std.log.debug("!! unload {s} !!", .{@typeName(@This())}); - return &.{}; -} - -pub fn reload(_: *Self, _: std.mem.Allocator, _: []u8) !void { - std.log.debug("!! reload {s} !!", .{@typeName(@This())}); -} - -pub fn shutdown(self: *Self, alloc: std.mem.Allocator) void { - std.log.debug("!! shutdown {s} !!", .{@typeName(@This())}); - alloc.destroy(self); -} +// TODO in comptime, loop through the struct; generate the "hook" api i used before. +// automatically generate the "interface" and return that. then consumers can import +// and use it directly, but it'll automatically go through the thing. +// use @export in comptime to export the necessary symbols. diff --git a/hotswap/src/common.zig b/hotswap/src/common.zig new file mode 100644 index 0000000..391d892 --- /dev/null +++ b/hotswap/src/common.zig @@ -0,0 +1,3 @@ +pub const Action = enum { quack, squawk, chirp, honk }; + +pub const Perform = *const fn (Action) void; diff --git a/hotswap/src/design.md b/hotswap/src/design.md new file mode 100644 index 0000000..06aa6de --- /dev/null +++ b/hotswap/src/design.md @@ -0,0 +1,42 @@ +Notes: + +"init" is an overloaded term. Try not to use it. Instead, have hooks: + +- startup / shutdown. executed once each per program lifecycle. +- load / unload. executed once per module load cycle. + +So the flow is: + +- startup. prepares long-lived state. +- load. deserializes and prepares short-lived state. +- unload. serializes and releases short-lived state. +- shutdown. releases long-lived state. + +So when the hotplug system reloads the module, it performs an unload/load cycle. +Normal operation without any reloads includes all four stages. + +Some considerations: + +- a module should be able to indicate specific failure: + - startup (preparing state) + - load (deserialization or preparing state) + - unload (serialization) +- a module should be able to indicate it cannot be reloaded. +- serialization data might come from disk, or from a reload, or be missing. + +On reloading and rollback: + +- when loading a module, copy the dynlib to a versioned temporary file, and load that temporary file. +- procedure for reloading a module: + - current old version unload (and serialize) + - attempt new version load (and deserialize) + - if success: release old version and delete lib + - if failure: + - release new version and delete lib. + - attempt old version load (and deserialize) (catch fatal error) +- Configure signal handlers to always clean up the temporary files on errors like segfault? + - temp files needs to gracefully handle multiple instances of the app running at once with shared hotloading. + +Things should be done in a thread-safe manner. Signal to a module that it should unload gracefully, and the +module signal when it can safely be unloaded. + diff --git a/hotswap/src/foo.zig b/hotswap/src/foo.zig index 3fca266..69aeee5 100644 --- a/hotswap/src/foo.zig +++ b/hotswap/src/foo.zig @@ -1,25 +1,5 @@ const std = @import("std"); -const core = @import("core.zig"); -const Self = @This(); - -export const MODULE = core.module(Self); - -pub fn startup(alloc: std.mem.Allocator) !*Self { - std.log.debug("!! startup {s} !!", .{@typeName(@This())}); - return try alloc.create(Self); -} - -pub fn unload(_: *Self, _: std.mem.Allocator) ![]u8 { - std.log.debug("!! unload {s} !!", .{@typeName(@This())}); - return &.{}; -} - -pub fn reload(_: *Self, _: std.mem.Allocator, _: []u8) !void { - std.log.debug("!! reload {s} !!", .{@typeName(@This())}); -} - -pub fn shutdown(self: *Self, alloc: std.mem.Allocator) void { - std.log.debug("!! shutdown {s} !!", .{@typeName(@This())}); - alloc.destroy(self); +export fn _invoke() void { + std.log.debug("Hello {s}", .{@typeName(@This())}); } diff --git a/hotswap/src/main.zig b/hotswap/src/main.zig index c492e83..4c9593f 100644 --- a/hotswap/src/main.zig +++ b/hotswap/src/main.zig @@ -1,222 +1,49 @@ const std = @import("std"); const linux = std.os.linux; -const core = @import("core.zig"); +const mods = @import("mods.zig"); -const ModLib = struct { - path: []const u8, - lib: std.DynLib, - mod: *core.Module(anyopaque), - - fn open(realpath: []const u8, version: u32, alloc: std.mem.Allocator) !ModLib { - const basename = std.fs.path.basename(realpath); - const name = try std.fmt.allocPrint( - alloc, - ".{s}.{d}", - .{ basename, version }, - ); - defer alloc.free(name); - - const path = if (std.fs.path.dirname(realpath)) |dirname| - try std.fs.path.join(alloc, &.{ dirname, name }) - else - try alloc.dupe(u8, name); - errdefer alloc.free(path); - - try std.fs.copyFileAbsolute(realpath, path, .{}); - - var lib = try std.DynLib.open(path); - errdefer lib.close(); - - const mod = lib.lookup(*core.Module(anyopaque), "MODULE") orelse - return error.MissingModuleDefinition; - - return .{ - .path = path, - .lib = lib, - .mod = mod, - }; - } - - fn close(self: *ModLib, alloc: std.mem.Allocator) void { - self.lib.close(); - // std.fs.deleteFileAbsolute(self.path) catch std.log.warn("Failed to delete {s}", .{self.path}); - alloc.free(self.path); - } -}; - -const DynamicModule = struct { - gpa: std.heap.GeneralPurposeAllocator(.{}), - realpath: []const u8, - curr: ModLib, - state: *anyopaque, - next: u32, - - pub fn init(path: []const u8, alloc: std.mem.Allocator) !DynamicModule { - const realpath = try std.fs.cwd().realpathAlloc(alloc, path); - errdefer alloc.free(realpath); - - var gpa = std.heap.GeneralPurposeAllocator(.{}).init; - errdefer if (gpa.deinit() == .leak) { - std.log.warn("Module {s} leaked.", .{realpath}); - }; - - var self: DynamicModule = .{ - .gpa = gpa, - .realpath = realpath, - .curr = undefined, - .state = undefined, - .next = 0, - }; - - self.curr = try ModLib.open(realpath, self.next, alloc); - errdefer self.curr.close(alloc); - - self.state = try self.curr.mod.startup.invoke(.{self.gpa.allocator()}); - errdefer self.curr.mod.shutdown.invoke(.{ self.state, self.gpa.allocator() }); - - // todo deserialize from disk. - try self.curr.mod.reload.invoke(.{ self.state, self.gpa.allocator(), &.{} }); - - self.next += 1; - - return self; - } - - pub fn reload(self: *@This(), alloc: std.mem.Allocator) !void { - var next = try ModLib.open(self.realpath, self.next, alloc); - errdefer next.close(alloc); - - const data = try self.curr.mod.unload.invoke(.{ self.state, self.gpa.allocator() }); - errdefer self.curr.mod.reload.invoke(.{ self.state, self.gpa.allocator(), data }) catch - std.debug.panic("Failed to rollback to {s}", .{self.curr.path}); - - try next.mod.reload.invoke(.{ self.state, self.gpa.allocator(), data }); - - self.curr.close(alloc); - self.next += 1; - self.curr = next; - } - - pub fn deinit(self: *DynamicModule, alloc: std.mem.Allocator) void { - const data = self.curr.mod.unload.invoke(.{ self.state, self.gpa.allocator() }) catch - std.debug.panic("Failed to unload {s}", .{self.curr.path}); - - _ = data; // todo serialize to disk. - - self.curr.mod.shutdown.invoke(.{ self.state, self.gpa.allocator() }); - - self.curr.close(alloc); - - if (self.gpa.deinit() == .leak) { - std.log.warn("Module {s} leaked.", .{self.realpath}); - } - - alloc.free(self.realpath); - } -}; +const common = @import("common.zig"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}).init; + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer _ = gpa.deinit(); const alloc = gpa.allocator(); - const self = try std.fs.selfExeDirPathAlloc(alloc); - defer alloc.free(self); - std.log.debug("running from '{s}'", .{self}); + var root = try std.fs.cwd().openDir(".mod-cache", .{}); + defer root.close(); + + var index = try mods.ModIndex.init(alloc, root); + defer index.deinit(); var env = try std.process.getEnvMap(alloc); defer env.deinit(); - const mods_path = if (env.get("MODS_DIR")) |envvar| blk: { - break :blk try alloc.dupe(u8, envvar); - } else blk: { - const dir = try std.fs.selfExeDirPathAlloc(alloc); - defer alloc.free(dir); - break :blk try std.fs.path.resolve(alloc, &.{ dir, "..", "mods" }); - }; - defer alloc.free(mods_path); - - var mods: std.StringHashMapUnmanaged(DynamicModule) = .{}; - defer { - var it = mods.valueIterator(); - while (it.next()) |mod| { - mod.deinit(alloc); - } - mods.deinit(alloc); - } - - std.log.debug("loading mods in '{s}'", .{mods_path}); - { - var dir = try std.fs.openDirAbsolute(mods_path, .{ .iterate = true }); + if (env.get("MODS_DIR")) |mods_dir| { + var dir = try std.fs.cwd().openDir(mods_dir, .{ .iterate = true }); defer dir.close(); - var it = dir.iterate(); - while (try it.next()) |entry| { - const mod_path = try std.fs.path.resolve(alloc, &.{ mods_path, entry.name }); - defer alloc.free(mod_path); + try index.load(dir); + } else { + const exe_dir_path = try std.fs.selfExeDirPathAlloc(alloc); + defer alloc.free(exe_dir_path); - if (std.mem.startsWith(u8, std.fs.path.basename(mod_path), ".")) - continue; + const mods_dir_path = try std.fs.path.resolve( + alloc, + &.{ exe_dir_path, "..", "mods" }, + ); + defer alloc.free(mods_dir_path); - var mod = try DynamicModule.init(mod_path, alloc); - errdefer mod.deinit(alloc); + var dir = try std.fs.cwd().openDir(mods_dir_path, .{}); + defer dir.close(); - try mods.putNoClobber(alloc, entry.name, mod); - } + try index.load(dir); } - const fd: i32 = blk: { - const ret: isize = @bitCast(linux.inotify_init1(linux.IN.NONBLOCK)); - if (ret < 0) return error.inotify_init_error; - break :blk @intCast(ret); - }; - defer _ = linux.close(fd); - - const wd: i32 = blk: { - const pathz = try alloc.dupeZ(u8, mods_path); - defer alloc.free(pathz); - const ret: isize = @bitCast(linux.inotify_add_watch( - fd, - pathz, - linux.IN.MOVED_TO, - )); - if (ret < 0) return error.inotify_add_watch_error; - break :blk @intCast(ret); - }; - defer _ = linux.inotify_rm_watch(fd, wd); - - var fds = [_]linux.pollfd{ - .{ .fd = fd, .events = linux.POLL.IN, .revents = undefined }, - }; - - const eventbuf = try alloc.alloc(u8, 5 * (@sizeOf(linux.inotify_event) + linux.NAME_MAX)); - defer alloc.free(eventbuf); - - for (0..60 * 10) |_| { - const n = linux.poll(&fds, fds.len, std.time.ms_per_s); - - if (n > 0) { - const c = linux.read(fd, eventbuf.ptr, eventbuf.len); - - var i: usize = 0; - while (i < c) { - const event: *linux.inotify_event = @alignCast(@ptrCast(eventbuf.ptr + i)); - const name = event.getName().?; // Impossible as IN.MOVE_TO includes a name. - - if (mods.getPtr(name)) |mod| { - if (mod.reload(alloc)) |_| { - std.log.info("Reloaded {s}.", .{name}); - } else |_| { - std.log.err("Failed to reload {s}. Rolled back.", .{mod.realpath}); - } - } else { - // Ignore unloaded module. - } - - i += @sizeOf(linux.inotify_event) + event.len; - } - } + for (index.libs.items) |*lib| { + const sym = lib.lib.lookup( + common.Perform, + "_perform", + ).?; + sym(.quack); } - - std.log.debug("-" ** 80, .{}); } diff --git a/hotswap/src/mods.zig b/hotswap/src/mods.zig new file mode 100644 index 0000000..21b4ef3 --- /dev/null +++ b/hotswap/src/mods.zig @@ -0,0 +1,102 @@ +const std = @import("std"); + +const Md5 = std.crypto.hash.Md5; +const Digest = [Md5.digest_length]u8; +const HexDigest = [2 * Md5.digest_length]u8; + +pub const ModIndex = struct { + const Mod = struct { + libidx: u16, + realpath: []const u8, + }; + + const ModLib = struct { + lib: std.DynLib, + hexdigest: HexDigest, + }; + + alloc: std.mem.Allocator, + root: std.fs.Dir, + mods: std.ArrayListUnmanaged(Mod), + libs: std.ArrayListUnmanaged(ModLib), + + pub fn init(alloc: std.mem.Allocator, root: std.fs.Dir) !@This() { + return .{ + .alloc = alloc, + .root = root, + .mods = .{}, + .libs = .{}, + }; + } + + pub fn load(self: *@This(), mods_dir: std.fs.Dir) !void { + const buffer = try self.alloc.alloc(u8, std.heap.pageSize()); + defer self.alloc.free(buffer); + + var it = mods_dir.iterate(); + while (try it.next()) |entry| { + if (entry.kind != .file) continue; + + var random: Digest = undefined; + std.crypto.random.bytes(&random); + const tempdigest = std.fmt.bytesToHex(random, .lower); + + try mods_dir.copyFile( + entry.name, + self.root, + &tempdigest, + .{}, + ); + + var md5 = Md5.init(.{}); + + { + const f = try self.root.openFile( + &tempdigest, + .{}, + ); + defer f.close(); + + while (true) { + const n = try f.read(buffer); + if (n == 0) break; + md5.update(buffer[0..n]); + } + } + + var digest: Digest = undefined; + md5.final(&digest); + + const hexdigest: HexDigest = std.fmt.bytesToHex(&digest, .lower); + try self.root.rename(&tempdigest, &hexdigest); + + var realpath_buf: [std.fs.max_path_bytes]u8 = undefined; + const realpath = try self.root.realpath(&hexdigest, &realpath_buf); + + var lib = try std.DynLib.open(realpath); + errdefer lib.close(); + + try self.libs.append(self.alloc, .{ .lib = lib, .hexdigest = hexdigest }); + } + } + + pub fn deinit(self: *@This()) void { + for (self.mods.items) |*mod| { + _ = mod; + // TODO unload + // TODO shutdown + } + + for (self.libs.items) |*lib| { + lib.lib.close(); + self.root.deleteFile(&lib.hexdigest) catch { + std.log.err( + "Failed to delete mod lib: {s}", + .{lib.hexdigest}, + ); + }; + } + + self.libs.deinit(self.alloc); + } +};