Unknown state [2025-08-04]
This commit is contained in:
25
hotswap/src.bak/bar.zig
Normal file
25
hotswap/src.bak/bar.zig
Normal file
@@ -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);
|
||||
}
|
95
hotswap/src.bak/core.zig
Normal file
95
hotswap/src.bak/core.zig
Normal file
@@ -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);
|
25
hotswap/src.bak/foo.zig
Normal file
25
hotswap/src.bak/foo.zig
Normal file
@@ -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);
|
||||
}
|
222
hotswap/src.bak/main.zig
Normal file
222
hotswap/src.bak/main.zig
Normal file
@@ -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, .{});
|
||||
}
|
@@ -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.
|
||||
|
3
hotswap/src/common.zig
Normal file
3
hotswap/src/common.zig
Normal file
@@ -0,0 +1,3 @@
|
||||
pub const Action = enum { quack, squawk, chirp, honk };
|
||||
|
||||
pub const Perform = *const fn (Action) void;
|
42
hotswap/src/design.md
Normal file
42
hotswap/src/design.md
Normal file
@@ -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.
|
||||
|
@@ -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())});
|
||||
}
|
||||
|
@@ -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, .{});
|
||||
}
|
||||
|
102
hotswap/src/mods.zig
Normal file
102
hotswap/src/mods.zig
Normal file
@@ -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);
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user