// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const jsruntime = @import("jsruntime"); const Browser = @import("browser/browser.zig").Browser; const server = @import("server.zig"); const parser = @import("netsurf"); const apiweb = @import("apiweb.zig"); pub const Types = jsruntime.reflect(apiweb.Interfaces); pub const UserContext = apiweb.UserContext; pub const IO = @import("asyncio").Wrapper(jsruntime.Loop); const log = std.log.scoped(.cli); pub const std_options = .{ // Set the log level to info .log_level = .debug, // Define logFn to override the std implementation .logFn = logFn, }; pub fn main() !void { var mem: [4096]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&mem); const args = try parseArgs(fba.allocator()); if (args.mode == .usage) { // don't need to create a gpa in this case return args.printUsageAndExit(args.mode.usage); } // allocator // - in Debug mode we use the General Purpose Allocator to detect memory leaks // - in Release mode we use the c allocator var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const alloc = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator; defer if (builtin.mode == .Debug) { _ = gpa.detectLeaks(); }; switch (args.mode) { .usage => unreachable, // handled above .serve => |opts| { const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| { log.err("address (host:port) {any}\n", .{err}); return args.printUsageAndExit(false); }; var loop = try jsruntime.Loop.init(alloc); defer loop.deinit(); const timeout = std.time.ns_per_s * @as(u64, opts.timeout); server.run(alloc, address, timeout, &loop) catch |err| { log.err("Server error", .{}); return err; }; }, .fetch => |opts| { log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump }); // vm const vm = jsruntime.VM.init(); defer vm.deinit(); // loop var loop = try jsruntime.Loop.init(alloc); defer loop.deinit(); // browser var browser = Browser.init(alloc, &loop); defer browser.deinit(); var session = try browser.newSession({}); // page const page = try session.createPage(); try page.start(null); defer page.end(); _ = page.navigate(opts.url, null) catch |err| switch (err) { error.UnsupportedUriScheme, error.UriMissingHost => { log.err("'{s}' is not a valid URL ({any})\n", .{ opts.url, err }); return args.printUsageAndExit(false); }, else => { log.err("'{s}' fetching error ({any})\n", .{ opts.url, err }); return err; }, }; try page.wait(); // dump if (opts.dump) { try page.dump(std.io.getStdOut()); } }, } } const Command = struct { mode: Mode, exec_name: []const u8, const Mode = union(enum) { usage: bool, // false when being printed because of an error fetch: Fetch, serve: Serve, }; const Serve = struct { host: []const u8, port: u16, timeout: u8, }; const Fetch = struct { url: []const u8, dump: bool = false, }; fn printUsageAndExit(self: *const Command, success: bool) void { const usage = \\usage: {s} command [options] [URL] \\ \\Command can be either 'fetch', 'serve' or 'help' \\ \\fetch command \\Fetches the specified URL \\Example: {s} fetch --dump https://lightpanda.io/ \\ \\Options: \\--dump Dumps document to stdout. \\ Defaults to false. \\ \\serve command \\Starts a websocket CDP server \\Example: {s} server --host 127.0.0.1 --port 9222 \\ \\Options: \\--host Host of the CDP server \\ \\ Defaults to "127.0.0.1" \\--port Port of the CDP server \\ \\ Defaults to 9222 \\--timeout Inactivity timeout in seconds before disconnecting clients \\ Defaults to 3 (seconds) \\ \\help command \\Displays this message ; std.debug.print(usage, .{ self.exec_name, self.exec_name, self.exec_name }); if (success) { return std.process.cleanExit(); } std.process.exit(1); } }; fn parseArgs(allocator: Allocator) !Command { var args = try std.process.argsWithAllocator(allocator); const exec_name = std.fs.path.basename(args.next().?); var cmd = Command{ .mode = .{ .usage = false }, .exec_name = try allocator.dupe(u8, exec_name), }; const command = args.next() orelse ""; if (std.mem.eql(u8, command, "serve")) { cmd.mode = .{ .serve = parseServeArgs(allocator, &args) catch return cmd }; return cmd; } if (std.mem.eql(u8, command, "fetch")) { cmd.mode = .{ .fetch = parseFetchArgs(allocator, &args) catch return cmd }; return cmd; } if (std.mem.eql(u8, command, "help")) { cmd.mode = .{ .usage = true }; return cmd; } log.err("first argument should be one of: fetch, serve or help", .{}); return cmd; } fn parseServeArgs( allocator: Allocator, args: *std.process.ArgIterator, ) !Command.Serve { var host: []const u8 = "127.0.0.1"; var port: u16 = 9222; var timeout: u8 = 3; while (args.next()) |opt| { if (std.mem.eql(u8, "--host", opt)) { const str = args.next() orelse { log.err("--host argument requires an value", .{}); return error.InvalidMissingHost; }; host = try allocator.dupe(u8, str); continue; } if (std.mem.eql(u8, "--port", opt)) { const str = args.next() orelse { log.err("--port argument requires an value", .{}); return error.InvalidMissingPort; }; port = std.fmt.parseInt(u16, str, 10) catch |err| { log.err("--port value is invalid: {}", .{err}); return error.InvalidPort; }; continue; } if (std.mem.eql(u8, "--timeout", opt)) { const str = args.next() orelse { log.err("--timeout argument requires an value", .{}); return error.MissingTimeout; }; timeout = std.fmt.parseInt(u8, str, 10) catch |err| { log.err("--timeout value is invalid: {}", .{err}); return error.InvalidTimeout; }; continue; } log.err("Unknown option to serve command: '{s}'", .{opt}); return error.UnkownOption; } return .{ .host = host, .port = port, .timeout = timeout, }; } fn parseFetchArgs( allocator: Allocator, args: *std.process.ArgIterator, ) !Command.Fetch { var dump: bool = false; var url: ?[]const u8 = null; while (args.next()) |opt| { if (std.mem.eql(u8, "--dump", opt)) { dump = true; continue; } if (std.mem.startsWith(u8, opt, "--")) { log.err("Unknown option to serve command: '{s}'", .{opt}); return error.UnkownOption; } if (url != null) { log.err("Can only fetch 1 URL", .{}); return error.TooManyURLs; } url = try allocator.dupe(u8, opt); } if (url == null) { log.err("A URL must be provided to the fetch command", .{}); return error.MissingURL; } return .{ .url = url.?, .dump = dump }; } var verbose: bool = builtin.mode == .Debug; // In debug mode, force verbose. fn logFn( comptime level: std.log.Level, comptime scope: @Type(.EnumLiteral), comptime format: []const u8, args: anytype, ) void { if (!verbose) { // hide all messages with level greater of equal to debug level. if (@intFromEnum(level) >= @intFromEnum(std.log.Level.debug)) return; } // default std log function. std.log.defaultLog(level, scope, format, args); }