223 lines
6.9 KiB
Zig
223 lines
6.9 KiB
Zig
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, .{});
|
|
}
|