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, .{}); }