jetzig/cli/commands/server.zig
Bob Farrell 1565ae3b73 Debug console
Use `jetzig server` or `zig build -Ddebug_console=true run` to launch a
development server with the debug console enabled.

When an error is encountered a formatted stack trace is dumped to the
browser along with the current response data. Both outputs are
syntax-highlighted using Prism JS.
2024-11-23 18:07:19 +00:00

155 lines
4.4 KiB
Zig

const std = @import("std");
const args = @import("args");
const util = @import("../util.zig");
pub const watch_changes_pause_duration = 1 * 1000 * 1000 * 1000;
/// Command line options for the `server` command.
pub const Options = struct {
reload: bool = true,
pub const meta = .{
.full_text =
\\Launches a development server.
\\
\\The development server reloads when files in `src/` are updated.
\\
\\To disable this behaviour, pass `--reload=false`
\\
\\Example:
\\
\\ jetzig server
\\ jetzig server --reload=false
,
.option_docs = .{
.reload = "Enable or disable automatic reload on update (default: true)",
},
};
};
/// Run the `jetzig server` command.
pub fn run(
allocator: std.mem.Allocator,
options: Options,
writer: anytype,
T: type,
main_options: T,
) !void {
if (main_options.options.help) {
try args.printHelp(Options, "jetzig server", writer);
return;
}
if (main_options.positionals.len > 0) {
std.debug.print("The `server` command does not accept positional arguments.", .{});
return error.JetzigCommandError;
}
var cwd = try util.detectJetzigProjectDir();
defer cwd.close();
const realpath = try std.fs.realpathAlloc(allocator, ".");
defer allocator.free(realpath);
var mtime = try totalMtime(allocator, cwd, "src");
std.debug.print(
"Launching development server. [reload:{s}]\n",
.{
if (options.reload) "enabled" else "disabled",
},
);
while (true) {
util.runCommandInDir(
allocator,
&.{
"zig",
"build",
util.environmentBuildOption(main_options.options.environment),
"-Djetzig_runner=true",
"-Ddebug_console=true",
"install",
"--color",
"on",
},
.{ .path = realpath },
) catch {
std.debug.print("Build failed, waiting for file change...\n", .{});
try awaitFileChange(allocator, cwd, &mtime);
std.debug.print("Changes detected, restarting server...\n", .{});
continue;
};
const exe_path = try util.locateExecutable(allocator, cwd, .{});
if (exe_path == null) {
std.debug.print("Unable to locate compiled executable. Exiting.\n", .{});
std.process.exit(1);
}
const argv = &[_][]const u8{exe_path.?};
defer allocator.free(exe_path.?);
var process = std.process.Child.init(argv, allocator);
process.stdin_behavior = .Inherit;
process.stdout_behavior = .Inherit;
process.stderr_behavior = .Inherit;
process.cwd = realpath;
var stdout_buf = std.ArrayList(u8).init(allocator);
defer stdout_buf.deinit();
var stderr_buf = std.ArrayList(u8).init(allocator);
defer stderr_buf.deinit();
try process.spawn();
if (!options.reload) {
const term = try process.wait();
std.process.exit(term.Exited);
}
// HACK: This currenly doesn't restart the server when it exits, maybe that
// could be implemented in the future.
try awaitFileChange(allocator, cwd, &mtime);
std.debug.print("Changes detected, restarting server...\n", .{});
_ = try process.kill();
}
}
fn awaitFileChange(allocator: std.mem.Allocator, cwd: std.fs.Dir, mtime: *i128) !void {
while (true) {
std.time.sleep(watch_changes_pause_duration);
const new_mtime = try totalMtime(allocator, cwd, "src");
if (new_mtime > mtime.*) {
mtime.* = new_mtime;
return;
}
}
}
fn totalMtime(allocator: std.mem.Allocator, cwd: std.fs.Dir, sub_path: []const u8) !i128 {
var dir = try cwd.openDir(sub_path, .{ .iterate = true });
defer dir.close();
var walker = try dir.walk(allocator);
defer walker.deinit();
var sum: i128 = 0;
while (try walker.next()) |entry| {
if (entry.kind != .file) continue;
const extension = std.fs.path.extension(entry.path);
if (std.mem.eql(u8, extension, ".zig") or std.mem.eql(u8, extension, ".zmpl")) {
const stat = try dir.statFile(entry.path);
sum += stat.mtime;
}
}
return sum;
}