Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 27 additions & 14 deletions app/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -886,25 +886,38 @@ pub fn themeFilenameToName(trimmed: []const u8) ?[]const u8 {
return null;
}

/// Human-readable, actionable explanation for a `PluginLoader.LoadError`.
/// Human-readable, actionable explanation for a `PluginLoader.LoadError`. The desktop and web
/// loaders have different error sets that share a name wherever they mean the same thing, so the
/// reasons are kept by name (`load_failure_reasons`): an error either loader gains without a
/// reason there is a compile error, as a missing switch prong would be.
pub fn pluginLoadFailureReason(err: PluginLoader.LoadError) []const u8 {
return switch (err) {
error.AbiMismatch => "built against an incompatible Fizzy SDK — rebuild the plugin against this Fizzy build",
error.AbiBuildEnvMismatch => "SDK versions match, but optimize mode does not match",
error.SdkVersionMismatch => "requires a newer Fizzy SDK — update Fizzy or install a matching plugin build",
error.PluginIdMismatch => "plugin id in the dylib does not match its filename — rename the file or fix manifest.id",
error.DylibOpenFailed => "the plugin library could not be opened (missing file, wrong architecture, or unresolved symbols)",
error.RegisterRejected => "the plugin's register() was rejected (often a duplicate plugin id — a built-in or another plugin already claims it)",
error.AbiFingerprintSymbolMissing,
error.RegisterSymbolMissing,
error.SetGlobalsSymbolMissing,
error.SetDvuiContextSymbolMissing,
error.SetRenderBridgeSymbolMissing,
error.SdkVersionSymbolMissing,
=> "the plugin is missing required entry symbols — rebuild it from a current root.zig template",
inline else => |e| @field(load_failure_reasons, @errorName(e)),
};
}

const missing_symbols_reason = "the plugin is missing required entry symbols — rebuild it from a current root.zig template";

const load_failure_reasons = .{
.AbiMismatch = "built against an incompatible Fizzy SDK — rebuild the plugin against this Fizzy build",
.AbiBuildEnvMismatch = "SDK versions match, but optimize mode does not match",
.SdkVersionMismatch = "requires a newer Fizzy SDK — update Fizzy or install a matching plugin build",
.PluginIdMismatch = "plugin id in the dylib does not match its filename — rename the file or fix manifest.id",
.DylibOpenFailed = "the plugin library could not be opened (missing file, wrong architecture, or unresolved symbols)",
.RegisterRejected = "the plugin's register() was rejected (often a duplicate plugin id — a built-in or another plugin already claims it)",
.AbiFingerprintSymbolMissing = missing_symbols_reason,
.RegisterSymbolMissing = missing_symbols_reason,
.SetGlobalsSymbolMissing = missing_symbols_reason,
.SetDvuiContextSymbolMissing = missing_symbols_reason,
.SetRenderBridgeSymbolMissing = missing_symbols_reason,
.SdkVersionSymbolMissing = missing_symbols_reason,
// The web loader's own: a linked module without an entry point, a build that cannot load
// plugins at all, and an allocation failing on the way.
.LoadFailed = missing_symbols_reason,
.Unsupported = "this build of Fizzy cannot load plugins",
.OutOfMemory = "ran out of memory while loading",
};

pub fn formatPluginProbeDetail(allocator: std.mem.Allocator, info: PluginLoader.PluginVersionInfo) ![]const u8 {
return std.fmt.allocPrint(allocator, "plugin {d}.{d}.{d}, min SDK {d}.{d}.{d}", .{
info.plugin_version.major,
Expand Down
11 changes: 11 additions & 0 deletions app/store/PluginLoader_web.zig
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,17 @@ fn readVersionTriplet(get_fn: ?dylib_api.GetSdkVersionFn) std.SemanticVersion {
return .{ .major = 0, .minor = 0, .patch = 0 };
}

/// The versions a linked module declares, read whether or not `prepare` accepts it — the web's
/// `probeVersionInfo`, so a refused build is reported with the same detail as one on disk. Only
/// the triplet getters run, which `prepare` already calls on a module whose fingerprint is wrong.
pub fn versionInfo(lib: WebDynLib) PluginVersionInfo {
return .{
.plugin_version = readVersionTriplet(lib.lookup(dylib_api.GetSdkVersionFn, dylib_api.symbol_plugin_version)),
.built_with_sdk_version = readVersionTriplet(lib.lookup(dylib_api.GetSdkVersionFn, dylib_api.symbol_sdk_version)),
.min_sdk_version = readVersionTriplet(lib.lookup(dylib_api.GetSdkVersionFn, dylib_api.symbol_min_sdk_version)),
};
}

/// A linked module that has passed every check and has not yet been registered. Split from
/// `register` so an update can find out whether the new build is acceptable *before* the running
/// one is torn down: everything that can reject a module (fingerprint, SDK version, declared id,
Expand Down
26 changes: 25 additions & 1 deletion src/editor/Editor.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1513,6 +1513,7 @@ const WebPluginRequest = struct {
}
const lib = arrival.lib orelse {
dvui.log.err("web plugin '{s}' ({s}): the page could not load it", .{ req.id, req.url });
req.failed("the page could not fetch or link the plugin's module (offline, a changed download, or not a plugin)", null);
gpa.free(req.url);
return;
};
Expand All @@ -1521,6 +1522,7 @@ const WebPluginRequest = struct {
// replacing is still running and still owns its documents.
const ready = PluginLoader.prepare(req.url, req.id, lib) catch |err| {
dvui.log.err("web plugin '{s}' ({s}): refused: {s}", .{ req.id, req.url, @errorName(err) });
req.failed(App.pluginLoadFailureReason(err), lib);
gpa.free(req.url);
return;
};
Expand All @@ -1543,6 +1545,7 @@ const WebPluginRequest = struct {
.arg_c = null,
}) catch |err| {
dvui.log.err("web plugin '{s}' ({s}): register failed: {s}", .{ req.id, req.url, @errorName(err) });
req.failed(App.pluginLoadFailureReason(err), lib);
gpa.free(req.url);
return;
};
Expand All @@ -1551,6 +1554,8 @@ const WebPluginRequest = struct {
return;
};
registered = true;
// As `loadUserPluginById` does: a build that loads retires any failure an earlier one left.
editor.app.clearFailedUserPlugin(req.id);
App.syncLoadedPluginDvuiContexts(&editor.app);
App.syncLoadedPluginRenderBridge(&editor.app);
for (editor.app.host.plugins.items) |p| {
Expand All @@ -1571,6 +1576,23 @@ const WebPluginRequest = struct {
dvui.log.info("web plugin '{s}' loaded from {s}", .{ req.id, req.url });
editor.app.host.refresh();
}

/// This build did not take. With no build of this id running (a remembered build coming
/// back, a first install, a repair), the store's Installed pane shows it as failed and says
/// why, as the desktop does for a build on disk (`App.recordLoadFailure`) — before, the web
/// said so only in the console. An update the running build stays in front of is only noted,
/// as a failed reload is on the desktop; its row in the update window turns to Retry
/// (`PluginStore.webLoadFailed`).
fn failed(req: *const WebPluginRequest, reason: []const u8, lib: ?PluginLoader.WebDynLib) void {
const app = &req.editor.app;
if (app.host.pluginById(req.id) != null) return app.noteLoadFailed(req.id, reason);
const info: ?PluginLoader.PluginVersionInfo = if (lib) |l| PluginLoader.versionInfo(l) else null;
const detail: ?[]const u8 = if (info) |i| App.formatPluginProbeDetail(app.gpa, i) catch null else null;
defer if (detail) |d| app.gpa.free(d);
// `recordPluginFailure` appends: one card per id however many times it is retried.
app.clearFailedUserPlugin(req.id);
app.recordPluginFailure(req.id, reason, detail, if (info) |i| i.plugin_version else null, .{});
}
};

/// The page asks for a plugin: `?plugin=<id>` on the URL (`plugins/<id>/<id>.wasm` beside
Expand Down Expand Up @@ -1934,9 +1956,11 @@ pub fn uninstallPlugin(editor: *Editor, id: []const u8, force: bool) !void {
error.NotUnloadable => {}, // already gone
else => return err,
};
// As on the desktop: a reinstall asks again (see `clearPluginOwnershipRecord`).
// As on the desktop: a reinstall asks again (see `clearPluginOwnershipRecord`), and a
// refused build's card goes with it.
editor.app.untrackDisabledPlugin(id);
editor.clearPluginOwnershipRecord(id);
editor.app.clearFailedUserPlugin(id);
editor.rebuildExtensionOwnerCache();
return;
}
Expand Down
Loading