Compare commits
19 Commits
5f1b1a52be
...
5cad3926d8
Author | SHA1 | Date | |
---|---|---|---|
5cad3926d8 | |||
c32546051d | |||
7919dd6160 | |||
9b4e1cb5bc | |||
4c43842ef3 | |||
c3e1ffc2ef | |||
1053c8f002 | |||
17661b9da2 | |||
1f58fbc743 | |||
e8430cb47a | |||
581c3de9ca | |||
73fa9791d3 | |||
1e24b3a87c | |||
9606e236a0 | |||
6242641fa4 | |||
30a4e24656 | |||
d72b5f6b6f | |||
decb2d34af | |||
a962e05d82 |
69
README.md
69
README.md
|
@ -1,7 +1,7 @@
|
|||
"Univeral Lambda" for Zig
|
||||
=========================
|
||||
|
||||
This a Zig 0.11 project intended to be used as a package to turn a zig program
|
||||
This a Zig 0.12 project intended to be used as a package to turn a zig program
|
||||
into a function that can be run as:
|
||||
|
||||
* A command line executable
|
||||
|
@ -14,69 +14,57 @@ into a function that can be run as:
|
|||
Usage - Development
|
||||
-------------------
|
||||
|
||||
From an empty directory, with Zig 0.11 installed:
|
||||
|
||||
`zig init-exe`
|
||||
|
||||
|
||||
Create a `build.zig.zon` with the following contents:
|
||||
From an empty directory, with Zig 0.12 installed:
|
||||
|
||||
```sh
|
||||
zig init-exe
|
||||
zig fetch --save https://git.lerch.org/lobo/universal-lambda-zig/archive/9b4e1cb5bc0513f0a0037b76a3415a357e8db427.tar.gz
|
||||
```
|
||||
.{
|
||||
.name = "univeral-zig-example",
|
||||
.version = "0.0.1",
|
||||
|
||||
.dependencies = .{
|
||||
.universal_lambda_build = .{
|
||||
.url = "https://git.lerch.org/lobo/universal-lambda-zig/archive/07366606696081f324591b66ab7a9a176a38424c.tar.gz",
|
||||
.hash = "122049daa19f61d778a79ffb82c64775ca5132ee5c4797d7f7d76667ab82593917cd",
|
||||
},
|
||||
.flexilib = .{
|
||||
.url = "https://git.lerch.org/lobo/flexilib/archive/c44ad2ba84df735421bef23a2ad612968fb50f06.tar.gz",
|
||||
.hash = "122051fdfeefdd75653d3dd678c8aa297150c2893f5fad0728e0d953481383690dbc",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Due to limitations in the build apis related to relative file paths, the
|
||||
dependency name currently must be "universal_lambda_build". Also, note that
|
||||
the flexilib dependency is required at all times. This requirement may go away
|
||||
with zig 0.12 (see [#17135](https://github.com/ziglang/zig/issues/17135))
|
||||
and/or changes to this library.
|
||||
|
||||
**Build.zig:**
|
||||
|
||||
* Add an import at the top:
|
||||
|
||||
```zig
|
||||
const universal_lambda = @import("universal_lambda_build");
|
||||
const universal_lambda = @import("universal-lambda-zig");
|
||||
```
|
||||
|
||||
* Set the return of the build function to return `!void` rather than `void`
|
||||
* Add a line to the build script, after any modules are used, but otherwise just
|
||||
after adding the exe is fine:
|
||||
after adding the exe is fine. Imports will also be added through universal_lambda:
|
||||
|
||||
```zig
|
||||
try universal_lambda.configureBuild(b, exe);
|
||||
const univeral_lambda_dep = b.dependency("universal-lambda-zig", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
try universal_lambda.configureBuild(b, exe, univeral_lambda_dep);
|
||||
_ = universal_lambda.addImports(b, exe, univeral_lambda_dep);
|
||||
```
|
||||
|
||||
This will provide most of the magic functionality of the package, including
|
||||
several new build steps to manage the system, and a new import to be used. For
|
||||
testing, it is also advisable to add the modules to your tests by adding a line
|
||||
like so:
|
||||
several new build steps to manage the system, as well as imports necessary
|
||||
for each of the providers. Note that addImports should also be called for
|
||||
unit tests.
|
||||
|
||||
```zig
|
||||
_ = try universal_lambda.addModules(b, main_tests);
|
||||
_ = universal_lambda.addImports(b, unit_tests, univeral_lambda_dep);
|
||||
```
|
||||
|
||||
**main.zig**
|
||||
|
||||
The build changes above will add several modules:
|
||||
`addImports` will make the following primary imports available for use:
|
||||
|
||||
* universal_lambda_handler: Main import, used to register your handler
|
||||
* universal_lambda_interface: Contains the context type used in the handler function
|
||||
* flexilib-interface: Used as a dependency of the handler. Not normally needed
|
||||
|
||||
Additional imports are available and used by the universal lambda runtime, but
|
||||
should not normally be needed for direct use:
|
||||
|
||||
* flexilib-interface: Used as a dependency of the handler
|
||||
* universal_lambda_build_options: Provides the ability to determine which provider is used
|
||||
The build type is stored under a `build_type` variable.
|
||||
* aws_lambda_runtime: Provides the aws lambda provider access to the underlying library
|
||||
|
||||
Add imports for the handler registration and interface:
|
||||
|
||||
|
@ -85,9 +73,6 @@ const universal_lambda = @import("universal_lambda_handler");
|
|||
const universal_lambda_interface = @import("universal_lambda_interface");
|
||||
```
|
||||
|
||||
Another module `universal_lambda_build_options` is available if access to the
|
||||
environment is needed. The build type is stored under a `build_type` variable.
|
||||
|
||||
Add a handler to be executed. The handler must follow this signature:
|
||||
|
||||
```zig
|
||||
|
@ -100,7 +85,7 @@ Your main function should return `!u8`. Let the package know about your handler
|
|||
return try universal_lambda.run(null, handler);
|
||||
```
|
||||
|
||||
The first parameter above is an allocator. If you have a specific handler you
|
||||
The first parameter above is an allocator. If you have a specific allocator you
|
||||
would like to use, you may specify it. Otherwise, an appropriate allocator
|
||||
will be created and used. Currently this is an ArenaAllocator wrapped around
|
||||
an appropriate base allocator, so your handler does not require deallocation.
|
||||
|
|
66
build.zig
66
build.zig
|
@ -58,28 +58,66 @@ pub fn build(b: *std.Build) !void {
|
|||
});
|
||||
const universal_lambda = @import("src/universal_lambda_build.zig");
|
||||
universal_lambda.module_root = b.build_root.path;
|
||||
_ = try universal_lambda.addModules(b, lib);
|
||||
|
||||
// re-expose flexilib-interface and aws_lambda modules downstream
|
||||
const flexilib_dep = b.dependency("flexilib", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const flexilib_module = flexilib_dep.module("flexilib-interface");
|
||||
_ = b.addModule("flexilib-interface", .{
|
||||
.root_source_file = flexilib_module.root_source_file,
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const aws_lambda_dep = b.dependency("lambda-zig", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const aws_lambda_module = aws_lambda_dep.module("lambda_runtime");
|
||||
_ = b.addModule("aws_lambda_runtime", .{
|
||||
.root_source_file = aws_lambda_module.root_source_file,
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Expose our own modules downstream
|
||||
_ = b.addModule("universal_lambda_interface", .{
|
||||
.root_source_file = b.path("src/interface.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
_ = b.addModule("universal_lambda_handler", .{
|
||||
.root_source_file = b.path("src/universal_lambda.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
universal_lambda.addImports(b, lib, null);
|
||||
|
||||
// This declares intent for the library to be installed into the standard
|
||||
// location when the user invokes the "install" step (the default step when
|
||||
// running `zig build`).
|
||||
b.installArtifact(lib);
|
||||
|
||||
const test_step = b.step("test", "Run library tests");
|
||||
const test_step = b.step("test", "Run tests (all architectures)");
|
||||
const native_test_step = b.step("test-native", "Run tests (native only)");
|
||||
|
||||
for (test_targets) |t| {
|
||||
// Creates steps for unit testing. This only builds the test executable
|
||||
// but does not run it.
|
||||
const exe_tests = b.addTest(.{
|
||||
.name = "test: as executable",
|
||||
.root_source_file = .{ .path = "src/test.zig" },
|
||||
.target = t,
|
||||
.target = b.resolveTargetQuery(t),
|
||||
.optimize = optimize,
|
||||
});
|
||||
_ = try universal_lambda.addModules(b, exe_tests);
|
||||
universal_lambda.addImports(b, exe_tests, null);
|
||||
|
||||
var run_exe_tests = b.addRunArtifact(exe_tests);
|
||||
run_exe_tests.skip_foreign_checks = true;
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
if (t.cpu_arch == null) native_test_step.dependOn(&run_exe_tests.step);
|
||||
|
||||
// Universal lambda can end up as an exe or a lib. When it is a library,
|
||||
// we end up changing the root source file away from downstream so we can
|
||||
|
@ -89,24 +127,26 @@ pub fn build(b: *std.Build) !void {
|
|||
// in the future. Scaleway, for instance, is another system that works
|
||||
// via shared library
|
||||
const lib_tests = b.addTest(.{
|
||||
.name = "test: as library",
|
||||
.root_source_file = .{ .path = "src/flexilib.zig" },
|
||||
.target = t,
|
||||
.target = b.resolveTargetQuery(t),
|
||||
.optimize = optimize,
|
||||
});
|
||||
_ = try universal_lambda.addModules(b, lib_tests);
|
||||
universal_lambda.addImports(b, lib_tests, null);
|
||||
|
||||
var run_lib_tests = b.addRunArtifact(lib_tests);
|
||||
run_lib_tests.skip_foreign_checks = true;
|
||||
// This creates a build step. It will be visible in the `zig build --help` menu,
|
||||
// and can be selected like this: `zig build test`
|
||||
// This will evaluate the `test` step rather than the default, which is "install".
|
||||
test_step.dependOn(&run_lib_tests.step);
|
||||
if (t.cpu_arch == null) native_test_step.dependOn(&run_lib_tests.step);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile) !void {
|
||||
try @import("src/universal_lambda_build.zig").configureBuild(b, cs);
|
||||
pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile, universal_lambda_zig_dep: *std.Build.Dependency) !void {
|
||||
try @import("src/universal_lambda_build.zig").configureBuild(b, cs, universal_lambda_zig_dep);
|
||||
}
|
||||
pub fn addModules(b: *std.Build, cs: *std.Build.Step.Compile) ![]const u8 {
|
||||
return try @import("src/universal_lambda_build.zig").addModules(b, cs);
|
||||
pub fn addImports(b: *std.Build, cs: *std.Build.Step.Compile, universal_lambda_zig_dep: *std.Build.Dependency) void {
|
||||
// The underlying call has an optional dependency here, but we do not.
|
||||
// Downstream must provide the dependency, which will ensure that the
|
||||
// modules we have exposed above do, in fact, get exposed
|
||||
return @import("src/universal_lambda_build.zig").addImports(b, cs, universal_lambda_zig_dep);
|
||||
}
|
||||
|
|
|
@ -4,8 +4,35 @@
|
|||
|
||||
.dependencies = .{
|
||||
.flexilib = .{
|
||||
.url = "https://git.lerch.org/lobo/flexilib/archive/3d3dab9c792651477932e2b61c9f4794ac694dcb.tar.gz",
|
||||
.hash = "1220fd7a614fe3c9f6006b630bba528e2ec9dca9c66f5ff10f7e471ad2bdd41b6c89",
|
||||
.url = "https://git.lerch.org/lobo/FlexiLib/archive/48c0e893da20c9821f6fe55516861e08b9cb6c77.tar.gz",
|
||||
.hash = "1220a4db25529543a82d199a17495aedd7766fef4f5d0122893566663721167b6168",
|
||||
},
|
||||
.@"lambda-zig" = .{
|
||||
.url = "https://git.lerch.org/lobo/lambda-zig/archive/9d1108697282d24509472d7019c97d1afe06ba1c.tar.gz",
|
||||
.hash = "12205b8a2be03b63a1b07bde373e8293c96be53a68a58054abe8689f80d691633862",
|
||||
},
|
||||
.@"cloudflare-worker-deploy" = .{
|
||||
.url = "https://git.lerch.org/lobo/cloudflare-worker-deploy/archive/0581221c11cf32dce48abc5b950d974e570afb0d.tar.gz",
|
||||
.hash = "12204e7befb1691ac40c124fcb63edf3c792cae7b4b1974edc1da23a36ba6f089d02",
|
||||
},
|
||||
},
|
||||
|
||||
// This field is optional.
|
||||
// This is currently advisory only; Zig does not yet do anything
|
||||
// with this value.
|
||||
.minimum_zig_version = "0.12.0",
|
||||
|
||||
// Specifies the set of files and directories that are included in this package.
|
||||
// Only files and directories listed here are included in the `hash` that
|
||||
// is computed for this package.
|
||||
// Paths are relative to the build root. Use the empty string (`""`) to refer to
|
||||
// the build root itself.
|
||||
// A directory listed here means that all files within, recursively, are included.
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
const std = @import("std");
|
||||
const cloudflare = @import("cloudflaredeploy.zig");
|
||||
const CloudflareDeployStep = @This();
|
||||
|
||||
pub const base_id: std.Build.Step.Id = .custom;
|
||||
|
||||
step: std.Build.Step,
|
||||
primary_javascript_file: std.Build.LazyPath,
|
||||
worker_name: []const u8,
|
||||
options: Options,
|
||||
|
||||
pub const Options = struct {
|
||||
/// if set, the primary file will not be read (and may not exist). This data
|
||||
/// will be used instead
|
||||
primary_file_data: ?[]const u8 = null,
|
||||
|
||||
/// When set, the Javascript file will be searched/replaced with the target
|
||||
/// file name for import
|
||||
wasm_name: ?struct {
|
||||
search: []const u8,
|
||||
replace: []const u8,
|
||||
} = null,
|
||||
|
||||
/// When set, the directory specified will be used rather than the current directory
|
||||
wasm_dir: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn create(
|
||||
owner: *std.Build,
|
||||
worker_name: []const u8,
|
||||
primary_javascript_file: std.Build.LazyPath,
|
||||
options: Options,
|
||||
) *CloudflareDeployStep {
|
||||
const self = owner.allocator.create(CloudflareDeployStep) catch @panic("OOM");
|
||||
self.* = CloudflareDeployStep{
|
||||
.step = std.Build.Step.init(.{
|
||||
.id = base_id,
|
||||
.name = owner.fmt("cloudflare deploy {s}", .{primary_javascript_file.getDisplayName()}),
|
||||
.owner = owner,
|
||||
.makeFn = make,
|
||||
}),
|
||||
.primary_javascript_file = primary_javascript_file,
|
||||
.worker_name = worker_name,
|
||||
.options = options,
|
||||
};
|
||||
if (options.primary_file_data == null)
|
||||
primary_javascript_file.addStepDependencies(&self.step);
|
||||
return self;
|
||||
}
|
||||
|
||||
fn make(step: *std.Build.Step, prog_node: *std.Progress.Node) !void {
|
||||
_ = prog_node;
|
||||
const b = step.owner;
|
||||
const self = @fieldParentPtr(CloudflareDeployStep, "step", step);
|
||||
|
||||
var client = std.http.Client{ .allocator = b.allocator };
|
||||
defer client.deinit();
|
||||
var proxy_text = std.os.getenv("https_proxy") orelse std.os.getenv("HTTPS_PROXY");
|
||||
if (proxy_text) |p| {
|
||||
client.deinit();
|
||||
const proxy = try std.Uri.parse(p);
|
||||
client = std.http.Client{
|
||||
.allocator = b.allocator,
|
||||
.proxy = .{
|
||||
.protocol = if (std.ascii.eqlIgnoreCase(proxy.scheme, "http")) .plain else .tls,
|
||||
.host = proxy.host.?,
|
||||
.port = proxy.port,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const script = self.options.primary_file_data orelse
|
||||
try std.fs.cwd().readFileAlloc(b.allocator, self.primary_javascript_file.path, std.math.maxInt(usize));
|
||||
defer if (self.options.primary_file_data == null) b.allocator.free(script);
|
||||
|
||||
var final_script = script;
|
||||
if (self.options.wasm_name) |n| {
|
||||
final_script = try std.mem.replaceOwned(u8, b.allocator, script, n.search, n.replace);
|
||||
if (self.options.primary_file_data == null) b.allocator.free(script);
|
||||
}
|
||||
defer if (self.options.wasm_name) |_| b.allocator.free(final_script);
|
||||
|
||||
var al = std.ArrayList(u8).init(b.allocator);
|
||||
defer al.deinit();
|
||||
try cloudflare.pushWorker(
|
||||
b.allocator,
|
||||
&client,
|
||||
self.worker_name,
|
||||
self.options.wasm_dir orelse ".",
|
||||
final_script,
|
||||
al.writer(),
|
||||
std.io.getStdErr().writer(),
|
||||
);
|
||||
const start = std.mem.lastIndexOf(u8, al.items, "http").?;
|
||||
step.name = try std.fmt.allocPrint(
|
||||
b.allocator,
|
||||
"cloudflare deploy {s} to {s}",
|
||||
.{ self.primary_javascript_file.getDisplayName(), al.items[start .. al.items.len - 1] },
|
||||
);
|
||||
}
|
83
src/awslambda.zig
Normal file
83
src/awslambda.zig
Normal file
|
@ -0,0 +1,83 @@
|
|||
const std = @import("std");
|
||||
const interface = @import("universal_lambda_interface");
|
||||
const lambda_zig = @import("aws_lambda_runtime");
|
||||
|
||||
const log = std.log.scoped(.awslambda);
|
||||
|
||||
threadlocal var universal_handler: interface.HandlerFn = undefined;
|
||||
|
||||
/// This is called by the aws lambda runtime (our import), and must
|
||||
/// call the universal handler (our downstream client). The main job here
|
||||
/// is to convert signatures, ore more specifically, build out the context
|
||||
/// that the universal handler is expecting
|
||||
fn lambdaHandler(arena: std.mem.Allocator, event_data: []const u8) ![]const u8 {
|
||||
const response_body: std.ArrayList(u8) = std.ArrayList(u8).init(arena);
|
||||
// Marshal lambda_zig data into a context
|
||||
// TODO: Maybe this should parse API Gateway data into a proper response
|
||||
// TODO: environment variables -> Headers?
|
||||
var response = interface.Response{
|
||||
.allocator = arena,
|
||||
.headers = &.{},
|
||||
.body = response_body,
|
||||
.request = .{
|
||||
.headers = &.{},
|
||||
},
|
||||
};
|
||||
|
||||
// Call back to the handler that we were given
|
||||
const result = universal_handler(arena, event_data, &response);
|
||||
|
||||
// TODO: If our universal handler writes to the response body, we should
|
||||
// handle that in a consistent way
|
||||
|
||||
// Return result from our handler back to AWS lambda via the lambda module
|
||||
return result;
|
||||
}
|
||||
|
||||
// AWS lambda zig handler: const HandlerFn = *const fn (std.mem.Allocator, []const u8) anyerror![]const u8;
|
||||
// Our handler: pub const HandlerFn = *const fn (std.mem.Allocator, []const u8, Context) anyerror![]const u8;
|
||||
pub fn run(allocator: ?std.mem.Allocator, event_handler: interface.HandlerFn) !u8 {
|
||||
universal_handler = event_handler;
|
||||
|
||||
// pub fn run(allocator: ?std.mem.Allocator, event_handler: HandlerFn) !void { // TODO: remove inferred error set?
|
||||
try lambda_zig.run(allocator, lambdaHandler);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn handler(allocator: std.mem.Allocator, event_data: []const u8) ![]const u8 {
|
||||
_ = allocator;
|
||||
return event_data;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// All code below this line is for testing
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
test {
|
||||
std.testing.refAllDecls(lambda_zig);
|
||||
}
|
||||
test "basic request" {
|
||||
// std.testing.log_level = .debug;
|
||||
const allocator = std.testing.allocator;
|
||||
const request =
|
||||
\\{"foo": "bar", "baz": "qux"}
|
||||
;
|
||||
|
||||
// This is what's actually coming back. Is this right?
|
||||
const expected_response =
|
||||
\\nothing but net
|
||||
;
|
||||
const TestHandler = struct {
|
||||
pub fn handler(alloc: std.mem.Allocator, event_data: []const u8, context: interface.Context) ![]const u8 {
|
||||
_ = alloc;
|
||||
_ = event_data;
|
||||
_ = context;
|
||||
log.debug("in handler", .{});
|
||||
return "nothing but net";
|
||||
}
|
||||
};
|
||||
universal_handler = TestHandler.handler;
|
||||
const lambda_response = try lambda_zig.test_lambda_request(allocator, request, 1, lambdaHandler);
|
||||
defer lambda_zig.deinit();
|
||||
defer allocator.free(lambda_response);
|
||||
try std.testing.expectEqualStrings(expected_response, lambda_response);
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const CloudflareDeployStep = @import("CloudflareDeployStep.zig");
|
||||
|
||||
const script = @embedFile("index.js");
|
||||
|
||||
pub fn configureBuild(b: *std.build.Builder, cs: *std.Build.Step.Compile, function_name: []const u8) !void {
|
||||
const wasm_name = try std.fmt.allocPrint(b.allocator, "{s}.wasm", .{cs.name});
|
||||
const deploy_cmd = CloudflareDeployStep.create(
|
||||
b,
|
||||
function_name,
|
||||
.{ .path = "index.js" },
|
||||
.{
|
||||
.primary_file_data = script,
|
||||
.wasm_name = .{
|
||||
.search = "custom.wasm",
|
||||
.replace = wasm_name,
|
||||
},
|
||||
.wasm_dir = b.getInstallPath(.bin, "."),
|
||||
},
|
||||
);
|
||||
deploy_cmd.step.dependOn(b.getInstallStep());
|
||||
|
||||
const deploy_step = b.step("cloudflare", "Deploy as Cloudflare worker (must be compiled with -Dtarget=wasm32-wasi)");
|
||||
deploy_step.dependOn(&deploy_cmd.step);
|
||||
}
|
|
@ -1,377 +0,0 @@
|
|||
const std = @import("std");
|
||||
|
||||
var x_auth_token: ?[:0]const u8 = undefined;
|
||||
var x_auth_email: ?[:0]const u8 = undefined;
|
||||
var x_auth_key: ?[:0]const u8 = undefined;
|
||||
var initialized = false;
|
||||
|
||||
const cf_api_base = "https://api.cloudflare.com/client/v4";
|
||||
|
||||
pub fn main() !u8 {
|
||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||
defer arena.deinit();
|
||||
const allocator = arena.allocator();
|
||||
var client = std.http.Client{ .allocator = allocator };
|
||||
defer client.deinit();
|
||||
var proxy_text = std.os.getenv("https_proxy") orelse std.os.getenv("HTTPS_PROXY");
|
||||
if (proxy_text) |p| {
|
||||
client.deinit();
|
||||
const proxy = try std.Uri.parse(p);
|
||||
client = std.http.Client{
|
||||
.allocator = allocator,
|
||||
.proxy = .{
|
||||
.protocol = if (std.ascii.eqlIgnoreCase(proxy.scheme, "http")) .plain else .tls,
|
||||
.host = proxy.host.?,
|
||||
.port = proxy.port,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const stdout_file = std.io.getStdOut().writer();
|
||||
var bw = std.io.bufferedWriter(stdout_file);
|
||||
const stdout = bw.writer();
|
||||
|
||||
var argIterator = try std.process.argsWithAllocator(allocator);
|
||||
defer argIterator.deinit();
|
||||
const exe_name = argIterator.next().?;
|
||||
var maybe_name = argIterator.next();
|
||||
if (maybe_name == null) {
|
||||
try usage(std.io.getStdErr().writer(), exe_name);
|
||||
return 1;
|
||||
}
|
||||
const worker_name = maybe_name.?;
|
||||
if (std.mem.eql(u8, worker_name, "-h")) {
|
||||
try usage(stdout, exe_name);
|
||||
return 0;
|
||||
}
|
||||
var maybe_script_name = argIterator.next();
|
||||
if (maybe_script_name == null) {
|
||||
try usage(std.io.getStdErr().writer(), exe_name);
|
||||
return 1;
|
||||
}
|
||||
const script = std.fs.cwd().readFileAlloc(allocator, maybe_script_name.?, std.math.maxInt(usize)) catch |err| {
|
||||
try usage(std.io.getStdErr().writer(), exe_name);
|
||||
return err;
|
||||
};
|
||||
|
||||
pushWorker(allocator, &client, worker_name, script, ".", stdout, std.io.getStdErr().writer()) catch return 1;
|
||||
try bw.flush(); // don't forget to flush!
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn usage(writer: anytype, this: []const u8) !void {
|
||||
try writer.print("usage: {s} <worker name> <script file>\n", .{this});
|
||||
}
|
||||
const Wasm = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
name: []const u8,
|
||||
data: []const u8,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.allocator.free(self.name);
|
||||
self.allocator.free(self.data);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn pushWorker(
|
||||
allocator: std.mem.Allocator,
|
||||
client: *std.http.Client,
|
||||
worker_name: []const u8,
|
||||
wasm_dir: []const u8,
|
||||
script: []const u8,
|
||||
writer: anytype,
|
||||
err_writer: anytype,
|
||||
) !void {
|
||||
var wasm = try loadWasm(allocator, script, wasm_dir);
|
||||
defer wasm.deinit();
|
||||
|
||||
var accountid = std.os.getenv("CLOUDFLARE_ACCOUNT_ID");
|
||||
const account_id_free = accountid == null;
|
||||
if (accountid == null) accountid = try getAccountId(allocator, client);
|
||||
defer if (account_id_free) allocator.free(accountid.?);
|
||||
|
||||
try writer.print("Using Cloudflare account: {s}\n", .{accountid.?});
|
||||
|
||||
// Determine if worker exists. This lets us know if we need to enable it later
|
||||
const worker_exists = try workerExists(allocator, client, accountid.?, worker_name);
|
||||
try writer.print(
|
||||
"{s}\n",
|
||||
.{if (worker_exists) "Worker exists, will not re-enable" else "Worker is new. Will enable after code update"},
|
||||
);
|
||||
|
||||
var worker = Worker{
|
||||
.account_id = accountid.?,
|
||||
.name = worker_name,
|
||||
.wasm = wasm,
|
||||
.main_module = script,
|
||||
};
|
||||
putNewWorker(allocator, client, &worker) catch |err| {
|
||||
if (worker.errors == null) return err;
|
||||
try err_writer.print("{d} errors returned from CloudFlare:\n\n", .{worker.errors.?.len});
|
||||
for (worker.errors.?) |cf_err| {
|
||||
try err_writer.print("{s}\n", .{cf_err});
|
||||
allocator.free(cf_err);
|
||||
}
|
||||
return error.CloudFlareErrorResponse;
|
||||
};
|
||||
const subdomain = try getSubdomain(allocator, client, accountid.?);
|
||||
defer allocator.free(subdomain);
|
||||
try writer.print("Worker available at: https://{s}.{s}.workers.dev/\n", .{ worker_name, subdomain });
|
||||
if (!worker_exists)
|
||||
try enableWorker(allocator, client, accountid.?, worker_name);
|
||||
}
|
||||
|
||||
fn loadWasm(allocator: std.mem.Allocator, script: []const u8, wasm_dir: []const u8) !Wasm {
|
||||
// Looking for a string like this: import demoWasm from "demo.wasm"
|
||||
// JavaScript may or may not have ; characters. We're not doing
|
||||
// a full JS parsing here, so this may not be the most robust
|
||||
|
||||
var inx: usize = 0;
|
||||
|
||||
var name: ?[]const u8 = null;
|
||||
while (true) {
|
||||
inx = std.mem.indexOf(u8, script[inx..], "import ") orelse if (inx == 0) return error.NoImportFound else break;
|
||||
inx += "import ".len;
|
||||
|
||||
// oh god, we're not doing this: https://262.ecma-international.org/5.1/#sec-7.5
|
||||
// advance to next token - we don't care what the name is
|
||||
while (inx < script.len and script[inx] != ' ') inx += 1;
|
||||
// continue past space(s)
|
||||
while (inx < script.len and script[inx] == ' ') inx += 1;
|
||||
// We expect "from " to be next
|
||||
if (!std.mem.startsWith(u8, script[inx..], "from ")) continue;
|
||||
inx += "from ".len;
|
||||
// continue past space(s)
|
||||
while (inx < script.len and script[inx] == ' ') inx += 1;
|
||||
// We now expect the name of our file...
|
||||
if (script[inx] != '"' and script[inx] != '\'') continue; // we're not where we think we are
|
||||
const quote = script[inx]; // there are two possibilities here
|
||||
// we don't need to advance inx any more, as we're on the name, and if
|
||||
// we loop, that's ok
|
||||
inx += 1; // move off the quote onto the name
|
||||
const end_quote_inx = std.mem.indexOfScalar(u8, script[inx..], quote);
|
||||
if (end_quote_inx == null) continue;
|
||||
const candidate_name = script[inx .. inx + end_quote_inx.?];
|
||||
if (std.mem.endsWith(u8, candidate_name, ".wasm")) {
|
||||
if (name != null) // we are importing two wasm files, and we are now lost
|
||||
return error.MultipleWasmImportsUnsupported;
|
||||
name = candidate_name;
|
||||
}
|
||||
}
|
||||
if (name == null) return error.NoWasmImportFound;
|
||||
|
||||
const nm = try allocator.dupe(u8, name.?);
|
||||
errdefer allocator.free(nm);
|
||||
const path = try std.fs.path.join(allocator, &[_][]const u8{ wasm_dir, nm });
|
||||
defer allocator.free(path);
|
||||
const data = try std.fs.cwd().readFileAlloc(allocator, path, std.math.maxInt(usize));
|
||||
return Wasm{
|
||||
.allocator = allocator,
|
||||
.name = nm,
|
||||
.data = data,
|
||||
};
|
||||
}
|
||||
|
||||
fn getAccountId(allocator: std.mem.Allocator, client: *std.http.Client) ![:0]const u8 {
|
||||
const url = cf_api_base ++ "/accounts/";
|
||||
var headers = std.http.Headers.init(allocator);
|
||||
defer headers.deinit();
|
||||
try addAuthHeaders(&headers);
|
||||
var req = try client.request(.GET, try std.Uri.parse(url), headers, .{});
|
||||
defer req.deinit();
|
||||
try req.start();
|
||||
try req.wait();
|
||||
if (req.response.status != .ok) {
|
||||
std.debug.print("Status is {}\n", .{req.response.status});
|
||||
return error.RequestFailed;
|
||||
}
|
||||
var json_reader = std.json.reader(allocator, req.reader());
|
||||
defer json_reader.deinit();
|
||||
var body = try std.json.parseFromTokenSource(std.json.Value, allocator, &json_reader, .{});
|
||||
defer body.deinit();
|
||||
const arr = body.value.object.get("result").?.array.items;
|
||||
if (arr.len == 0) return error.NoAccounts;
|
||||
if (arr.len > 1) return error.TooManyAccounts;
|
||||
return try allocator.dupeZ(u8, arr[0].object.get("id").?.string);
|
||||
}
|
||||
|
||||
fn enableWorker(allocator: std.mem.Allocator, client: *std.http.Client, account_id: []const u8, name: []const u8) !void {
|
||||
const enable_script = cf_api_base ++ "/accounts/{s}/workers/scripts/{s}/subdomain";
|
||||
const url = try std.fmt.allocPrint(allocator, enable_script, .{ account_id, name });
|
||||
defer allocator.free(url);
|
||||
var headers = std.http.Headers.init(allocator);
|
||||
defer headers.deinit();
|
||||
try addAuthHeaders(&headers);
|
||||
try headers.append("Content-Type", "application/json; charset=UTF-8");
|
||||
var req = try client.request(.POST, try std.Uri.parse(url), headers, .{});
|
||||
defer req.deinit();
|
||||
|
||||
const request_payload =
|
||||
\\{ "enabled": true }
|
||||
;
|
||||
req.transfer_encoding = .{ .content_length = @as(u64, request_payload.len) };
|
||||
try req.start();
|
||||
try req.writeAll(request_payload);
|
||||
try req.finish();
|
||||
try req.wait();
|
||||
if (req.response.status != .ok) {
|
||||
std.debug.print("Status is {}\n", .{req.response.status});
|
||||
return error.RequestFailed;
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the subdomain for a worker. Caller owns memory
|
||||
fn getSubdomain(allocator: std.mem.Allocator, client: *std.http.Client, account_id: []const u8) ![]const u8 {
|
||||
const get_subdomain = cf_api_base ++ "/accounts/{s}/workers/subdomain";
|
||||
const url = try std.fmt.allocPrint(allocator, get_subdomain, .{account_id});
|
||||
defer allocator.free(url);
|
||||
|
||||
var headers = std.http.Headers.init(allocator);
|
||||
defer headers.deinit();
|
||||
try addAuthHeaders(&headers);
|
||||
var req = try client.request(.GET, try std.Uri.parse(url), headers, .{});
|
||||
defer req.deinit();
|
||||
try req.start();
|
||||
try req.wait();
|
||||
if (req.response.status != .ok) return error.RequestNotOk;
|
||||
var json_reader = std.json.reader(allocator, req.reader());
|
||||
defer json_reader.deinit();
|
||||
var body = try std.json.parseFromTokenSource(std.json.Value, allocator, &json_reader, .{});
|
||||
defer body.deinit();
|
||||
return try allocator.dupe(u8, body.value.object.get("result").?.object.get("subdomain").?.string);
|
||||
}
|
||||
|
||||
const Worker = struct {
|
||||
account_id: []const u8,
|
||||
name: []const u8,
|
||||
main_module: []const u8,
|
||||
wasm: Wasm,
|
||||
errors: ?[][]const u8 = null,
|
||||
};
|
||||
fn putNewWorker(allocator: std.mem.Allocator, client: *std.http.Client, worker: *Worker) !void {
|
||||
const put_script = cf_api_base ++ "/accounts/{s}/workers/scripts/{s}?include_subdomain_availability=true&excludeScript=true";
|
||||
const url = try std.fmt.allocPrint(allocator, put_script, .{ worker.account_id, worker.name });
|
||||
defer allocator.free(url);
|
||||
const memfs = @embedFile("dist/memfs.wasm");
|
||||
const outer_script_shell = @embedFile("script_harness.js");
|
||||
const script = try std.fmt.allocPrint(allocator, "{s}{s}", .{ outer_script_shell, worker.main_module });
|
||||
defer allocator.free(script);
|
||||
const deploy_request =
|
||||
"------formdata-undici-032998177938\r\n" ++
|
||||
"Content-Disposition: form-data; name=\"metadata\"\r\n\r\n" ++
|
||||
"{{\"main_module\":\"index.js\",\"bindings\":[],\"compatibility_date\":\"2023-10-02\",\"compatibility_flags\":[]}}\r\n" ++
|
||||
"------formdata-undici-032998177938\r\n" ++
|
||||
"Content-Disposition: form-data; name=\"index.js\"; filename=\"index.js\"\r\n" ++
|
||||
"Content-Type: application/javascript+module\r\n" ++
|
||||
"\r\n" ++
|
||||
"{[script]s}\r\n" ++
|
||||
"------formdata-undici-032998177938\r\n" ++
|
||||
"Content-Disposition: form-data; name=\"./{[wasm_name]s}\"; filename=\"./{[wasm_name]s}\"\r\n" ++
|
||||
"Content-Type: application/wasm\r\n" ++
|
||||
"\r\n" ++
|
||||
"{[wasm]s}\r\n" ++
|
||||
"------formdata-undici-032998177938\r\n" ++
|
||||
"Content-Disposition: form-data; name=\"./c5f1acc97ad09df861eff9ef567c2186d4e38de3-memfs.wasm\"; filename=\"./c5f1acc97ad09df861eff9ef567c2186d4e38de3-memfs.wasm\"\r\n" ++
|
||||
"Content-Type: application/wasm\r\n" ++
|
||||
"\r\n" ++
|
||||
"{[memfs]s}\r\n" ++
|
||||
"------formdata-undici-032998177938--";
|
||||
|
||||
var headers = std.http.Headers.init(allocator);
|
||||
defer headers.deinit();
|
||||
try addAuthHeaders(&headers);
|
||||
// TODO: fix this
|
||||
try headers.append("Content-Type", "multipart/form-data; boundary=----formdata-undici-032998177938");
|
||||
const request_payload = try std.fmt.allocPrint(allocator, deploy_request, .{
|
||||
.script = script,
|
||||
.wasm_name = worker.wasm.name,
|
||||
.wasm = worker.wasm.data,
|
||||
.memfs = memfs,
|
||||
});
|
||||
defer allocator.free(request_payload);
|
||||
// Get content length. For some reason it's forcing a chunked transfer type without this.
|
||||
// That's not entirely a bad idea, but for now I want to match what I see
|
||||
// coming through wrangler
|
||||
const cl = try std.fmt.allocPrint(allocator, "{d}", .{request_payload.len});
|
||||
defer allocator.free(cl);
|
||||
try headers.append("Content-Length", cl);
|
||||
var req = try client.request(.PUT, try std.Uri.parse(url), headers, .{});
|
||||
defer req.deinit();
|
||||
|
||||
req.transfer_encoding = .{ .content_length = @as(u64, request_payload.len) };
|
||||
try req.start();
|
||||
|
||||
// Workaround for https://github.com/ziglang/zig/issues/15626
|
||||
const max_bytes: usize = 1 << 14;
|
||||
var inx: usize = 0;
|
||||
while (request_payload.len > inx) {
|
||||
try req.writeAll(request_payload[inx..@min(request_payload.len, inx + max_bytes)]);
|
||||
inx += max_bytes;
|
||||
}
|
||||
try req.finish();
|
||||
try req.wait();
|
||||
// std.debug.print("Status is {}\n", .{req.response.status});
|
||||
// std.debug.print("Url is {s}\n", .{url});
|
||||
if (req.response.status != .ok) {
|
||||
std.debug.print("Status is {}\n", .{req.response.status});
|
||||
if (req.response.status == .bad_request)
|
||||
worker.errors = getErrors(allocator, &req) catch null;
|
||||
return error.RequestFailed;
|
||||
}
|
||||
}
|
||||
|
||||
fn getErrors(allocator: std.mem.Allocator, req: *std.http.Client.Request) !?[][]const u8 {
|
||||
var json_reader = std.json.reader(allocator, req.reader());
|
||||
defer json_reader.deinit();
|
||||
var body = try std.json.parseFromTokenSource(std.json.Value, allocator, &json_reader, .{});
|
||||
defer body.deinit();
|
||||
const arr = body.value.object.get("errors").?.array.items;
|
||||
if (arr.len == 0) return null;
|
||||
var error_list = try std.ArrayList([]const u8).initCapacity(allocator, arr.len);
|
||||
defer error_list.deinit();
|
||||
for (arr) |item| {
|
||||
error_list.appendAssumeCapacity(item.object.get("message").?.string);
|
||||
}
|
||||
return try error_list.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn workerExists(allocator: std.mem.Allocator, client: *std.http.Client, account_id: []const u8, name: []const u8) !bool {
|
||||
const existence_check = cf_api_base ++ "/accounts/{s}/workers/services/{s}";
|
||||
const url = try std.fmt.allocPrint(allocator, existence_check, .{ account_id, name });
|
||||
defer allocator.free(url);
|
||||
var headers = std.http.Headers.init(allocator);
|
||||
defer headers.deinit();
|
||||
try addAuthHeaders(&headers);
|
||||
var req = try client.request(.GET, try std.Uri.parse(url), headers, .{});
|
||||
defer req.deinit();
|
||||
try req.start();
|
||||
try req.wait();
|
||||
// std.debug.print("Status is {}\n", .{req.response.status});
|
||||
// std.debug.print("Url is {s}\n", .{url});
|
||||
return req.response.status == .ok;
|
||||
}
|
||||
|
||||
threadlocal var auth_buf: [1024]u8 = undefined;
|
||||
|
||||
fn addAuthHeaders(headers: *std.http.Headers) !void {
|
||||
if (!initialized) {
|
||||
x_auth_email = std.os.getenv("CLOUDFLARE_EMAIL");
|
||||
x_auth_key = std.os.getenv("CLOUDFLARE_API_KEY");
|
||||
x_auth_token = std.os.getenv("CLOUDFLARE_API_TOKEN");
|
||||
initialized = true;
|
||||
}
|
||||
if (x_auth_token) |tok| {
|
||||
var auth = try std.fmt.bufPrint(auth_buf[0..], "Bearer {s}", .{tok});
|
||||
try headers.append("Authorization", auth);
|
||||
return;
|
||||
}
|
||||
if (x_auth_email) |email| {
|
||||
if (x_auth_key == null)
|
||||
return error.MissingCloudflareApiKeyEnvironmentVariable;
|
||||
try headers.append("X-Auth-Email", email);
|
||||
try headers.append("X-Auth-Key", x_auth_key.?);
|
||||
}
|
||||
return error.NoCloudflareAuthenticationEnvironmentVariablesSet;
|
||||
}
|
|
@ -31,7 +31,7 @@ pub fn run(allocator: ?std.mem.Allocator, event_handler: interface.HandlerFn) !u
|
|||
defer response.deinit();
|
||||
var headers = try findHeaders(aa);
|
||||
defer headers.deinit();
|
||||
response.request.headers = headers.http_headers.*;
|
||||
response.request.headers = headers.http_headers;
|
||||
response.request.headers_owned = false;
|
||||
response.request.target = try findTarget(aa);
|
||||
response.request.method = try findMethod(aa);
|
||||
|
@ -107,7 +107,7 @@ fn findTarget(allocator: std.mem.Allocator) ![]const u8 {
|
|||
return error.CommandLineError;
|
||||
if (is_target_option)
|
||||
return arg;
|
||||
return (try std.Uri.parse(arg)).path;
|
||||
return (try std.Uri.parse(arg)).path.raw;
|
||||
}
|
||||
if (std.mem.startsWith(u8, arg, "-" ++ target_option.short) or
|
||||
std.mem.startsWith(u8, arg, "--" ++ target_option.long))
|
||||
|
@ -126,7 +126,7 @@ fn findTarget(allocator: std.mem.Allocator) ![]const u8 {
|
|||
var split = std.mem.splitSequence(u8, arg, "=");
|
||||
_ = split.next();
|
||||
const rest = split.rest();
|
||||
if (split.next()) |_| return (try std.Uri.parse(rest)).path; // found it
|
||||
if (split.next()) |_| return (try std.Uri.parse(rest)).path.raw; // found it
|
||||
is_url_option = true;
|
||||
}
|
||||
}
|
||||
|
@ -134,13 +134,13 @@ fn findTarget(allocator: std.mem.Allocator) ![]const u8 {
|
|||
}
|
||||
|
||||
pub const Headers = struct {
|
||||
http_headers: *std.http.Headers,
|
||||
http_headers: []std.http.Header,
|
||||
owned: bool,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, headers: *std.http.Headers, owned: bool) Self {
|
||||
pub fn init(allocator: std.mem.Allocator, headers: []std.http.Header, owned: bool) Self {
|
||||
return .{
|
||||
.http_headers = headers,
|
||||
.owned = owned,
|
||||
|
@ -150,8 +150,7 @@ pub const Headers = struct {
|
|||
|
||||
pub fn deinit(self: *Self) void {
|
||||
if (self.owned) {
|
||||
self.http_headers.deinit();
|
||||
self.allocator.destroy(self.http_headers);
|
||||
self.allocator.free(self.http_headers);
|
||||
self.http_headers = undefined;
|
||||
}
|
||||
}
|
||||
|
@ -160,13 +159,8 @@ pub const Headers = struct {
|
|||
// Get headers from request. Headers will be gathered from the command line
|
||||
// and include all environment variables
|
||||
pub fn findHeaders(allocator: std.mem.Allocator) !Headers {
|
||||
var headers = try allocator.create(std.http.Headers);
|
||||
errdefer allocator.destroy(headers);
|
||||
headers.allocator = allocator;
|
||||
headers.list = .{};
|
||||
headers.index = .{};
|
||||
headers.owned = true;
|
||||
errdefer headers.deinit();
|
||||
var headers = std.ArrayList(std.http.Header).init(allocator);
|
||||
defer headers.deinit();
|
||||
|
||||
// without context, we have environment variables
|
||||
// possibly event data (API Gateway does this if so configured),
|
||||
|
@ -185,7 +179,7 @@ pub fn findHeaders(allocator: std.mem.Allocator) !Headers {
|
|||
is_header_option = false;
|
||||
var split = std.mem.splitSequence(u8, arg, "=");
|
||||
const name = split.next().?;
|
||||
try headers.append(name, split.rest());
|
||||
try headers.append(.{ .name = name, .value = split.rest() });
|
||||
}
|
||||
if (std.mem.startsWith(u8, arg, "-" ++ header_option.short) or
|
||||
std.mem.startsWith(u8, arg, "--" ++ header_option.long))
|
||||
|
@ -196,21 +190,30 @@ pub fn findHeaders(allocator: std.mem.Allocator) !Headers {
|
|||
is_header_option = true;
|
||||
}
|
||||
}
|
||||
if (is_test) return Headers.init(allocator, headers, true);
|
||||
if (is_test) return Headers.init(allocator, try headers.toOwnedSlice(), true);
|
||||
|
||||
// not found on command line. Let's check environment
|
||||
// TODO: Get this under test - fake an environment map with deterministic values
|
||||
var map = try std.process.getEnvMap(allocator);
|
||||
defer map.deinit();
|
||||
var it = map.iterator();
|
||||
while (it.next()) |kvp| {
|
||||
// Do not allow environment variables to interfere with command line
|
||||
if (headers.getFirstValue(kvp.key_ptr.*) == null)
|
||||
try headers.append(
|
||||
kvp.key_ptr.*,
|
||||
kvp.value_ptr.*,
|
||||
);
|
||||
var found = false;
|
||||
const key = kvp.key_ptr.*;
|
||||
for (headers.items) |h| {
|
||||
if (std.ascii.eqlIgnoreCase(h.name, key)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
try headers.append(.{
|
||||
.name = try allocator.dupe(u8, key),
|
||||
.value = try allocator.dupe(u8, kvp.value_ptr.*),
|
||||
});
|
||||
}
|
||||
return Headers.init(allocator, headers, true);
|
||||
return Headers.init(allocator, try headers.toOwnedSlice(), true);
|
||||
}
|
||||
|
||||
test {
|
||||
|
@ -233,13 +236,13 @@ test "can get headers" {
|
|||
try test_args.append(allocator, "X-Foo=Bar");
|
||||
var headers = try findHeaders(allocator);
|
||||
defer headers.deinit();
|
||||
try std.testing.expectEqual(@as(usize, 1), headers.http_headers.list.items.len);
|
||||
try std.testing.expectEqual(@as(usize, 1), headers.http_headers.len);
|
||||
}
|
||||
|
||||
fn testHandler(allocator: std.mem.Allocator, event_data: []const u8, context: interface.Context) ![]const u8 {
|
||||
try context.headers.append("X-custom-foo", "bar");
|
||||
context.headers = &.{.{ .name = "X-custom-foo", .value = "bar" }};
|
||||
try context.writeAll(event_data);
|
||||
return std.fmt.allocPrint(allocator, "{d}", .{context.request.headers.list.items.len});
|
||||
return std.fmt.allocPrint(allocator, "{d}", .{context.request.headers.len});
|
||||
}
|
||||
|
||||
var test_args: std.SegmentedList([]const u8, 8) = undefined;
|
||||
|
@ -249,7 +252,7 @@ var test_output: std.ArrayList(u8) = undefined;
|
|||
test "handle_request" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var aa = arena.allocator();
|
||||
const aa = arena.allocator();
|
||||
test_args = .{};
|
||||
defer test_args.deinit(aa);
|
||||
try test_args.append(aa, "mainexe");
|
||||
|
|
BIN
src/dist/memfs.wasm
vendored
BIN
src/dist/memfs.wasm
vendored
Binary file not shown.
|
@ -27,7 +27,7 @@ const Application = if (@import("builtin").is_test) @This() else @import("flexil
|
|||
// }
|
||||
//
|
||||
comptime {
|
||||
@export(interface.zigInit, .{ .name = "zigInit", .linkage = .Strong });
|
||||
@export(interface.zigInit, .{ .name = "zigInit", .linkage = .strong });
|
||||
}
|
||||
|
||||
/// handle_request will be called on a single request, but due to the preservation
|
||||
|
@ -73,17 +73,17 @@ fn handleRequest(allocator: std.mem.Allocator, response: *interface.ZigResponse)
|
|||
ul_response.request.headers = response.request.headers;
|
||||
ul_response.request.method = std.meta.stringToEnum(std.http.Method, response.request.method) orelse std.http.Method.GET;
|
||||
const builtin = @import("builtin");
|
||||
const supports_getrusage = builtin.os.tag != .windows and @hasDecl(std.os.system, "rusage"); // Is Windows it?
|
||||
var rss: if (supports_getrusage) std.os.rusage else void = undefined;
|
||||
const supports_getrusage = builtin.os.tag != .windows and @hasDecl(std.posix.system, "rusage"); // Is Windows it?
|
||||
var rss: if (supports_getrusage) std.posix.rusage else void = undefined;
|
||||
if (supports_getrusage and builtin.mode == .Debug)
|
||||
rss = std.os.getrusage(std.os.rusage.SELF);
|
||||
rss = std.posix.getrusage(std.posix.rusage.SELF);
|
||||
const response_content = try handler.?(
|
||||
allocator,
|
||||
response.request.content,
|
||||
&ul_response,
|
||||
);
|
||||
if (supports_getrusage and builtin.mode == .Debug) { // and debug mode) {
|
||||
const rusage = std.os.getrusage(std.os.rusage.SELF);
|
||||
const rusage = std.posix.getrusage(std.posix.rusage.SELF);
|
||||
log.debug(
|
||||
"Request complete, max RSS of process: {d}M. Incremental: {d}K, User: {d}μs, System: {d}μs",
|
||||
.{
|
||||
|
@ -96,10 +96,8 @@ fn handleRequest(allocator: std.mem.Allocator, response: *interface.ZigResponse)
|
|||
},
|
||||
);
|
||||
}
|
||||
// Copy any headers
|
||||
for (ul_response.headers.list.items) |entry| {
|
||||
try response.headers.append(entry.name, entry.value);
|
||||
}
|
||||
response.headers = try allocator.dupe(std.http.Header, ul_response.headers);
|
||||
// response.headers = ul_response.headers;
|
||||
// Anything manually written goes first
|
||||
try response_writer.writeAll(ul_response.body.items);
|
||||
// Now we right the official body (response from handler)
|
||||
|
@ -130,10 +128,10 @@ pub fn main() !u8 {
|
|||
register(testHandler);
|
||||
return 0;
|
||||
}
|
||||
fn testHandler(allocator: std.mem.Allocator, event_data: []const u8, context: @import("universal_lambda_interface").Context) ![]const u8 {
|
||||
try context.headers.append("X-custom-foo", "bar");
|
||||
fn testHandler(allocator: std.mem.Allocator, event_data: []const u8, context: universal_lambda_interface.Context) ![]const u8 {
|
||||
context.headers = &.{.{ .name = "X-custom-foo", .value = "bar" }};
|
||||
try context.writeAll(event_data);
|
||||
return std.fmt.allocPrint(allocator, "{d}", .{context.request.headers.list.items.len});
|
||||
return std.fmt.allocPrint(allocator, "{d}", .{context.request.headers.len});
|
||||
}
|
||||
// Need to figure out how tests would work
|
||||
test "handle_request" {
|
||||
|
@ -141,7 +139,7 @@ test "handle_request" {
|
|||
defer arena.deinit();
|
||||
var aa = arena.allocator();
|
||||
interface.zigInit(&aa);
|
||||
var headers: []interface.Header = @constCast(&[_]interface.Header{.{
|
||||
const headers: []interface.Header = @constCast(&[_]interface.Header{.{
|
||||
.name_ptr = @ptrCast(@constCast("GET".ptr)),
|
||||
.name_len = 3,
|
||||
.value_ptr = @ptrCast(@constCast("GET".ptr)),
|
||||
|
|
|
@ -3,36 +3,43 @@ const builtin = @import("builtin");
|
|||
|
||||
/// flexilib will create a dynamic library for use with flexilib.
|
||||
/// Flexilib will need to get the exe compiled as a library
|
||||
pub fn configureBuild(b: *std.build.Builder, cs: *std.Build.Step.Compile, build_root_src: []const u8) !void {
|
||||
pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile, universal_lambda_zig_dep: *std.Build.Dependency) !void {
|
||||
const package_step = b.step("flexilib", "Create a flexilib dynamic library");
|
||||
|
||||
const lib = b.addSharedLibrary(.{
|
||||
.name = cs.name,
|
||||
.root_source_file = .{ .path = b.pathJoin(&[_][]const u8{ build_root_src, "flexilib.zig" }) },
|
||||
.target = cs.target,
|
||||
.optimize = cs.optimize,
|
||||
.root_source_file = .{
|
||||
.path = b.pathJoin(&[_][]const u8{
|
||||
// root path comes from our dependency, which should be us,
|
||||
// and if it's not, we'll just blow up here but it's not our fault ;-)
|
||||
universal_lambda_zig_dep.builder.build_root.path.?,
|
||||
"src",
|
||||
"flexilib.zig",
|
||||
}),
|
||||
},
|
||||
.target = cs.root_module.resolved_target.?,
|
||||
.optimize = cs.root_module.optimize.?,
|
||||
});
|
||||
|
||||
// We will not free this, as the rest of the build system will use it.
|
||||
// This should be ok because our allocator is, I believe, an arena
|
||||
var module_dependencies = try b.allocator.alloc(std.Build.ModuleDependency, cs.modules.count());
|
||||
var iterator = cs.modules.iterator();
|
||||
|
||||
var i: usize = 0;
|
||||
while (iterator.next()) |entry| : (i += 1) {
|
||||
module_dependencies[i] = .{
|
||||
.name = entry.key_ptr.*,
|
||||
.module = entry.value_ptr.*,
|
||||
};
|
||||
lib.addModule(entry.key_ptr.*, entry.value_ptr.*);
|
||||
}
|
||||
|
||||
// Add the downstream root source file back into the build as a module
|
||||
// that our new root source file can import
|
||||
lib.addAnonymousModule("flexilib_handler", .{
|
||||
const flexilib_handler = b.createModule(.{
|
||||
// Source file can be anywhere on disk, does not need to be a subdirectory
|
||||
.source_file = cs.root_src.?,
|
||||
.dependencies = module_dependencies,
|
||||
.root_source_file = cs.root_module.root_source_file,
|
||||
});
|
||||
|
||||
lib.root_module.addImport("flexilib_handler", flexilib_handler);
|
||||
|
||||
// Now we need to get our imports added. Rather than reinvent the wheel, we'll
|
||||
// utilize our addImports function, but tell it to work on our library
|
||||
@import("universal_lambda_build.zig").addImports(b, lib, universal_lambda_zig_dep);
|
||||
|
||||
// flexilib_handler module needs imports to work...we are not in control
|
||||
// of this file, so it could expect anything that's already imported. So
|
||||
// we'll walk through the import table and simply add all the imports back in
|
||||
var iterator = lib.root_module.import_table.iterator();
|
||||
while (iterator.next()) |entry|
|
||||
flexilib_handler.addImport(entry.key_ptr.*, entry.value_ptr.*);
|
||||
|
||||
package_step.dependOn(&b.addInstallArtifact(lib, .{}).step);
|
||||
}
|
||||
|
|
31
src/index.js
31
src/index.js
|
@ -1,31 +0,0 @@
|
|||
import customWasm from "custom.wasm";
|
||||
export default {
|
||||
async fetch(request, _env2, ctx) {
|
||||
const stdout = new TransformStream();
|
||||
console.log(request);
|
||||
console.log(_env2);
|
||||
console.log(ctx);
|
||||
let env = {};
|
||||
request.headers.forEach((value, key) => {
|
||||
env[key] = value;
|
||||
});
|
||||
const wasi = new WASI({
|
||||
args: [
|
||||
"./custom.wasm",
|
||||
// In a CLI, the first arg is the name of the exe
|
||||
"--url=" + request.url,
|
||||
// this contains the target but is the full url, so we will use a different arg for this
|
||||
"--method=" + request.method,
|
||||
'-request="' + JSON.stringify(request) + '"'
|
||||
],
|
||||
env,
|
||||
stdin: request.body,
|
||||
stdout: stdout.writable
|
||||
});
|
||||
const instance = new WebAssembly.Instance(customWasm, {
|
||||
wasi_snapshot_preview1: wasi.wasiImport
|
||||
});
|
||||
ctx.waitUntil(wasi.start(instance));
|
||||
return new Response(stdout.readable);
|
||||
}
|
||||
};
|
|
@ -4,7 +4,7 @@ pub const HandlerFn = *const fn (std.mem.Allocator, []const u8, Context) anyerro
|
|||
|
||||
pub const Response = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
headers: std.http.Headers,
|
||||
headers: []const std.http.Header,
|
||||
headers_owned: bool = true,
|
||||
status: std.http.Status = .ok,
|
||||
reason: ?[]const u8 = null,
|
||||
|
@ -15,7 +15,7 @@ pub const Response = struct {
|
|||
/// that here
|
||||
request: struct {
|
||||
target: []const u8 = "/",
|
||||
headers: std.http.Headers,
|
||||
headers: []const std.http.Header,
|
||||
headers_owned: bool = true,
|
||||
method: std.http.Method = .GET,
|
||||
},
|
||||
|
@ -37,9 +37,9 @@ pub const Response = struct {
|
|||
pub fn init(allocator: std.mem.Allocator) Response {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.headers = .{ .allocator = allocator },
|
||||
.headers = &.{},
|
||||
.request = .{
|
||||
.headers = .{ .allocator = allocator },
|
||||
.headers = &.{},
|
||||
},
|
||||
.body = std.ArrayList(u8).init(allocator),
|
||||
};
|
||||
|
@ -61,8 +61,8 @@ pub const Response = struct {
|
|||
}
|
||||
pub fn deinit(res: *Response) void {
|
||||
res.body.deinit();
|
||||
if (res.headers_owned) res.headers.deinit();
|
||||
if (res.request.headers_owned) res.request.headers.deinit();
|
||||
if (res.headers_owned) res.allocator.free(res.headers);
|
||||
if (res.request.headers_owned) res.allocator.free(res.request.headers);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
516
src/lambda.zig
516
src/lambda.zig
|
@ -1,516 +0,0 @@
|
|||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const universal_lambda_interface = @import("universal_lambda_interface");
|
||||
const HandlerFn = universal_lambda_interface.HandlerFn;
|
||||
const Context = universal_lambda_interface.Context;
|
||||
const UniversalLambdaResponse = universal_lambda_interface.Response;
|
||||
|
||||
const log = std.log.scoped(.lambda);
|
||||
|
||||
var empty_headers: std.http.Headers = undefined;
|
||||
var client: ?std.http.Client = null;
|
||||
|
||||
const prefix = "http://";
|
||||
const postfix = "/2018-06-01/runtime/invocation";
|
||||
|
||||
pub fn deinit() void {
|
||||
if (client) |*c| c.deinit();
|
||||
client = null;
|
||||
}
|
||||
/// Starts the lambda framework. Handler will be called when an event is processing
|
||||
/// If an allocator is not provided, an approrpriate allocator will be selected and used
|
||||
/// This function is intended to loop infinitely. If not used in this manner,
|
||||
/// make sure to call the deinit() function
|
||||
pub fn run(allocator: ?std.mem.Allocator, event_handler: HandlerFn) !u8 { // TODO: remove inferred error set?
|
||||
const lambda_runtime_uri = std.os.getenv("AWS_LAMBDA_RUNTIME_API") orelse test_lambda_runtime_uri.?;
|
||||
// TODO: If this is null, go into single use command line mode
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const alloc = allocator orelse gpa.allocator();
|
||||
|
||||
const url = try std.fmt.allocPrint(alloc, "{s}{s}{s}/next", .{ prefix, lambda_runtime_uri, postfix });
|
||||
defer alloc.free(url);
|
||||
const uri = try std.Uri.parse(url);
|
||||
|
||||
// TODO: Simply adding this line without even using the client is enough
|
||||
// to cause seg faults!?
|
||||
// client = client orelse .{ .allocator = alloc };
|
||||
// so we'll do this instead
|
||||
if (client != null) return error.MustDeInitBeforeCallingRunAgain;
|
||||
client = .{ .allocator = alloc };
|
||||
empty_headers = std.http.Headers.init(alloc);
|
||||
defer empty_headers.deinit();
|
||||
log.info("tid {d} (lambda): Bootstrap initializing with event url: {s}", .{ std.Thread.getCurrentId(), url });
|
||||
|
||||
while (lambda_remaining_requests == null or lambda_remaining_requests.? > 0) {
|
||||
if (lambda_remaining_requests) |*r| {
|
||||
// we're under test
|
||||
log.debug("lambda remaining requests: {d}", .{r.*});
|
||||
r.* -= 1;
|
||||
}
|
||||
var req_alloc = std.heap.ArenaAllocator.init(alloc);
|
||||
defer req_alloc.deinit();
|
||||
const req_allocator = req_alloc.allocator();
|
||||
|
||||
// Fundamentally we're doing 3 things:
|
||||
// 1. Get the next event from Lambda (event data and request id)
|
||||
// 2. Call our handler to get the response
|
||||
// 3. Post the response back to Lambda
|
||||
var ev = getEvent(req_allocator, uri) catch |err| {
|
||||
// Well, at this point all we can do is shout at the void
|
||||
log.err("Error fetching event details: {}", .{err});
|
||||
std.os.exit(1);
|
||||
// continue;
|
||||
};
|
||||
if (ev == null) continue; // this gets logged in getEvent, and without
|
||||
// a request id, we still can't do anything
|
||||
// reasonable to report back
|
||||
const event = ev.?;
|
||||
defer ev.?.deinit();
|
||||
// Lambda does not have context, just environment variables. API Gateway
|
||||
// might be configured to pass in lots of context, but this comes through
|
||||
// event data, not context. In this case, we lose:
|
||||
//
|
||||
// request headers
|
||||
// request method
|
||||
// request target
|
||||
var response = UniversalLambdaResponse.init(req_allocator);
|
||||
defer response.deinit();
|
||||
const body_writer = std.io.getStdOut();
|
||||
const event_response = event_handler(req_allocator, event.event_data, &response) catch |err| {
|
||||
body_writer.writeAll(response.body.items) catch unreachable;
|
||||
event.reportError(@errorReturnTrace(), err, lambda_runtime_uri) catch unreachable;
|
||||
continue;
|
||||
};
|
||||
// No error during handler. Write anything sent to body to stdout instead
|
||||
// I'm not totally sure this is the right behavior as it is a little inconsistent
|
||||
// (flexilib and console both write to the same io stream as the main output)
|
||||
body_writer.writeAll(response.body.items) catch unreachable;
|
||||
event.postResponse(lambda_runtime_uri, event_response) catch |err| {
|
||||
event.reportError(@errorReturnTrace(), err, lambda_runtime_uri) catch unreachable;
|
||||
continue;
|
||||
};
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Event = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
event_data: []u8,
|
||||
request_id: []u8,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, event_data: []u8, request_id: []u8) Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.event_data = event_data,
|
||||
.request_id = request_id,
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.allocator.free(self.event_data);
|
||||
self.allocator.free(self.request_id);
|
||||
}
|
||||
fn reportError(
|
||||
self: Self,
|
||||
return_trace: ?*std.builtin.StackTrace,
|
||||
err: anytype,
|
||||
lambda_runtime_uri: []const u8,
|
||||
) !void {
|
||||
// If we fail in this function, we're pretty hosed up
|
||||
if (return_trace) |rt|
|
||||
log.err("Caught error: {}. Return Trace: {any}", .{ err, rt })
|
||||
else
|
||||
log.err("Caught error: {}. No return trace available", .{err});
|
||||
const err_url = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"{s}{s}{s}/{s}/error",
|
||||
.{ prefix, lambda_runtime_uri, postfix, self.request_id },
|
||||
);
|
||||
defer self.allocator.free(err_url);
|
||||
const err_uri = try std.Uri.parse(err_url);
|
||||
const content =
|
||||
\\{{
|
||||
\\ "errorMessage": "{s}",
|
||||
\\ "errorType": "HandlerReturnedError",
|
||||
\\ "stackTrace": [ "{any}" ]
|
||||
\\}}
|
||||
;
|
||||
const content_fmt = if (return_trace) |rt|
|
||||
try std.fmt.allocPrint(self.allocator, content, .{ @errorName(err), rt })
|
||||
else
|
||||
try std.fmt.allocPrint(self.allocator, content, .{ @errorName(err), "no return trace available" });
|
||||
defer self.allocator.free(content_fmt);
|
||||
log.err("Posting to {s}: Data {s}", .{ err_url, content_fmt });
|
||||
|
||||
var err_headers = std.http.Headers.init(self.allocator);
|
||||
defer err_headers.deinit();
|
||||
err_headers.append(
|
||||
"Lambda-Runtime-Function-Error-Type",
|
||||
"HandlerReturned",
|
||||
) catch |append_err| {
|
||||
log.err("Error appending error header to post response for request id {s}: {}", .{ self.request_id, append_err });
|
||||
std.os.exit(1);
|
||||
};
|
||||
// TODO: There is something up with using a shared client in this way
|
||||
// so we're taking a perf hit in favor of stability. In a practical
|
||||
// sense, without making HTTPS connections (lambda environment is
|
||||
// non-ssl), this shouldn't be a big issue
|
||||
var cl = std.http.Client{ .allocator = self.allocator };
|
||||
defer cl.deinit();
|
||||
var req = try cl.request(.POST, err_uri, empty_headers, .{});
|
||||
// var req = try client.?.request(.POST, err_uri, empty_headers, .{});
|
||||
// defer req.deinit();
|
||||
req.transfer_encoding = .{ .content_length = content_fmt.len };
|
||||
req.start() catch |post_err| { // Well, at this point all we can do is shout at the void
|
||||
log.err("Error posting response (start) for request id {s}: {}", .{ self.request_id, post_err });
|
||||
std.os.exit(1);
|
||||
};
|
||||
try req.writeAll(content_fmt);
|
||||
try req.finish();
|
||||
req.wait() catch |post_err| { // Well, at this point all we can do is shout at the void
|
||||
log.err("Error posting response (wait) for request id {s}: {}", .{ self.request_id, post_err });
|
||||
std.os.exit(1);
|
||||
};
|
||||
// TODO: Determine why this post is not returning
|
||||
if (req.response.status != .ok) {
|
||||
// Documentation says something about "exit immediately". The
|
||||
// Lambda infrastrucutre restarts, so it's unclear if that's necessary.
|
||||
// It seems as though a continue should be fine, and slightly faster
|
||||
log.err("Get fail: {} {s}", .{
|
||||
@intFromEnum(req.response.status),
|
||||
req.response.status.phrase() orelse "",
|
||||
});
|
||||
std.os.exit(1);
|
||||
}
|
||||
log.err("Error reporting post complete", .{});
|
||||
}
|
||||
|
||||
fn postResponse(self: Self, lambda_runtime_uri: []const u8, event_response: []const u8) !void {
|
||||
const response_url = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"{s}{s}{s}/{s}/response",
|
||||
.{ prefix, lambda_runtime_uri, postfix, self.request_id },
|
||||
);
|
||||
defer self.allocator.free(response_url);
|
||||
const response_uri = try std.Uri.parse(response_url);
|
||||
var cl = std.http.Client{ .allocator = self.allocator };
|
||||
defer cl.deinit();
|
||||
var req = try cl.request(.POST, response_uri, empty_headers, .{});
|
||||
// var req = try client.?.request(.POST, response_uri, empty_headers, .{});
|
||||
defer req.deinit();
|
||||
// Lambda does different things, depending on the runtime. Go 1.x takes
|
||||
// any return value but escapes double quotes. Custom runtimes can
|
||||
// do whatever they want. node I believe wraps as a json object. We're
|
||||
// going to leave the return value up to the handler, and they can
|
||||
// use a seperate API for normalization so we're explicit.
|
||||
const response_content = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"{s}",
|
||||
.{event_response},
|
||||
);
|
||||
defer self.allocator.free(response_content);
|
||||
|
||||
req.transfer_encoding = .{ .content_length = response_content.len };
|
||||
try req.start();
|
||||
try req.writeAll(response_content);
|
||||
try req.finish();
|
||||
try req.wait();
|
||||
}
|
||||
};
|
||||
|
||||
fn getEvent(allocator: std.mem.Allocator, event_data_uri: std.Uri) !?Event {
|
||||
// TODO: There is something up with using a shared client in this way
|
||||
// so we're taking a perf hit in favor of stability. In a practical
|
||||
// sense, without making HTTPS connections (lambda environment is
|
||||
// non-ssl), this shouldn't be a big issue
|
||||
var cl = std.http.Client{ .allocator = allocator };
|
||||
defer cl.deinit();
|
||||
var req = try cl.request(.GET, event_data_uri, empty_headers, .{});
|
||||
// var req = try client.?.request(.GET, event_data_uri, empty_headers, .{});
|
||||
// defer req.deinit();
|
||||
|
||||
try req.start();
|
||||
try req.finish();
|
||||
// Lambda freezes the process at this line of code. During warm start,
|
||||
// the process will unfreeze and data will be sent in response to client.get
|
||||
try req.wait();
|
||||
if (req.response.status != .ok) {
|
||||
// Documentation says something about "exit immediately". The
|
||||
// Lambda infrastrucutre restarts, so it's unclear if that's necessary.
|
||||
// It seems as though a continue should be fine, and slightly faster
|
||||
// std.os.exit(1);
|
||||
log.err("Lambda server event response returned bad error code: {} {s}", .{
|
||||
@intFromEnum(req.response.status),
|
||||
req.response.status.phrase() orelse "",
|
||||
});
|
||||
return error.EventResponseNotOkResponse;
|
||||
}
|
||||
|
||||
var request_id: ?[]const u8 = null;
|
||||
var content_length: ?usize = null;
|
||||
for (req.response.headers.list.items) |h| {
|
||||
if (std.ascii.eqlIgnoreCase(h.name, "Lambda-Runtime-Aws-Request-Id"))
|
||||
request_id = h.value;
|
||||
if (std.ascii.eqlIgnoreCase(h.name, "Content-Length")) {
|
||||
content_length = std.fmt.parseUnsigned(usize, h.value, 10) catch null;
|
||||
if (content_length == null)
|
||||
log.warn("Error parsing content length value: '{s}'", .{h.value});
|
||||
}
|
||||
// TODO: XRay uses an environment variable to do its magic. It's our
|
||||
// responsibility to set this, but no zig-native setenv(3)/putenv(3)
|
||||
// exists. I would kind of rather not link in libc for this,
|
||||
// so we'll hold for now and think on this
|
||||
// if (std.mem.indexOf(u8, h.name.value, "Lambda-Runtime-Trace-Id")) |_|
|
||||
// std.process.
|
||||
// std.os.setenv("AWS_LAMBDA_RUNTIME_API");
|
||||
}
|
||||
if (request_id == null) {
|
||||
// We can't report back an issue because the runtime error reporting endpoint
|
||||
// uses request id in its path. So the best we can do is log the error and move
|
||||
// on here.
|
||||
log.err("Could not find request id: skipping request", .{});
|
||||
return null;
|
||||
}
|
||||
if (content_length == null) {
|
||||
// We can't report back an issue because the runtime error reporting endpoint
|
||||
// uses request id in its path. So the best we can do is log the error and move
|
||||
// on here.
|
||||
log.err("No content length provided for event data", .{});
|
||||
return null;
|
||||
}
|
||||
const req_id = request_id.?;
|
||||
log.debug("got lambda request with id {s}", .{req_id});
|
||||
|
||||
var response_data: []u8 =
|
||||
if (req.response.transfer_encoding) |_| // the only value here is "chunked"
|
||||
try req.reader().readAllAlloc(allocator, std.math.maxInt(usize))
|
||||
else blk: {
|
||||
// content length
|
||||
var tmp_data = try allocator.alloc(u8, content_length.?);
|
||||
errdefer allocator.free(tmp_data);
|
||||
_ = try req.readAll(tmp_data);
|
||||
break :blk tmp_data;
|
||||
};
|
||||
|
||||
return Event.init(
|
||||
allocator,
|
||||
response_data,
|
||||
try allocator.dupe(u8, req_id),
|
||||
);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// All code below this line is for testing
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
var server_port: ?u16 = null;
|
||||
var server_remaining_requests: usize = 0;
|
||||
var lambda_remaining_requests: ?usize = null;
|
||||
var server_response: []const u8 = "unset";
|
||||
var server_request_aka_lambda_response: []u8 = "";
|
||||
var test_lambda_runtime_uri: ?[]u8 = null;
|
||||
|
||||
var server_ready = false;
|
||||
/// This starts a test server. We're not testing the server itself,
|
||||
/// so the main tests will start this thing up and create an arena around the
|
||||
/// whole thing so we can just deallocate everything at once at the end,
|
||||
/// leaks be damned
|
||||
fn startServer(allocator: std.mem.Allocator) !std.Thread {
|
||||
return try std.Thread.spawn(
|
||||
.{},
|
||||
threadMain,
|
||||
.{allocator},
|
||||
);
|
||||
}
|
||||
|
||||
fn threadMain(allocator: std.mem.Allocator) !void {
|
||||
var server = std.http.Server.init(allocator, .{ .reuse_address = true });
|
||||
// defer server.deinit();
|
||||
|
||||
const address = try std.net.Address.parseIp("127.0.0.1", 0);
|
||||
try server.listen(address);
|
||||
server_port = server.socket.listen_address.in.getPort();
|
||||
|
||||
test_lambda_runtime_uri = try std.fmt.allocPrint(allocator, "127.0.0.1:{d}", .{server_port.?});
|
||||
log.debug("server listening at {s}", .{test_lambda_runtime_uri.?});
|
||||
defer server.deinit();
|
||||
defer test_lambda_runtime_uri = null;
|
||||
defer server_port = null;
|
||||
log.info("starting server thread, tid {d}", .{std.Thread.getCurrentId()});
|
||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||
defer arena.deinit();
|
||||
var aa = arena.allocator();
|
||||
// We're in control of all requests/responses, so this flag will tell us
|
||||
// when it's time to shut down
|
||||
while (server_remaining_requests > 0) {
|
||||
server_remaining_requests -= 1;
|
||||
// defer {
|
||||
// if (!arena.reset(.{ .retain_capacity = {} })) {
|
||||
// // reallocation failed, arena is degraded
|
||||
// log.warn("Arena reset failed and is degraded. Resetting arena", .{});
|
||||
// arena.deinit();
|
||||
// arena = std.heap.ArenaAllocator.init(allocator);
|
||||
// aa = arena.allocator();
|
||||
// }
|
||||
// }
|
||||
|
||||
processRequest(aa, &server) catch |e| {
|
||||
log.err("Unexpected error processing request: {any}", .{e});
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpStackTrace(trace.*);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn processRequest(allocator: std.mem.Allocator, server: *std.http.Server) !void {
|
||||
server_ready = true;
|
||||
errdefer server_ready = false;
|
||||
log.debug(
|
||||
"tid {d} (server): server waiting to accept. requests remaining: {d}",
|
||||
.{ std.Thread.getCurrentId(), server_remaining_requests + 1 },
|
||||
);
|
||||
var res = try server.accept(.{ .allocator = allocator });
|
||||
server_ready = false;
|
||||
defer res.deinit();
|
||||
defer _ = res.reset();
|
||||
try res.wait(); // wait for client to send a complete request head
|
||||
|
||||
const errstr = "Internal Server Error\n";
|
||||
var errbuf: [errstr.len]u8 = undefined;
|
||||
@memcpy(&errbuf, errstr);
|
||||
var response_bytes: []const u8 = errbuf[0..];
|
||||
|
||||
if (res.request.content_length) |l|
|
||||
server_request_aka_lambda_response = try res.reader().readAllAlloc(allocator, @as(usize, @intCast(l)));
|
||||
|
||||
log.debug(
|
||||
"tid {d} (server): {d} bytes read from request",
|
||||
.{ std.Thread.getCurrentId(), server_request_aka_lambda_response.len },
|
||||
);
|
||||
|
||||
// try response.headers.append("content-type", "text/plain");
|
||||
response_bytes = serve(allocator, &res) catch |e| brk: {
|
||||
res.status = .internal_server_error;
|
||||
// TODO: more about this particular request
|
||||
log.err("Unexpected error from executor processing request: {any}", .{e});
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpStackTrace(trace.*);
|
||||
}
|
||||
break :brk "Unexpected error generating request to lambda";
|
||||
};
|
||||
res.transfer_encoding = .{ .content_length = response_bytes.len };
|
||||
try res.do();
|
||||
_ = try res.writer().writeAll(response_bytes);
|
||||
try res.finish();
|
||||
log.debug(
|
||||
"tid {d} (server): sent response",
|
||||
.{std.Thread.getCurrentId()},
|
||||
);
|
||||
}
|
||||
|
||||
fn serve(allocator: std.mem.Allocator, res: *std.http.Server.Response) ![]const u8 {
|
||||
_ = allocator;
|
||||
// try res.headers.append("content-length", try std.fmt.allocPrint(allocator, "{d}", .{server_response.len}));
|
||||
try res.headers.append("Lambda-Runtime-Aws-Request-Id", "69");
|
||||
return server_response;
|
||||
}
|
||||
|
||||
fn handler(allocator: std.mem.Allocator, event_data: []const u8, context: Context) ![]const u8 {
|
||||
_ = allocator;
|
||||
_ = context;
|
||||
return event_data;
|
||||
}
|
||||
fn thread_run(allocator: ?std.mem.Allocator, event_handler: HandlerFn) !void {
|
||||
_ = try run(allocator, event_handler);
|
||||
}
|
||||
fn test_run(allocator: std.mem.Allocator, event_handler: HandlerFn) !std.Thread {
|
||||
return try std.Thread.spawn(
|
||||
.{},
|
||||
thread_run,
|
||||
.{ allocator, event_handler },
|
||||
);
|
||||
}
|
||||
|
||||
fn lambda_request(allocator: std.mem.Allocator, request: []const u8, request_count: usize) ![]u8 {
|
||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||
defer arena.deinit();
|
||||
var aa = arena.allocator();
|
||||
// Setup our server to run, and set the response for the server to the
|
||||
// request. There is a cognitive disconnect here between mental model and
|
||||
// physical model.
|
||||
//
|
||||
// Mental model:
|
||||
//
|
||||
// Lambda request -> λ -> Lambda response
|
||||
//
|
||||
// Physcial Model:
|
||||
//
|
||||
// 1. λ requests instructions from server
|
||||
// 2. server provides "Lambda request"
|
||||
// 3. λ posts response back to server
|
||||
//
|
||||
// So here we are setting up our server, then our lambda request loop,
|
||||
// but it all needs to be in seperate threads so we can control startup
|
||||
// and shut down. Both server and Lambda are set up to watch global variable
|
||||
// booleans to know when to shut down. This function is designed for a
|
||||
// single request/response pair only
|
||||
|
||||
lambda_remaining_requests = request_count;
|
||||
server_remaining_requests = lambda_remaining_requests.? * 2; // Lambda functions
|
||||
// fetch from the server,
|
||||
// then post back. Always
|
||||
// 2, no more, no less
|
||||
server_response = request; // set our instructions to lambda, which in our
|
||||
// physical model above, is the server response
|
||||
defer server_response = "unset"; // set it back so we don't get confused later
|
||||
// when subsequent tests fail
|
||||
const server_thread = try startServer(aa); // start the server, get it ready
|
||||
while (!server_ready)
|
||||
std.time.sleep(100);
|
||||
|
||||
log.debug("tid {d} (main): server reports ready", .{std.Thread.getCurrentId()});
|
||||
// we aren't testing the server,
|
||||
// so we'll use the arena allocator
|
||||
defer server_thread.join(); // we'll be shutting everything down before we exit
|
||||
|
||||
// Now we need to start the lambda framework, following a siimilar pattern
|
||||
const lambda_thread = try test_run(allocator, handler); // We want our function under test to report leaks
|
||||
lambda_thread.join();
|
||||
return try allocator.dupe(u8, server_request_aka_lambda_response);
|
||||
}
|
||||
|
||||
test "basic request" {
|
||||
// std.testing.log_level = .debug;
|
||||
const allocator = std.testing.allocator;
|
||||
const request =
|
||||
\\{"foo": "bar", "baz": "qux"}
|
||||
;
|
||||
|
||||
const expected_response =
|
||||
\\{"foo": "bar", "baz": "qux"}
|
||||
;
|
||||
const lambda_response = try lambda_request(allocator, request, 1);
|
||||
defer deinit();
|
||||
defer allocator.free(lambda_response);
|
||||
try std.testing.expectEqualStrings(expected_response, lambda_response);
|
||||
}
|
||||
|
||||
test "several requests do not fail" {
|
||||
// std.testing.log_level = .debug;
|
||||
const allocator = std.testing.allocator;
|
||||
const request =
|
||||
\\{"foo": "bar", "baz": "qux"}
|
||||
;
|
||||
|
||||
const expected_response =
|
||||
\\{"foo": "bar", "baz": "qux"}
|
||||
;
|
||||
const lambda_response = try lambda_request(allocator, request, 5);
|
||||
defer deinit();
|
||||
defer allocator.free(lambda_response);
|
||||
try std.testing.expectEqualStrings(expected_response, lambda_response);
|
||||
}
|
|
@ -1,198 +0,0 @@
|
|||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
fn fileExists(file_name: []const u8) bool {
|
||||
const file = std.fs.openFileAbsolute(file_name, .{}) catch return false;
|
||||
defer file.close();
|
||||
return true;
|
||||
}
|
||||
fn addArgs(allocator: std.mem.Allocator, original: []const u8, args: [][]const u8) ![]const u8 {
|
||||
var rc = original;
|
||||
for (args) |arg| {
|
||||
rc = try std.mem.concat(allocator, u8, &.{ rc, " ", arg });
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
/// lambdaBuildSteps will add four build steps to the build (if compiling
|
||||
/// the code on a Linux host):
|
||||
///
|
||||
/// * awslambda_package: Packages the function for deployment to Lambda
|
||||
/// (dependencies are the zip executable and a shell)
|
||||
/// * awslambda_iam: Gets an IAM role for the Lambda function, and creates it if it does not exist
|
||||
/// (dependencies are the AWS CLI, grep and a shell)
|
||||
/// * awslambda_deploy: Deploys the lambda function to a live AWS environment
|
||||
/// (dependencies are the AWS CLI, and a shell)
|
||||
/// * awslambda_run: Runs the lambda function in a live AWS environment
|
||||
/// (dependencies are the AWS CLI, and a shell)
|
||||
///
|
||||
/// awslambda_run depends on deploy
|
||||
/// awslambda_deploy depends on iam and package
|
||||
///
|
||||
/// iam and package do not have any dependencies
|
||||
pub fn configureBuild(b: *std.build.Builder, exe: *std.Build.Step.Compile, function_name: []const u8) !void {
|
||||
// The rest of this function is currently reliant on the use of Linux
|
||||
// system being used to build the lambda function
|
||||
//
|
||||
// It is likely that much of this will work on other Unix-like OSs, but
|
||||
// we will work this out later
|
||||
//
|
||||
// TODO: support other host OSs
|
||||
if (builtin.os.tag != .linux) return;
|
||||
|
||||
// Package step
|
||||
const package_step = b.step("awslambda_package", "Package the function");
|
||||
const function_zip = b.getInstallPath(.bin, "function.zip");
|
||||
|
||||
// TODO: Avoid use of system-installed zip, maybe using something like
|
||||
// https://github.com/hdorio/hwzip.zig/blob/master/src/hwzip.zig
|
||||
const zip = if (std.mem.eql(u8, "bootstrap", exe.out_filename))
|
||||
try std.fmt.allocPrint(b.allocator,
|
||||
\\zip -qj9 {s} {s}
|
||||
, .{
|
||||
function_zip,
|
||||
b.getInstallPath(.bin, "bootstrap"),
|
||||
})
|
||||
else
|
||||
// We need to copy stuff around
|
||||
try std.fmt.allocPrint(b.allocator,
|
||||
\\cp {s} {s} && \
|
||||
\\zip -qj9 {s} {s} && \
|
||||
\\rm {s}
|
||||
, .{
|
||||
b.getInstallPath(.bin, exe.out_filename),
|
||||
b.getInstallPath(.bin, "bootstrap"),
|
||||
function_zip,
|
||||
b.getInstallPath(.bin, "bootstrap"),
|
||||
b.getInstallPath(.bin, "bootstrap"),
|
||||
});
|
||||
// std.debug.print("\nzip cmdline: {s}", .{zip});
|
||||
defer b.allocator.free(zip);
|
||||
var zip_cmd = b.addSystemCommand(&.{ "/bin/sh", "-c", zip });
|
||||
zip_cmd.step.dependOn(b.getInstallStep());
|
||||
package_step.dependOn(&zip_cmd.step);
|
||||
|
||||
// Deployment
|
||||
const deploy_step = b.step("awslambda_deploy", "Deploy the function");
|
||||
|
||||
const iam_role_name = b.option(
|
||||
[]const u8,
|
||||
"function-role",
|
||||
"IAM role name for function (will create if it does not exist) [lambda_basic_execution]",
|
||||
) orelse "lambda_basic_execution";
|
||||
var iam_role_arn = b.option(
|
||||
[]const u8,
|
||||
"function-arn",
|
||||
"Preexisting IAM role arn for function",
|
||||
);
|
||||
|
||||
const iam_step = b.step("awslambda_iam", "Create/Get IAM role for function");
|
||||
deploy_step.dependOn(iam_step); // iam_step will either be a noop or all the stuff below
|
||||
const iam_role_param: []u8 = blk: {
|
||||
if (iam_role_arn != null)
|
||||
break :blk try std.fmt.allocPrint(b.allocator, "--role {s}", .{iam_role_arn.?});
|
||||
|
||||
if (iam_role_name.len == 0)
|
||||
@panic("Either function-role or function-arn must be specified. function-arn will allow deployment without creating a role");
|
||||
|
||||
// Now we have an iam role name to use, but no iam role arn. Let's go hunting
|
||||
// Once this is done once, we'll have a file with the arn in "cache"
|
||||
// The iam arn will reside in an 'iam_role' file in the bin directory
|
||||
|
||||
// Build system command to create the role if necessary and get the role arn
|
||||
const iam_role_file = b.getInstallPath(.bin, "iam_role");
|
||||
|
||||
if (!fileExists(iam_role_file)) {
|
||||
// std.debug.print("file does not exist", .{});
|
||||
// Our cache file does not exist on disk, so we'll create/get the role
|
||||
// arn using the AWS CLI and dump to disk here
|
||||
const ifstatement_fmt =
|
||||
\\ if aws iam get-role --role-name {s} 2>&1 |grep -q NoSuchEntity; then aws iam create-role --output text --query Role.Arn --role-name {s} --assume-role-policy-document '{{
|
||||
\\ "Version": "2012-10-17",
|
||||
\\ "Statement": [
|
||||
\\ {{
|
||||
\\ "Sid": "",
|
||||
\\ "Effect": "Allow",
|
||||
\\ "Principal": {{
|
||||
\\ "Service": "lambda.amazonaws.com"
|
||||
\\ }},
|
||||
\\ "Action": "sts:AssumeRole"
|
||||
\\ }}
|
||||
\\ ]}}' > /dev/null; fi && \
|
||||
\\ aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AWSLambdaExecute --role-name lambda_basic_execution && \
|
||||
\\ aws iam get-role --role-name lambda_basic_execution --query Role.Arn --output text > {s}
|
||||
;
|
||||
const ifstatement = try std.fmt.allocPrint(
|
||||
b.allocator,
|
||||
ifstatement_fmt,
|
||||
.{ iam_role_name, iam_role_name, iam_role_file },
|
||||
);
|
||||
iam_step.dependOn(&b.addSystemCommand(&.{ "/bin/sh", "-c", ifstatement }).step);
|
||||
}
|
||||
|
||||
break :blk try std.fmt.allocPrint(b.allocator, "--role \"$(cat {s})\"", .{iam_role_file});
|
||||
};
|
||||
const function_name_file = b.getInstallPath(.bin, function_name);
|
||||
const ifstatement = "if [ ! -f {s} ] || [ {s} -nt {s} ]; then if aws lambda get-function --function-name {s} 2>&1 |grep -q ResourceNotFoundException; then echo not found > /dev/null; {s}; else echo found > /dev/null; {s}; fi; fi";
|
||||
// The architectures option was introduced in 2.2.43 released 2021-10-01
|
||||
// We want to use arm64 here because it is both faster and cheaper for most
|
||||
// Amazon Linux 2 is the only arm64 supported option
|
||||
// TODO: This should determine compilation target and use x86_64 if needed
|
||||
const not_found = "aws lambda create-function --architectures arm64 --runtime provided.al2 --function-name {s} --zip-file fileb://{s} --handler not_applicable {s} && touch {s}";
|
||||
const not_found_fmt = try std.fmt.allocPrint(b.allocator, not_found, .{ function_name, function_zip, iam_role_param, function_name_file });
|
||||
defer b.allocator.free(not_found_fmt);
|
||||
const found = "aws lambda update-function-code --function-name {s} --zip-file fileb://{s} && touch {s}";
|
||||
const found_fmt = try std.fmt.allocPrint(b.allocator, found, .{ function_name, function_zip, function_name_file });
|
||||
defer b.allocator.free(found_fmt);
|
||||
var found_final: []const u8 = undefined;
|
||||
var not_found_final: []const u8 = undefined;
|
||||
if (b.args) |args| {
|
||||
found_final = try addArgs(b.allocator, found_fmt, args);
|
||||
not_found_final = try addArgs(b.allocator, not_found_fmt, args);
|
||||
} else {
|
||||
found_final = found_fmt;
|
||||
not_found_final = not_found_fmt;
|
||||
}
|
||||
const cmd = try std.fmt.allocPrint(b.allocator, ifstatement, .{
|
||||
function_name_file,
|
||||
b.getInstallPath(.bin, exe.out_filename),
|
||||
function_name_file,
|
||||
function_name,
|
||||
not_found_fmt,
|
||||
found_fmt,
|
||||
});
|
||||
|
||||
defer b.allocator.free(cmd);
|
||||
|
||||
// std.debug.print("{s}\n", .{cmd});
|
||||
deploy_step.dependOn(package_step);
|
||||
deploy_step.dependOn(&b.addSystemCommand(&.{ "/bin/sh", "-c", cmd }).step);
|
||||
|
||||
const payload = b.option([]const u8, "payload", "Lambda payload [{\"foo\":\"bar\", \"baz\": \"qux\"}]") orelse
|
||||
\\ {"foo": "bar", "baz": "qux"}"
|
||||
;
|
||||
|
||||
const run_script =
|
||||
\\ f=$(mktemp) && \
|
||||
\\ logs=$(aws lambda invoke \
|
||||
\\ --cli-binary-format raw-in-base64-out \
|
||||
\\ --invocation-type RequestResponse \
|
||||
\\ --function-name {s} \
|
||||
\\ --payload '{s}' \
|
||||
\\ --log-type Tail \
|
||||
\\ --query LogResult \
|
||||
\\ --output text "$f" |base64 -d) && \
|
||||
\\ cat "$f" && rm "$f" && \
|
||||
\\ echo && echo && echo "$logs"
|
||||
;
|
||||
const run_script_fmt = try std.fmt.allocPrint(b.allocator, run_script, .{ function_name, payload });
|
||||
defer b.allocator.free(run_script_fmt);
|
||||
const run_cmd = b.addSystemCommand(&.{ "/bin/sh", "-c", run_script_fmt });
|
||||
run_cmd.step.dependOn(deploy_step);
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
const run_step = b.step("awslambda_run", "Run the app in AWS lambda");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}
|
|
@ -1,777 +0,0 @@
|
|||
// node_modules/@cloudflare/workers-wasi/dist/index.mjs
|
||||
import wasm from "./c5f1acc97ad09df861eff9ef567c2186d4e38de3-memfs.wasm";
|
||||
var __accessCheck = (obj, member, msg) => {
|
||||
if (!member.has(obj))
|
||||
throw TypeError("Cannot " + msg);
|
||||
};
|
||||
var __privateGet = (obj, member, getter) => {
|
||||
__accessCheck(obj, member, "read from private field");
|
||||
return getter ? getter.call(obj) : member.get(obj);
|
||||
};
|
||||
var __privateAdd = (obj, member, value) => {
|
||||
if (member.has(obj))
|
||||
throw TypeError("Cannot add the same private member more than once");
|
||||
member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
||||
};
|
||||
var __privateSet = (obj, member, value, setter) => {
|
||||
__accessCheck(obj, member, "write to private field");
|
||||
setter ? setter.call(obj, value) : member.set(obj, value);
|
||||
return value;
|
||||
};
|
||||
var __privateMethod = (obj, member, method) => {
|
||||
__accessCheck(obj, member, "access private method");
|
||||
return method;
|
||||
};
|
||||
var Result;
|
||||
(function(Result2) {
|
||||
Result2[Result2["SUCCESS"] = 0] = "SUCCESS";
|
||||
Result2[Result2["EBADF"] = 8] = "EBADF";
|
||||
Result2[Result2["EINVAL"] = 28] = "EINVAL";
|
||||
Result2[Result2["ENOENT"] = 44] = "ENOENT";
|
||||
Result2[Result2["ENOSYS"] = 52] = "ENOSYS";
|
||||
Result2[Result2["ENOTSUP"] = 58] = "ENOTSUP";
|
||||
})(Result || (Result = {}));
|
||||
var Clock;
|
||||
(function(Clock2) {
|
||||
Clock2[Clock2["REALTIME"] = 0] = "REALTIME";
|
||||
Clock2[Clock2["MONOTONIC"] = 1] = "MONOTONIC";
|
||||
Clock2[Clock2["PROCESS_CPUTIME_ID"] = 2] = "PROCESS_CPUTIME_ID";
|
||||
Clock2[Clock2["THREAD_CPUTIME_ID"] = 3] = "THREAD_CPUTIME_ID";
|
||||
})(Clock || (Clock = {}));
|
||||
var iovViews = (view, iovs_ptr, iovs_len) => {
|
||||
let result = Array(iovs_len);
|
||||
for (let i = 0; i < iovs_len; i++) {
|
||||
const bufferPtr = view.getUint32(iovs_ptr, true);
|
||||
iovs_ptr += 4;
|
||||
const bufferLen = view.getUint32(iovs_ptr, true);
|
||||
iovs_ptr += 4;
|
||||
result[i] = new Uint8Array(view.buffer, bufferPtr, bufferLen);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
var _instance;
|
||||
var _hostMemory;
|
||||
var _getInternalView;
|
||||
var getInternalView_fn;
|
||||
var _copyFrom;
|
||||
var copyFrom_fn;
|
||||
var MemFS = class {
|
||||
constructor(preopens, fs) {
|
||||
__privateAdd(this, _getInternalView);
|
||||
__privateAdd(this, _copyFrom);
|
||||
__privateAdd(this, _instance, void 0);
|
||||
__privateAdd(this, _hostMemory, void 0);
|
||||
__privateSet(this, _instance, new WebAssembly.Instance(wasm, {
|
||||
internal: {
|
||||
now_ms: () => Date.now(),
|
||||
trace: (isError, addr, size) => {
|
||||
const view = new Uint8Array(__privateMethod(this, _getInternalView, getInternalView_fn).call(this).buffer, addr, size);
|
||||
const s = new TextDecoder().decode(view);
|
||||
if (isError) {
|
||||
throw new Error(s);
|
||||
} else {
|
||||
console.info(s);
|
||||
}
|
||||
},
|
||||
copy_out: (srcAddr, dstAddr, size) => {
|
||||
const dst = new Uint8Array(__privateGet(this, _hostMemory).buffer, dstAddr, size);
|
||||
const src = new Uint8Array(__privateMethod(this, _getInternalView, getInternalView_fn).call(this).buffer, srcAddr, size);
|
||||
dst.set(src);
|
||||
},
|
||||
copy_in: (srcAddr, dstAddr, size) => {
|
||||
const src = new Uint8Array(__privateGet(this, _hostMemory).buffer, srcAddr, size);
|
||||
const dst = new Uint8Array(__privateMethod(this, _getInternalView, getInternalView_fn).call(this).buffer, dstAddr, size);
|
||||
dst.set(src);
|
||||
}
|
||||
},
|
||||
wasi_snapshot_preview1: {
|
||||
proc_exit: (_) => {
|
||||
},
|
||||
fd_seek: () => Result.ENOSYS,
|
||||
fd_write: () => Result.ENOSYS,
|
||||
fd_close: () => Result.ENOSYS
|
||||
}
|
||||
}));
|
||||
this.exports = __privateGet(this, _instance).exports;
|
||||
const start = __privateGet(this, _instance).exports._start;
|
||||
start();
|
||||
const data = new TextEncoder().encode(JSON.stringify({ preopens, fs }));
|
||||
const initialize_internal = __privateGet(this, _instance).exports.initialize_internal;
|
||||
initialize_internal(__privateMethod(this, _copyFrom, copyFrom_fn).call(this, data), data.byteLength);
|
||||
}
|
||||
initialize(hostMemory) {
|
||||
__privateSet(this, _hostMemory, hostMemory);
|
||||
}
|
||||
};
|
||||
_instance = /* @__PURE__ */ new WeakMap();
|
||||
_hostMemory = /* @__PURE__ */ new WeakMap();
|
||||
_getInternalView = /* @__PURE__ */ new WeakSet();
|
||||
getInternalView_fn = function() {
|
||||
const memory = __privateGet(this, _instance).exports.memory;
|
||||
return new DataView(memory.buffer);
|
||||
};
|
||||
_copyFrom = /* @__PURE__ */ new WeakSet();
|
||||
copyFrom_fn = function(src) {
|
||||
const dstAddr = __privateGet(this, _instance).exports.allocate(src.byteLength);
|
||||
new Uint8Array(__privateMethod(this, _getInternalView, getInternalView_fn).call(this).buffer, dstAddr, src.byteLength).set(src);
|
||||
return dstAddr;
|
||||
};
|
||||
var DATA_ADDR = 16;
|
||||
var DATA_START = DATA_ADDR + 8;
|
||||
var DATA_END = 1024;
|
||||
var WRAPPED_EXPORTS = /* @__PURE__ */ new WeakMap();
|
||||
var State = {
|
||||
None: 0,
|
||||
Unwinding: 1,
|
||||
Rewinding: 2
|
||||
};
|
||||
function isPromise(obj) {
|
||||
return !!obj && (typeof obj === "object" || typeof obj === "function") && typeof obj.then === "function";
|
||||
}
|
||||
function proxyGet(obj, transform) {
|
||||
return new Proxy(obj, {
|
||||
get: (obj2, name) => transform(obj2[name])
|
||||
});
|
||||
}
|
||||
var Asyncify = class {
|
||||
constructor() {
|
||||
this.value = void 0;
|
||||
this.exports = null;
|
||||
}
|
||||
getState() {
|
||||
return this.exports.asyncify_get_state();
|
||||
}
|
||||
assertNoneState() {
|
||||
let state = this.getState();
|
||||
if (state !== State.None) {
|
||||
throw new Error(`Invalid async state ${state}, expected 0.`);
|
||||
}
|
||||
}
|
||||
wrapImportFn(fn) {
|
||||
return (...args) => {
|
||||
if (this.getState() === State.Rewinding) {
|
||||
this.exports.asyncify_stop_rewind();
|
||||
return this.value;
|
||||
}
|
||||
this.assertNoneState();
|
||||
let value = fn(...args);
|
||||
if (!isPromise(value)) {
|
||||
return value;
|
||||
}
|
||||
this.exports.asyncify_start_unwind(DATA_ADDR);
|
||||
this.value = value;
|
||||
};
|
||||
}
|
||||
wrapModuleImports(module) {
|
||||
return proxyGet(module, (value) => {
|
||||
if (typeof value === "function") {
|
||||
return this.wrapImportFn(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
wrapImports(imports) {
|
||||
if (imports === void 0)
|
||||
return;
|
||||
return proxyGet(imports, (moduleImports = /* @__PURE__ */ Object.create(null)) => this.wrapModuleImports(moduleImports));
|
||||
}
|
||||
wrapExportFn(fn) {
|
||||
let newExport = WRAPPED_EXPORTS.get(fn);
|
||||
if (newExport !== void 0) {
|
||||
return newExport;
|
||||
}
|
||||
newExport = async (...args) => {
|
||||
this.assertNoneState();
|
||||
let result = fn(...args);
|
||||
while (this.getState() === State.Unwinding) {
|
||||
this.exports.asyncify_stop_unwind();
|
||||
this.value = await this.value;
|
||||
this.assertNoneState();
|
||||
this.exports.asyncify_start_rewind(DATA_ADDR);
|
||||
result = fn();
|
||||
}
|
||||
this.assertNoneState();
|
||||
return result;
|
||||
};
|
||||
WRAPPED_EXPORTS.set(fn, newExport);
|
||||
return newExport;
|
||||
}
|
||||
wrapExports(exports) {
|
||||
let newExports = /* @__PURE__ */ Object.create(null);
|
||||
for (let exportName in exports) {
|
||||
let value = exports[exportName];
|
||||
if (typeof value === "function" && !exportName.startsWith("asyncify_")) {
|
||||
value = this.wrapExportFn(value);
|
||||
}
|
||||
Object.defineProperty(newExports, exportName, {
|
||||
enumerable: true,
|
||||
value
|
||||
});
|
||||
}
|
||||
WRAPPED_EXPORTS.set(exports, newExports);
|
||||
return newExports;
|
||||
}
|
||||
init(instance, imports) {
|
||||
const { exports } = instance;
|
||||
const memory = exports.memory || imports.env && imports.env.memory;
|
||||
new Int32Array(memory.buffer, DATA_ADDR).set([DATA_START, DATA_END]);
|
||||
this.exports = this.wrapExports(exports);
|
||||
Object.setPrototypeOf(instance, Instance.prototype);
|
||||
}
|
||||
};
|
||||
var Instance = class extends WebAssembly.Instance {
|
||||
constructor(module, imports) {
|
||||
let state = new Asyncify();
|
||||
super(module, state.wrapImports(imports));
|
||||
state.init(this, imports);
|
||||
}
|
||||
get exports() {
|
||||
return WRAPPED_EXPORTS.get(super.exports);
|
||||
}
|
||||
};
|
||||
Object.defineProperty(Instance.prototype, "exports", { enumerable: true });
|
||||
var DevNull = class {
|
||||
writev(iovs) {
|
||||
return iovs.map((iov) => iov.byteLength).reduce((prev, curr) => prev + curr);
|
||||
}
|
||||
readv(iovs) {
|
||||
return 0;
|
||||
}
|
||||
close() {
|
||||
}
|
||||
async preRun() {
|
||||
}
|
||||
async postRun() {
|
||||
}
|
||||
};
|
||||
var ReadableStreamBase = class {
|
||||
writev(iovs) {
|
||||
throw new Error("Attempting to call write on a readable stream");
|
||||
}
|
||||
close() {
|
||||
}
|
||||
async preRun() {
|
||||
}
|
||||
async postRun() {
|
||||
}
|
||||
};
|
||||
var _pending;
|
||||
var _reader;
|
||||
var AsyncReadableStreamAdapter = class extends ReadableStreamBase {
|
||||
constructor(reader) {
|
||||
super();
|
||||
__privateAdd(this, _pending, new Uint8Array());
|
||||
__privateAdd(this, _reader, void 0);
|
||||
__privateSet(this, _reader, reader);
|
||||
}
|
||||
async readv(iovs) {
|
||||
let read = 0;
|
||||
for (let iov of iovs) {
|
||||
while (iov.byteLength > 0) {
|
||||
if (__privateGet(this, _pending).byteLength === 0) {
|
||||
const result = await __privateGet(this, _reader).read();
|
||||
if (result.done) {
|
||||
return read;
|
||||
}
|
||||
__privateSet(this, _pending, result.value);
|
||||
}
|
||||
const bytes = Math.min(iov.byteLength, __privateGet(this, _pending).byteLength);
|
||||
iov.set(__privateGet(this, _pending).subarray(0, bytes));
|
||||
__privateSet(this, _pending, __privateGet(this, _pending).subarray(bytes));
|
||||
read += bytes;
|
||||
iov = iov.subarray(bytes);
|
||||
}
|
||||
}
|
||||
return read;
|
||||
}
|
||||
};
|
||||
_pending = /* @__PURE__ */ new WeakMap();
|
||||
_reader = /* @__PURE__ */ new WeakMap();
|
||||
var WritableStreamBase = class {
|
||||
readv(iovs) {
|
||||
throw new Error("Attempting to call read on a writable stream");
|
||||
}
|
||||
close() {
|
||||
}
|
||||
async preRun() {
|
||||
}
|
||||
async postRun() {
|
||||
}
|
||||
};
|
||||
var _writer;
|
||||
var AsyncWritableStreamAdapter = class extends WritableStreamBase {
|
||||
constructor(writer) {
|
||||
super();
|
||||
__privateAdd(this, _writer, void 0);
|
||||
__privateSet(this, _writer, writer);
|
||||
}
|
||||
async writev(iovs) {
|
||||
let written = 0;
|
||||
for (const iov of iovs) {
|
||||
if (iov.byteLength === 0) {
|
||||
continue;
|
||||
}
|
||||
await __privateGet(this, _writer).write(iov);
|
||||
written += iov.byteLength;
|
||||
}
|
||||
return written;
|
||||
}
|
||||
async close() {
|
||||
await __privateGet(this, _writer).close();
|
||||
}
|
||||
};
|
||||
_writer = /* @__PURE__ */ new WeakMap();
|
||||
var _writer2;
|
||||
var _buffer;
|
||||
var _bytesWritten;
|
||||
var SyncWritableStreamAdapter = class extends WritableStreamBase {
|
||||
constructor(writer) {
|
||||
super();
|
||||
__privateAdd(this, _writer2, void 0);
|
||||
__privateAdd(this, _buffer, new Uint8Array(4096));
|
||||
__privateAdd(this, _bytesWritten, 0);
|
||||
__privateSet(this, _writer2, writer);
|
||||
}
|
||||
writev(iovs) {
|
||||
let written = 0;
|
||||
for (const iov of iovs) {
|
||||
if (iov.byteLength === 0) {
|
||||
continue;
|
||||
}
|
||||
const requiredCapacity = __privateGet(this, _bytesWritten) + iov.byteLength;
|
||||
if (requiredCapacity > __privateGet(this, _buffer).byteLength) {
|
||||
let desiredCapacity = __privateGet(this, _buffer).byteLength;
|
||||
while (desiredCapacity < requiredCapacity) {
|
||||
desiredCapacity *= 1.5;
|
||||
}
|
||||
const oldBuffer = __privateGet(this, _buffer);
|
||||
__privateSet(this, _buffer, new Uint8Array(desiredCapacity));
|
||||
__privateGet(this, _buffer).set(oldBuffer);
|
||||
}
|
||||
__privateGet(this, _buffer).set(iov, __privateGet(this, _bytesWritten));
|
||||
written += iov.byteLength;
|
||||
__privateSet(this, _bytesWritten, __privateGet(this, _bytesWritten) + iov.byteLength);
|
||||
}
|
||||
return written;
|
||||
}
|
||||
async postRun() {
|
||||
const slice = __privateGet(this, _buffer).subarray(0, __privateGet(this, _bytesWritten));
|
||||
await __privateGet(this, _writer2).write(slice);
|
||||
await __privateGet(this, _writer2).close();
|
||||
}
|
||||
};
|
||||
_writer2 = /* @__PURE__ */ new WeakMap();
|
||||
_buffer = /* @__PURE__ */ new WeakMap();
|
||||
_bytesWritten = /* @__PURE__ */ new WeakMap();
|
||||
var _buffer2;
|
||||
var _reader2;
|
||||
var SyncReadableStreamAdapter = class extends ReadableStreamBase {
|
||||
constructor(reader) {
|
||||
super();
|
||||
__privateAdd(this, _buffer2, void 0);
|
||||
__privateAdd(this, _reader2, void 0);
|
||||
__privateSet(this, _reader2, reader);
|
||||
}
|
||||
readv(iovs) {
|
||||
let read = 0;
|
||||
for (const iov of iovs) {
|
||||
const bytes = Math.min(iov.byteLength, __privateGet(this, _buffer2).byteLength);
|
||||
if (bytes <= 0) {
|
||||
break;
|
||||
}
|
||||
iov.set(__privateGet(this, _buffer2).subarray(0, bytes));
|
||||
__privateSet(this, _buffer2, __privateGet(this, _buffer2).subarray(bytes));
|
||||
read += bytes;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
async preRun() {
|
||||
const pending = [];
|
||||
let length = 0;
|
||||
for (; ; ) {
|
||||
const result2 = await __privateGet(this, _reader2).read();
|
||||
if (result2.done) {
|
||||
break;
|
||||
}
|
||||
const data = result2.value;
|
||||
pending.push(data);
|
||||
length += data.length;
|
||||
}
|
||||
let result = new Uint8Array(length);
|
||||
let offset = 0;
|
||||
pending.forEach((item) => {
|
||||
result.set(item, offset);
|
||||
offset += item.length;
|
||||
});
|
||||
__privateSet(this, _buffer2, result);
|
||||
}
|
||||
};
|
||||
_buffer2 = /* @__PURE__ */ new WeakMap();
|
||||
_reader2 = /* @__PURE__ */ new WeakMap();
|
||||
var fromReadableStream = (stream, supportsAsync) => {
|
||||
if (!stream) {
|
||||
return new DevNull();
|
||||
}
|
||||
if (supportsAsync) {
|
||||
return new AsyncReadableStreamAdapter(stream.getReader());
|
||||
}
|
||||
return new SyncReadableStreamAdapter(stream.getReader());
|
||||
};
|
||||
var fromWritableStream = (stream, supportsAsync) => {
|
||||
if (!stream) {
|
||||
return new DevNull();
|
||||
}
|
||||
if (supportsAsync) {
|
||||
return new AsyncWritableStreamAdapter(stream.getWriter());
|
||||
}
|
||||
return new SyncWritableStreamAdapter(stream.getWriter());
|
||||
};
|
||||
var ProcessExit = class extends Error {
|
||||
constructor(code) {
|
||||
super(`proc_exit=${code}`);
|
||||
this.code = code;
|
||||
Object.setPrototypeOf(this, ProcessExit.prototype);
|
||||
}
|
||||
};
|
||||
var _args;
|
||||
var _env;
|
||||
var _memory;
|
||||
var _preopens;
|
||||
var _returnOnExit;
|
||||
var _streams;
|
||||
var _memfs;
|
||||
var _state;
|
||||
var _asyncify;
|
||||
var _view;
|
||||
var view_fn;
|
||||
var _fillValues;
|
||||
var fillValues_fn;
|
||||
var _fillSizes;
|
||||
var fillSizes_fn;
|
||||
var _args_get;
|
||||
var args_get_fn;
|
||||
var _args_sizes_get;
|
||||
var args_sizes_get_fn;
|
||||
var _clock_res_get;
|
||||
var clock_res_get_fn;
|
||||
var _clock_time_get;
|
||||
var clock_time_get_fn;
|
||||
var _environ_get;
|
||||
var environ_get_fn;
|
||||
var _environ_sizes_get;
|
||||
var environ_sizes_get_fn;
|
||||
var _fd_read;
|
||||
var fd_read_fn;
|
||||
var _fd_write;
|
||||
var fd_write_fn;
|
||||
var _poll_oneoff;
|
||||
var poll_oneoff_fn;
|
||||
var _proc_exit;
|
||||
var proc_exit_fn;
|
||||
var _proc_raise;
|
||||
var proc_raise_fn;
|
||||
var _random_get;
|
||||
var random_get_fn;
|
||||
var _sched_yield;
|
||||
var sched_yield_fn;
|
||||
var _sock_recv;
|
||||
var sock_recv_fn;
|
||||
var _sock_send;
|
||||
var sock_send_fn;
|
||||
var _sock_shutdown;
|
||||
var sock_shutdown_fn;
|
||||
var WASI = class {
|
||||
constructor(options) {
|
||||
__privateAdd(this, _view);
|
||||
__privateAdd(this, _fillValues);
|
||||
__privateAdd(this, _fillSizes);
|
||||
__privateAdd(this, _args_get);
|
||||
__privateAdd(this, _args_sizes_get);
|
||||
__privateAdd(this, _clock_res_get);
|
||||
__privateAdd(this, _clock_time_get);
|
||||
__privateAdd(this, _environ_get);
|
||||
__privateAdd(this, _environ_sizes_get);
|
||||
__privateAdd(this, _fd_read);
|
||||
__privateAdd(this, _fd_write);
|
||||
__privateAdd(this, _poll_oneoff);
|
||||
__privateAdd(this, _proc_exit);
|
||||
__privateAdd(this, _proc_raise);
|
||||
__privateAdd(this, _random_get);
|
||||
__privateAdd(this, _sched_yield);
|
||||
__privateAdd(this, _sock_recv);
|
||||
__privateAdd(this, _sock_send);
|
||||
__privateAdd(this, _sock_shutdown);
|
||||
__privateAdd(this, _args, void 0);
|
||||
__privateAdd(this, _env, void 0);
|
||||
__privateAdd(this, _memory, void 0);
|
||||
__privateAdd(this, _preopens, void 0);
|
||||
__privateAdd(this, _returnOnExit, void 0);
|
||||
__privateAdd(this, _streams, void 0);
|
||||
__privateAdd(this, _memfs, void 0);
|
||||
__privateAdd(this, _state, new Asyncify());
|
||||
__privateAdd(this, _asyncify, void 0);
|
||||
__privateSet(this, _args, options?.args ?? []);
|
||||
const env = options?.env ?? {};
|
||||
__privateSet(this, _env, Object.keys(env).map((key) => {
|
||||
return `${key}=${env[key]}`;
|
||||
}));
|
||||
__privateSet(this, _returnOnExit, options?.returnOnExit ?? false);
|
||||
__privateSet(this, _preopens, options?.preopens ?? []);
|
||||
__privateSet(this, _asyncify, options?.streamStdio ?? false);
|
||||
__privateSet(this, _streams, [
|
||||
fromReadableStream(options?.stdin, __privateGet(this, _asyncify)),
|
||||
fromWritableStream(options?.stdout, __privateGet(this, _asyncify)),
|
||||
fromWritableStream(options?.stderr, __privateGet(this, _asyncify))
|
||||
]);
|
||||
__privateSet(this, _memfs, new MemFS(__privateGet(this, _preopens), options?.fs ?? {}));
|
||||
}
|
||||
async start(instance) {
|
||||
__privateSet(this, _memory, instance.exports.memory);
|
||||
__privateGet(this, _memfs).initialize(__privateGet(this, _memory));
|
||||
try {
|
||||
if (__privateGet(this, _asyncify)) {
|
||||
if (!instance.exports.asyncify_get_state) {
|
||||
throw new Error("streamStdio is requested but the module is missing 'Asyncify' exports, see https://github.com/GoogleChromeLabs/asyncify");
|
||||
}
|
||||
__privateGet(this, _state).init(instance);
|
||||
}
|
||||
await Promise.all(__privateGet(this, _streams).map((s) => s.preRun()));
|
||||
if (__privateGet(this, _asyncify)) {
|
||||
await __privateGet(this, _state).exports._start();
|
||||
} else {
|
||||
const entrypoint = instance.exports._start;
|
||||
entrypoint();
|
||||
}
|
||||
} catch (e) {
|
||||
if (!__privateGet(this, _returnOnExit)) {
|
||||
throw e;
|
||||
}
|
||||
if (e.message === "unreachable") {
|
||||
return 134;
|
||||
} else if (e instanceof ProcessExit) {
|
||||
return e.code;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
await Promise.all(__privateGet(this, _streams).map((s) => s.close()));
|
||||
await Promise.all(__privateGet(this, _streams).map((s) => s.postRun()));
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
get wasiImport() {
|
||||
const wrap = (f, self = this) => {
|
||||
const bound = f.bind(self);
|
||||
if (__privateGet(this, _asyncify)) {
|
||||
return __privateGet(this, _state).wrapImportFn(bound);
|
||||
}
|
||||
return bound;
|
||||
};
|
||||
return {
|
||||
args_get: wrap(__privateMethod(this, _args_get, args_get_fn)),
|
||||
args_sizes_get: wrap(__privateMethod(this, _args_sizes_get, args_sizes_get_fn)),
|
||||
clock_res_get: wrap(__privateMethod(this, _clock_res_get, clock_res_get_fn)),
|
||||
clock_time_get: wrap(__privateMethod(this, _clock_time_get, clock_time_get_fn)),
|
||||
environ_get: wrap(__privateMethod(this, _environ_get, environ_get_fn)),
|
||||
environ_sizes_get: wrap(__privateMethod(this, _environ_sizes_get, environ_sizes_get_fn)),
|
||||
fd_advise: wrap(__privateGet(this, _memfs).exports.fd_advise),
|
||||
fd_allocate: wrap(__privateGet(this, _memfs).exports.fd_allocate),
|
||||
fd_close: wrap(__privateGet(this, _memfs).exports.fd_close),
|
||||
fd_datasync: wrap(__privateGet(this, _memfs).exports.fd_datasync),
|
||||
fd_fdstat_get: wrap(__privateGet(this, _memfs).exports.fd_fdstat_get),
|
||||
fd_fdstat_set_flags: wrap(__privateGet(this, _memfs).exports.fd_fdstat_set_flags),
|
||||
fd_fdstat_set_rights: wrap(__privateGet(this, _memfs).exports.fd_fdstat_set_rights),
|
||||
fd_filestat_get: wrap(__privateGet(this, _memfs).exports.fd_filestat_get),
|
||||
fd_filestat_set_size: wrap(__privateGet(this, _memfs).exports.fd_filestat_set_size),
|
||||
fd_filestat_set_times: wrap(__privateGet(this, _memfs).exports.fd_filestat_set_times),
|
||||
fd_pread: wrap(__privateGet(this, _memfs).exports.fd_pread),
|
||||
fd_prestat_dir_name: wrap(__privateGet(this, _memfs).exports.fd_prestat_dir_name),
|
||||
fd_prestat_get: wrap(__privateGet(this, _memfs).exports.fd_prestat_get),
|
||||
fd_pwrite: wrap(__privateGet(this, _memfs).exports.fd_pwrite),
|
||||
fd_read: wrap(__privateMethod(this, _fd_read, fd_read_fn)),
|
||||
fd_readdir: wrap(__privateGet(this, _memfs).exports.fd_readdir),
|
||||
fd_renumber: wrap(__privateGet(this, _memfs).exports.fd_renumber),
|
||||
fd_seek: wrap(__privateGet(this, _memfs).exports.fd_seek),
|
||||
fd_sync: wrap(__privateGet(this, _memfs).exports.fd_sync),
|
||||
fd_tell: wrap(__privateGet(this, _memfs).exports.fd_tell),
|
||||
fd_write: wrap(__privateMethod(this, _fd_write, fd_write_fn)),
|
||||
path_create_directory: wrap(__privateGet(this, _memfs).exports.path_create_directory),
|
||||
path_filestat_get: wrap(__privateGet(this, _memfs).exports.path_filestat_get),
|
||||
path_filestat_set_times: wrap(__privateGet(this, _memfs).exports.path_filestat_set_times),
|
||||
path_link: wrap(__privateGet(this, _memfs).exports.path_link),
|
||||
path_open: wrap(__privateGet(this, _memfs).exports.path_open),
|
||||
path_readlink: wrap(__privateGet(this, _memfs).exports.path_readlink),
|
||||
path_remove_directory: wrap(__privateGet(this, _memfs).exports.path_remove_directory),
|
||||
path_rename: wrap(__privateGet(this, _memfs).exports.path_rename),
|
||||
path_symlink: wrap(__privateGet(this, _memfs).exports.path_symlink),
|
||||
path_unlink_file: wrap(__privateGet(this, _memfs).exports.path_unlink_file),
|
||||
poll_oneoff: wrap(__privateMethod(this, _poll_oneoff, poll_oneoff_fn)),
|
||||
proc_exit: wrap(__privateMethod(this, _proc_exit, proc_exit_fn)),
|
||||
proc_raise: wrap(__privateMethod(this, _proc_raise, proc_raise_fn)),
|
||||
random_get: wrap(__privateMethod(this, _random_get, random_get_fn)),
|
||||
sched_yield: wrap(__privateMethod(this, _sched_yield, sched_yield_fn)),
|
||||
sock_recv: wrap(__privateMethod(this, _sock_recv, sock_recv_fn)),
|
||||
sock_send: wrap(__privateMethod(this, _sock_send, sock_send_fn)),
|
||||
sock_shutdown: wrap(__privateMethod(this, _sock_shutdown, sock_shutdown_fn))
|
||||
};
|
||||
}
|
||||
};
|
||||
_args = /* @__PURE__ */ new WeakMap();
|
||||
_env = /* @__PURE__ */ new WeakMap();
|
||||
_memory = /* @__PURE__ */ new WeakMap();
|
||||
_preopens = /* @__PURE__ */ new WeakMap();
|
||||
_returnOnExit = /* @__PURE__ */ new WeakMap();
|
||||
_streams = /* @__PURE__ */ new WeakMap();
|
||||
_memfs = /* @__PURE__ */ new WeakMap();
|
||||
_state = /* @__PURE__ */ new WeakMap();
|
||||
_asyncify = /* @__PURE__ */ new WeakMap();
|
||||
_view = /* @__PURE__ */ new WeakSet();
|
||||
view_fn = function() {
|
||||
if (!__privateGet(this, _memory)) {
|
||||
throw new Error("this.memory not set");
|
||||
}
|
||||
return new DataView(__privateGet(this, _memory).buffer);
|
||||
};
|
||||
_fillValues = /* @__PURE__ */ new WeakSet();
|
||||
fillValues_fn = function(values, iter_ptr_ptr, buf_ptr) {
|
||||
const encoder = new TextEncoder();
|
||||
const buffer = new Uint8Array(__privateGet(this, _memory).buffer);
|
||||
const view = __privateMethod(this, _view, view_fn).call(this);
|
||||
for (const value of values) {
|
||||
view.setUint32(iter_ptr_ptr, buf_ptr, true);
|
||||
iter_ptr_ptr += 4;
|
||||
const data = encoder.encode(`${value}\0`);
|
||||
buffer.set(data, buf_ptr);
|
||||
buf_ptr += data.length;
|
||||
}
|
||||
return Result.SUCCESS;
|
||||
};
|
||||
_fillSizes = /* @__PURE__ */ new WeakSet();
|
||||
fillSizes_fn = function(values, count_ptr, buffer_size_ptr) {
|
||||
const view = __privateMethod(this, _view, view_fn).call(this);
|
||||
const encoder = new TextEncoder();
|
||||
const len = values.reduce((acc, value) => {
|
||||
return acc + encoder.encode(`${value}\0`).length;
|
||||
}, 0);
|
||||
view.setUint32(count_ptr, values.length, true);
|
||||
view.setUint32(buffer_size_ptr, len, true);
|
||||
return Result.SUCCESS;
|
||||
};
|
||||
_args_get = /* @__PURE__ */ new WeakSet();
|
||||
args_get_fn = function(argv_ptr_ptr, argv_buf_ptr) {
|
||||
return __privateMethod(this, _fillValues, fillValues_fn).call(this, __privateGet(this, _args), argv_ptr_ptr, argv_buf_ptr);
|
||||
};
|
||||
_args_sizes_get = /* @__PURE__ */ new WeakSet();
|
||||
args_sizes_get_fn = function(argc_ptr, argv_buf_size_ptr) {
|
||||
return __privateMethod(this, _fillSizes, fillSizes_fn).call(this, __privateGet(this, _args), argc_ptr, argv_buf_size_ptr);
|
||||
};
|
||||
_clock_res_get = /* @__PURE__ */ new WeakSet();
|
||||
clock_res_get_fn = function(id, retptr0) {
|
||||
switch (id) {
|
||||
case Clock.REALTIME:
|
||||
case Clock.MONOTONIC:
|
||||
case Clock.PROCESS_CPUTIME_ID:
|
||||
case Clock.THREAD_CPUTIME_ID: {
|
||||
const view = __privateMethod(this, _view, view_fn).call(this);
|
||||
view.setBigUint64(retptr0, BigInt(1e6), true);
|
||||
return Result.SUCCESS;
|
||||
}
|
||||
}
|
||||
return Result.EINVAL;
|
||||
};
|
||||
_clock_time_get = /* @__PURE__ */ new WeakSet();
|
||||
clock_time_get_fn = function(id, precision, retptr0) {
|
||||
switch (id) {
|
||||
case Clock.REALTIME:
|
||||
case Clock.MONOTONIC:
|
||||
case Clock.PROCESS_CPUTIME_ID:
|
||||
case Clock.THREAD_CPUTIME_ID: {
|
||||
const view = __privateMethod(this, _view, view_fn).call(this);
|
||||
view.setBigUint64(retptr0, BigInt(Date.now()) * BigInt(1e6), true);
|
||||
return Result.SUCCESS;
|
||||
}
|
||||
}
|
||||
return Result.EINVAL;
|
||||
};
|
||||
_environ_get = /* @__PURE__ */ new WeakSet();
|
||||
environ_get_fn = function(env_ptr_ptr, env_buf_ptr) {
|
||||
return __privateMethod(this, _fillValues, fillValues_fn).call(this, __privateGet(this, _env), env_ptr_ptr, env_buf_ptr);
|
||||
};
|
||||
_environ_sizes_get = /* @__PURE__ */ new WeakSet();
|
||||
environ_sizes_get_fn = function(env_ptr, env_buf_size_ptr) {
|
||||
return __privateMethod(this, _fillSizes, fillSizes_fn).call(this, __privateGet(this, _env), env_ptr, env_buf_size_ptr);
|
||||
};
|
||||
_fd_read = /* @__PURE__ */ new WeakSet();
|
||||
fd_read_fn = function(fd, iovs_ptr, iovs_len, retptr0) {
|
||||
if (fd < 3) {
|
||||
const desc = __privateGet(this, _streams)[fd];
|
||||
const view = __privateMethod(this, _view, view_fn).call(this);
|
||||
const iovs = iovViews(view, iovs_ptr, iovs_len);
|
||||
const result = desc.readv(iovs);
|
||||
if (typeof result === "number") {
|
||||
view.setUint32(retptr0, result, true);
|
||||
return Result.SUCCESS;
|
||||
}
|
||||
const promise = result;
|
||||
return promise.then((read) => {
|
||||
view.setUint32(retptr0, read, true);
|
||||
return Result.SUCCESS;
|
||||
});
|
||||
}
|
||||
return __privateGet(this, _memfs).exports.fd_read(fd, iovs_ptr, iovs_len, retptr0);
|
||||
};
|
||||
_fd_write = /* @__PURE__ */ new WeakSet();
|
||||
fd_write_fn = function(fd, ciovs_ptr, ciovs_len, retptr0) {
|
||||
if (fd < 3) {
|
||||
const desc = __privateGet(this, _streams)[fd];
|
||||
const view = __privateMethod(this, _view, view_fn).call(this);
|
||||
const iovs = iovViews(view, ciovs_ptr, ciovs_len);
|
||||
const result = desc.writev(iovs);
|
||||
if (typeof result === "number") {
|
||||
view.setUint32(retptr0, result, true);
|
||||
return Result.SUCCESS;
|
||||
}
|
||||
let promise = result;
|
||||
return promise.then((written) => {
|
||||
view.setUint32(retptr0, written, true);
|
||||
return Result.SUCCESS;
|
||||
});
|
||||
}
|
||||
return __privateGet(this, _memfs).exports.fd_write(fd, ciovs_ptr, ciovs_len, retptr0);
|
||||
};
|
||||
_poll_oneoff = /* @__PURE__ */ new WeakSet();
|
||||
poll_oneoff_fn = function(in_ptr, out_ptr, nsubscriptions, retptr0) {
|
||||
return Result.ENOSYS;
|
||||
};
|
||||
_proc_exit = /* @__PURE__ */ new WeakSet();
|
||||
proc_exit_fn = function(code) {
|
||||
throw new ProcessExit(code);
|
||||
};
|
||||
_proc_raise = /* @__PURE__ */ new WeakSet();
|
||||
proc_raise_fn = function(signal) {
|
||||
return Result.ENOSYS;
|
||||
};
|
||||
_random_get = /* @__PURE__ */ new WeakSet();
|
||||
random_get_fn = function(buffer_ptr, buffer_len) {
|
||||
const buffer = new Uint8Array(__privateGet(this, _memory).buffer, buffer_ptr, buffer_len);
|
||||
crypto.getRandomValues(buffer);
|
||||
return Result.SUCCESS;
|
||||
};
|
||||
_sched_yield = /* @__PURE__ */ new WeakSet();
|
||||
sched_yield_fn = function() {
|
||||
return Result.SUCCESS;
|
||||
};
|
||||
_sock_recv = /* @__PURE__ */ new WeakSet();
|
||||
sock_recv_fn = function(fd, ri_data_ptr, ri_data_len, ri_flags, retptr0, retptr1) {
|
||||
return Result.ENOSYS;
|
||||
};
|
||||
_sock_send = /* @__PURE__ */ new WeakSet();
|
||||
sock_send_fn = function(fd, si_data_ptr, si_data_len, si_flags, retptr0) {
|
||||
return Result.ENOSYS;
|
||||
};
|
||||
_sock_shutdown = /* @__PURE__ */ new WeakSet();
|
||||
sock_shutdown_fn = function(fd, how) {
|
||||
return Result.ENOSYS;
|
||||
};
|
||||
|
||||
// src/index.js
|
207
src/standalone_server.zig
Normal file
207
src/standalone_server.zig
Normal file
|
@ -0,0 +1,207 @@
|
|||
const builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
const flexilib = @import("flexilib-interface");
|
||||
const interface = @import("universal_lambda_interface");
|
||||
|
||||
const log = std.log.scoped(.standalone_server);
|
||||
|
||||
/// We need to create a child process to be able to deal with panics appropriately
|
||||
pub fn runStandaloneServerParent(allocator: ?std.mem.Allocator, event_handler: interface.HandlerFn) !u8 {
|
||||
if (builtin.os.tag == .wasi) return error.WasiNotSupported;
|
||||
const alloc = allocator orelse std.heap.page_allocator;
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(alloc);
|
||||
defer arena.deinit();
|
||||
|
||||
const aa = arena.allocator();
|
||||
var al = std.ArrayList([]const u8).init(aa);
|
||||
defer al.deinit();
|
||||
var argi = std.process.args();
|
||||
// We do this first so it shows more prominently when looking at processes
|
||||
// Also it will be slightly faster for whatever that is worth
|
||||
const child_arg = "--child_of_standalone_server";
|
||||
if (argi.next()) |a| try al.append(a);
|
||||
try al.append(child_arg);
|
||||
while (argi.next()) |a| {
|
||||
if (std.mem.eql(u8, child_arg, a)) {
|
||||
// This should never actually return
|
||||
try runStandaloneServer(allocator, event_handler, 8080); // TODO: configurable port
|
||||
return 0;
|
||||
}
|
||||
try al.append(a);
|
||||
}
|
||||
// Parent
|
||||
const stdin = std.io.getStdIn();
|
||||
const stdout = std.io.getStdOut();
|
||||
const stderr = std.io.getStdErr();
|
||||
while (true) {
|
||||
var cp = std.ChildProcess.init(al.items, alloc);
|
||||
cp.stdin = stdin;
|
||||
cp.stdout = stdout;
|
||||
cp.stderr = stderr;
|
||||
_ = try cp.spawnAndWait();
|
||||
try stderr.writeAll("Caught abnormal process termination, relaunching server");
|
||||
}
|
||||
}
|
||||
|
||||
/// Will create a web server and marshall all requests back to our event handler
|
||||
/// To keep things simple, we'll have this on a single thread, at least for now
|
||||
fn runStandaloneServer(allocator: ?std.mem.Allocator, event_handler: interface.HandlerFn, port: u16) !void {
|
||||
const alloc = allocator orelse std.heap.page_allocator;
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(alloc);
|
||||
defer arena.deinit();
|
||||
|
||||
var aa = arena.allocator();
|
||||
const address = try std.net.Address.parseIp("127.0.0.1", port);
|
||||
var net_server = try address.listen(.{ .reuse_address = true });
|
||||
const server_port = net_server.listen_address.in.getPort();
|
||||
_ = try std.fmt.bufPrint(&server_url, "http://127.0.0.1:{d}", .{server_port});
|
||||
log.info("server listening at {s}", .{server_url});
|
||||
if (builtin.is_test) server_ready = true;
|
||||
|
||||
// No threads, maybe later
|
||||
//log.info("starting server thread, tid {d}", .{std.Thread.getCurrentId()});
|
||||
while (remaining_requests == null or remaining_requests.? > 0) {
|
||||
defer {
|
||||
if (remaining_requests) |*r| r.* -= 1;
|
||||
if (!arena.reset(.{ .retain_with_limit = 1024 * 1024 })) {
|
||||
// reallocation failed, arena is degraded
|
||||
log.warn("Arena reset failed and is degraded. Resetting arena", .{});
|
||||
arena.deinit();
|
||||
arena = std.heap.ArenaAllocator.init(alloc);
|
||||
aa = arena.allocator();
|
||||
}
|
||||
}
|
||||
const supports_getrusage = builtin.os.tag != .windows and @hasDecl(std.posix.system, "rusage"); // Is Windows it?
|
||||
var rss: if (supports_getrusage) std.posix.rusage else void = undefined;
|
||||
if (supports_getrusage and builtin.mode == .Debug)
|
||||
rss = std.posix.getrusage(std.posix.rusage.SELF);
|
||||
if (builtin.is_test) bytes_allocated = arena.queryCapacity();
|
||||
processRequest(aa, &net_server, event_handler) catch |e| {
|
||||
log.err("Unexpected error processing request: {any}", .{e});
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpStackTrace(trace.*);
|
||||
}
|
||||
};
|
||||
if (builtin.is_test) bytes_allocated = arena.queryCapacity() - bytes_allocated;
|
||||
if (supports_getrusage and builtin.mode == .Debug) { // and debug mode) {
|
||||
const rusage = std.posix.getrusage(std.posix.rusage.SELF);
|
||||
log.debug(
|
||||
"Request complete, max RSS of process: {d}M. Incremental: {d}K, User: {d}μs, System: {d}μs",
|
||||
.{
|
||||
@divTrunc(rusage.maxrss, 1024),
|
||||
rusage.maxrss - rss.maxrss,
|
||||
(rusage.utime.tv_sec - rss.utime.tv_sec) * std.time.us_per_s +
|
||||
rusage.utime.tv_usec - rss.utime.tv_usec,
|
||||
(rusage.stime.tv_sec - rss.stime.tv_sec) * std.time.us_per_s +
|
||||
rusage.stime.tv_usec - rss.stime.tv_usec,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fn processRequest(aa: std.mem.Allocator, server: *std.net.Server, event_handler: interface.HandlerFn) !void {
|
||||
// This function is under test, but not the standalone server itself
|
||||
var connection = try server.accept();
|
||||
defer connection.stream.close();
|
||||
|
||||
var read_buffer: [1024 * 16]u8 = undefined; // TODO: Fix this
|
||||
var server_connection = std.http.Server.init(connection, &read_buffer);
|
||||
var req = try server_connection.receiveHead();
|
||||
|
||||
const request_body = try (try req.reader()).readAllAlloc(aa, @as(usize, std.math.maxInt(usize)));
|
||||
var request_headers = std.ArrayList(std.http.Header).init(aa);
|
||||
defer request_headers.deinit();
|
||||
var hi = req.iterateHeaders();
|
||||
while (hi.next()) |h| try request_headers.append(h);
|
||||
|
||||
var response = interface.Response.init(aa);
|
||||
defer response.deinit();
|
||||
response.request.headers = request_headers.items;
|
||||
response.request.headers_owned = false;
|
||||
response.request.target = req.head.target;
|
||||
response.request.method = req.head.method;
|
||||
response.headers = &.{};
|
||||
response.headers_owned = false;
|
||||
|
||||
var respond_options = std.http.Server.Request.RespondOptions{};
|
||||
const response_bytes = event_handler(aa, request_body, &response) catch |e| brk: {
|
||||
respond_options.status = response.status;
|
||||
respond_options.reason = response.reason;
|
||||
if (respond_options.status.class() == .success) {
|
||||
respond_options.status = .internal_server_error;
|
||||
respond_options.reason = null;
|
||||
response.body.items = "";
|
||||
}
|
||||
// TODO: stream body to client? or keep internal?
|
||||
// TODO: more about this particular request
|
||||
log.err("Unexpected error from executor processing request: {any}", .{e});
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpStackTrace(trace.*);
|
||||
}
|
||||
break :brk "Unexpected error generating request to lambda";
|
||||
};
|
||||
|
||||
const final_response = try std.mem.concat(aa, u8, &[_][]const u8{ response.body.items, response_bytes });
|
||||
try req.respond(final_response, respond_options);
|
||||
}
|
||||
|
||||
var server_ready = false;
|
||||
var remaining_requests: ?usize = null;
|
||||
var server_url: ["http://127.0.0.1:99999".len]u8 = undefined;
|
||||
var bytes_allocated: usize = 0;
|
||||
|
||||
fn testRequest(method: std.http.Method, target: []const u8, event_handler: interface.HandlerFn) !void {
|
||||
remaining_requests = 1;
|
||||
defer remaining_requests = null;
|
||||
const server_thread = try std.Thread.spawn(
|
||||
.{},
|
||||
runStandaloneServer,
|
||||
.{ null, event_handler, 0 },
|
||||
);
|
||||
var client = std.http.Client{ .allocator = std.testing.allocator };
|
||||
defer client.deinit();
|
||||
defer server_ready = false;
|
||||
while (!server_ready) std.time.sleep(1);
|
||||
|
||||
const url = try std.mem.concat(std.testing.allocator, u8, &[_][]const u8{ server_url[0..], target });
|
||||
defer std.testing.allocator.free(url);
|
||||
log.debug("fetch from url: {s}", .{url});
|
||||
|
||||
var response_data = std.ArrayList(u8).init(std.testing.allocator);
|
||||
defer response_data.deinit();
|
||||
|
||||
const resp = try client.fetch(.{
|
||||
.response_storage = .{ .dynamic = &response_data },
|
||||
.method = method,
|
||||
.location = .{ .url = url },
|
||||
});
|
||||
|
||||
server_thread.join();
|
||||
log.debug("Bytes allocated during request: {d}", .{bytes_allocated});
|
||||
log.debug("Response status: {}", .{resp.status});
|
||||
log.debug("Response: {s}", .{response_data.items});
|
||||
}
|
||||
|
||||
fn testGet(comptime path: []const u8, event_handler: interface.HandlerFn) !void {
|
||||
try testRequest(.GET, path, event_handler);
|
||||
}
|
||||
test "can make a request" {
|
||||
// std.testing.log_level = .debug;
|
||||
if (@import("builtin").os.tag == .wasi) return error.SkipZigTest;
|
||||
const HandlerClosure = struct {
|
||||
var data_received: []const u8 = undefined;
|
||||
var context_received: interface.Context = undefined;
|
||||
const Self = @This();
|
||||
pub fn handler(allocator: std.mem.Allocator, event_data: []const u8, context: interface.Context) ![]const u8 {
|
||||
_ = allocator;
|
||||
data_received = event_data;
|
||||
context_received = context;
|
||||
return "success";
|
||||
}
|
||||
};
|
||||
try testGet("/", HandlerClosure.handler);
|
||||
}
|
|
@ -5,7 +5,7 @@ const builtin = @import("builtin");
|
|||
///
|
||||
/// * standalone_server: This will run the handler as a standalone web server
|
||||
///
|
||||
pub fn configureBuild(b: *std.build.Builder, exe: *std.Build.Step.Compile) !void {
|
||||
pub fn configureBuild(b: *std.Build, exe: *std.Build.Step.Compile) !void {
|
||||
_ = exe;
|
||||
// We don't actually need to do much here. Basically we need a dummy step,
|
||||
// but one which the user will select, which will allow our options mechanism
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
const std = @import("std");
|
||||
const build_options = @import("universal_lambda_build_options");
|
||||
const flexilib = @import("flexilib-interface");
|
||||
const std = @import("std");
|
||||
const interface = @import("universal_lambda_interface");
|
||||
|
||||
const log = std.log.scoped(.universal_lambda);
|
||||
|
||||
const runFn = blk: {
|
||||
switch (build_options.build_type) {
|
||||
.awslambda => break :blk @import("lambda.zig").run,
|
||||
.standalone_server => break :blk runStandaloneServerParent,
|
||||
.awslambda => break :blk @import("awslambda.zig").run,
|
||||
.standalone_server => break :blk @import("standalone_server.zig").runStandaloneServerParent,
|
||||
// In the case of flexilib, our root module is actually flexilib.zig
|
||||
// so we need to import that, otherwise we risk the dreaded "file exists
|
||||
// in multiple modules" problem
|
||||
|
@ -27,163 +26,11 @@ pub fn run(allocator: ?std.mem.Allocator, event_handler: interface.HandlerFn) !u
|
|||
return try runFn(allocator, event_handler);
|
||||
}
|
||||
|
||||
/// We need to create a child process to be able to deal with panics appropriately
|
||||
fn runStandaloneServerParent(allocator: ?std.mem.Allocator, event_handler: interface.HandlerFn) !u8 {
|
||||
const alloc = allocator orelse std.heap.page_allocator;
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(alloc);
|
||||
defer arena.deinit();
|
||||
|
||||
var aa = arena.allocator();
|
||||
var al = std.ArrayList([]const u8).init(aa);
|
||||
defer al.deinit();
|
||||
var argi = std.process.args();
|
||||
// We do this first so it shows more prominently when looking at processes
|
||||
// Also it will be slightly faster for whatever that is worth
|
||||
const child_arg = "--child_of_standalone_server";
|
||||
if (argi.next()) |a| try al.append(a);
|
||||
try al.append(child_arg);
|
||||
while (argi.next()) |a| {
|
||||
if (std.mem.eql(u8, child_arg, a)) {
|
||||
// This should never actually return
|
||||
return try runStandaloneServer(allocator, event_handler);
|
||||
}
|
||||
try al.append(a);
|
||||
}
|
||||
// Parent
|
||||
const stdin = std.io.getStdIn();
|
||||
const stdout = std.io.getStdOut();
|
||||
const stderr = std.io.getStdErr();
|
||||
while (true) {
|
||||
var cp = std.ChildProcess.init(al.items, alloc);
|
||||
cp.stdin = stdin;
|
||||
cp.stdout = stdout;
|
||||
cp.stderr = stderr;
|
||||
_ = try cp.spawnAndWait();
|
||||
try stderr.writeAll("Caught abnormal process termination, relaunching server");
|
||||
}
|
||||
}
|
||||
|
||||
/// Will create a web server and marshall all requests back to our event handler
|
||||
/// To keep things simple, we'll have this on a single thread, at least for now
|
||||
fn runStandaloneServer(allocator: ?std.mem.Allocator, event_handler: interface.HandlerFn) !u8 {
|
||||
const alloc = allocator orelse std.heap.page_allocator;
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(alloc);
|
||||
defer arena.deinit();
|
||||
|
||||
var aa = arena.allocator();
|
||||
var server = std.http.Server.init(aa, .{ .reuse_address = true });
|
||||
defer server.deinit();
|
||||
const address = try std.net.Address.parseIp("127.0.0.1", 8080); // TODO: allow config
|
||||
try server.listen(address);
|
||||
const server_port = server.socket.listen_address.in.getPort();
|
||||
var uri: ["http://127.0.0.1:99999".len]u8 = undefined;
|
||||
_ = try std.fmt.bufPrint(&uri, "http://127.0.0.1:{d}", .{server_port});
|
||||
log.info("server listening at {s}", .{uri});
|
||||
|
||||
// No threads, maybe later
|
||||
//log.info("starting server thread, tid {d}", .{std.Thread.getCurrentId()});
|
||||
while (true) {
|
||||
defer {
|
||||
if (!arena.reset(.{ .retain_with_limit = 1024 * 1024 })) {
|
||||
// reallocation failed, arena is degraded
|
||||
log.warn("Arena reset failed and is degraded. Resetting arena", .{});
|
||||
arena.deinit();
|
||||
arena = std.heap.ArenaAllocator.init(alloc);
|
||||
aa = arena.allocator();
|
||||
}
|
||||
}
|
||||
const builtin = @import("builtin");
|
||||
const supports_getrusage = builtin.os.tag != .windows and @hasDecl(std.os.system, "rusage"); // Is Windows it?
|
||||
var rss: if (supports_getrusage) std.os.rusage else void = undefined;
|
||||
if (supports_getrusage and builtin.mode == .Debug)
|
||||
rss = std.os.getrusage(std.os.rusage.SELF);
|
||||
processRequest(aa, &server, event_handler) catch |e| {
|
||||
log.err("Unexpected error processing request: {any}", .{e});
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpStackTrace(trace.*);
|
||||
}
|
||||
};
|
||||
if (supports_getrusage and builtin.mode == .Debug) { // and debug mode) {
|
||||
const rusage = std.os.getrusage(std.os.rusage.SELF);
|
||||
log.debug(
|
||||
"Request complete, max RSS of process: {d}M. Incremental: {d}K, User: {d}μs, System: {d}μs",
|
||||
.{
|
||||
@divTrunc(rusage.maxrss, 1024),
|
||||
rusage.maxrss - rss.maxrss,
|
||||
(rusage.utime.tv_sec - rss.utime.tv_sec) * std.time.us_per_s +
|
||||
rusage.utime.tv_usec - rss.utime.tv_usec,
|
||||
(rusage.stime.tv_sec - rss.stime.tv_sec) * std.time.us_per_s +
|
||||
rusage.stime.tv_usec - rss.stime.tv_usec,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn processRequest(aa: std.mem.Allocator, server: *std.http.Server, event_handler: interface.HandlerFn) !void {
|
||||
var res = try server.accept(.{ .allocator = aa });
|
||||
defer {
|
||||
_ = res.reset();
|
||||
if (res.headers.owned and res.headers.list.items.len > 0) res.headers.deinit();
|
||||
res.deinit();
|
||||
}
|
||||
try res.wait(); // wait for client to send a complete request head
|
||||
|
||||
const errstr = "Internal Server Error\n";
|
||||
var errbuf: [errstr.len]u8 = undefined;
|
||||
@memcpy(&errbuf, errstr);
|
||||
var response_bytes: []const u8 = errbuf[0..];
|
||||
|
||||
var body = if (res.request.content_length) |l|
|
||||
try res.reader().readAllAlloc(aa, @as(usize, @intCast(l)))
|
||||
else
|
||||
try aa.dupe(u8, "");
|
||||
// no need to free - will be handled by arena
|
||||
|
||||
var response = interface.Response.init(aa);
|
||||
defer response.deinit();
|
||||
response.request.headers = res.request.headers;
|
||||
response.request.headers_owned = false;
|
||||
response.request.target = res.request.target;
|
||||
response.request.method = res.request.method;
|
||||
response.headers = res.headers;
|
||||
response.headers_owned = false;
|
||||
|
||||
response_bytes = event_handler(aa, body, &response) catch |e| brk: {
|
||||
res.status = response.status;
|
||||
res.reason = response.reason;
|
||||
if (res.status.class() == .success) {
|
||||
res.status = .internal_server_error;
|
||||
res.reason = null;
|
||||
}
|
||||
// TODO: stream body to client? or keep internal?
|
||||
// TODO: more about this particular request
|
||||
log.err("Unexpected error from executor processing request: {any}", .{e});
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpStackTrace(trace.*);
|
||||
}
|
||||
break :brk "Unexpected error generating request to lambda";
|
||||
};
|
||||
res.transfer_encoding = .{ .content_length = response_bytes.len };
|
||||
|
||||
try res.do();
|
||||
_ = try res.writer().writeAll(response.body.items);
|
||||
_ = try res.writer().writeAll(response_bytes);
|
||||
try res.finish();
|
||||
}
|
||||
test {
|
||||
std.testing.refAllDecls(@This());
|
||||
// if (builtin.os.tag == .wasi) return error.SkipZigTest;
|
||||
if (@import("builtin").os.tag != .wasi) {
|
||||
// these use http
|
||||
std.testing.refAllDecls(@import("lambda.zig"));
|
||||
std.testing.refAllDecls(@import("cloudflaredeploy.zig"));
|
||||
std.testing.refAllDecls(@import("CloudflareDeployStep.zig"));
|
||||
}
|
||||
std.testing.refAllDecls(@import("console.zig"));
|
||||
std.testing.refAllDecls(@import("standalone_server.zig"));
|
||||
std.testing.refAllDecls(@import("awslambda.zig"));
|
||||
// By importing flexilib.zig, this breaks downstream any time someone
|
||||
// tries to build flexilib, because flexilib.zig becomes the root module,
|
||||
// then gets imported here again. It shouldn't be done unless doing
|
||||
|
@ -201,58 +48,3 @@ test {
|
|||
|
||||
// TODO: Do we want build files here too?
|
||||
}
|
||||
|
||||
fn testRequest(request_bytes: []const u8, event_handler: interface.HandlerFn) !void {
|
||||
const allocator = std.testing.allocator;
|
||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
var server = std.http.Server.init(allocator, .{ .reuse_address = true });
|
||||
defer server.deinit();
|
||||
|
||||
const address = try std.net.Address.parseIp("127.0.0.1", 0);
|
||||
try server.listen(address);
|
||||
const server_port = server.socket.listen_address.in.getPort();
|
||||
|
||||
var al = std.ArrayList(u8).init(allocator);
|
||||
defer al.deinit();
|
||||
var writer = al.writer();
|
||||
_ = writer;
|
||||
var aa = arena.allocator();
|
||||
var bytes_allocated: usize = 0;
|
||||
// pre-warm
|
||||
const server_thread = try std.Thread.spawn(
|
||||
.{},
|
||||
processRequest,
|
||||
.{ aa, &server, event_handler },
|
||||
);
|
||||
|
||||
const stream = try std.net.tcpConnectToHost(allocator, "127.0.0.1", server_port);
|
||||
defer stream.close();
|
||||
_ = try stream.writeAll(request_bytes[0..]);
|
||||
|
||||
server_thread.join();
|
||||
log.debug("Bytes allocated during request: {d}", .{arena.queryCapacity() - bytes_allocated});
|
||||
log.debug("Stdout: {s}", .{al.items});
|
||||
}
|
||||
|
||||
fn testGet(comptime path: []const u8, event_handler: interface.HandlerFn) !void {
|
||||
try testRequest("GET " ++ path ++ " HTTP/1.1\r\n" ++
|
||||
"Accept: */*\r\n" ++
|
||||
"\r\n", event_handler);
|
||||
}
|
||||
test "can make a request" {
|
||||
if (@import("builtin").os.tag == .wasi) return error.SkipZigTest;
|
||||
const HandlerClosure = struct {
|
||||
var data_received: []const u8 = undefined;
|
||||
var context_received: interface.Context = undefined;
|
||||
const Self = @This();
|
||||
pub fn handler(allocator: std.mem.Allocator, event_data: []const u8, context: interface.Context) ![]const u8 {
|
||||
_ = allocator;
|
||||
data_received = event_data;
|
||||
context_received = context;
|
||||
return "success";
|
||||
}
|
||||
};
|
||||
try testGet("/", HandlerClosure.handler);
|
||||
}
|
||||
|
|
|
@ -14,15 +14,15 @@ pub const BuildType = enum {
|
|||
|
||||
pub var module_root: ?[]const u8 = null;
|
||||
|
||||
pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile) !void {
|
||||
pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile, universal_lambda_zig_dep: *std.Build.Dependency) !void {
|
||||
const function_name = b.option([]const u8, "function-name", "Function name for Lambda [zig-fn]") orelse "zig-fn";
|
||||
|
||||
const file_location = try addModules(b, cs);
|
||||
// const file_location = try addModules(b, cs);
|
||||
|
||||
// Add steps
|
||||
try @import("lambda_build.zig").configureBuild(b, cs, function_name);
|
||||
try @import("cloudflare_build.zig").configureBuild(b, cs, function_name);
|
||||
try @import("flexilib_build.zig").configureBuild(b, cs, file_location);
|
||||
try @import("lambda-zig").configureBuild(b, cs, function_name);
|
||||
try @import("cloudflare-worker-deploy").configureBuild(b, cs, function_name);
|
||||
try @import("flexilib_build.zig").configureBuild(b, cs, universal_lambda_zig_dep);
|
||||
try @import("standalone_server_build.zig").configureBuild(b, cs);
|
||||
}
|
||||
|
||||
|
@ -33,127 +33,72 @@ pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile) !void {
|
|||
/// * universal_lambda_build_options
|
||||
/// * flexilib-interface
|
||||
/// * universal_lambda_handler
|
||||
pub fn addModules(b: *std.Build, cs: *std.Build.Step.Compile) ![]const u8 {
|
||||
const file_location = try findFileLocation(b);
|
||||
const options_module = try createOptionsModule(b, cs);
|
||||
|
||||
// We need to add the interface module here as well, so universal_lambda.zig
|
||||
// can reference it. Unfortunately, this creates an issue that the consuming
|
||||
// build.zig.zon must have flexilib included, even if they're not building
|
||||
// flexilib. TODO: Accept for now, but we need to think through this situation
|
||||
// This might be fixed in 0.12.0 (see https://github.com/ziglang/zig/issues/16172).
|
||||
// We can also possibly use the findFileLocation hack above in concert with
|
||||
// addAnonymousModule
|
||||
const flexilib_dep = b.dependency("flexilib", .{
|
||||
.target = cs.target,
|
||||
.optimize = cs.optimize,
|
||||
});
|
||||
const flexilib_module = flexilib_dep.module("flexilib-interface");
|
||||
// Make the interface available for consumption
|
||||
cs.addModule("flexilib-interface", flexilib_module);
|
||||
cs.addAnonymousModule("universal_lambda_interface", .{
|
||||
.source_file = .{ .path = b.pathJoin(&[_][]const u8{ file_location, "interface.zig" }) },
|
||||
// We alsso need the interface module available here
|
||||
.dependencies = &[_]std.Build.ModuleDependency{},
|
||||
});
|
||||
// Add module
|
||||
cs.addAnonymousModule("universal_lambda_handler", .{
|
||||
// Source file can be anywhere on disk, does not need to be a subdirectory
|
||||
.source_file = .{ .path = b.pathJoin(&[_][]const u8{ file_location, "universal_lambda.zig" }) },
|
||||
// We alsso need the interface module available here
|
||||
.dependencies = &[_]std.Build.ModuleDependency{
|
||||
// Add options module so we can let our universal_lambda know what
|
||||
// type of interface is necessary
|
||||
.{
|
||||
.name = "universal_lambda_build_options",
|
||||
.module = options_module,
|
||||
},
|
||||
.{
|
||||
.name = "flexilib-interface",
|
||||
.module = flexilib_module,
|
||||
},
|
||||
.{
|
||||
.name = "universal_lambda_interface",
|
||||
.module = cs.modules.get("universal_lambda_interface").?,
|
||||
},
|
||||
},
|
||||
});
|
||||
return file_location;
|
||||
}
|
||||
|
||||
/// This function relies on internal implementation of the build runner
|
||||
/// When a developer launches "zig build", a program is compiled, with the
|
||||
/// main entrypoint existing in build_runner.zig (this can be overridden by
|
||||
/// by command line).
|
||||
///
|
||||
/// The code we see in build.zig is compiled into that program. The program
|
||||
/// is named 'build' and stuck in the zig cache, then it is run. There are
|
||||
/// two phases to the build.
|
||||
///
|
||||
/// Build phase, where a graph is established of the steps that need to be run,
|
||||
/// then the "make phase", where the steps are actually executed. Steps have
|
||||
/// a make function that is called.
|
||||
///
|
||||
/// This function is reaching into the struct that is the build_runner.zig, and
|
||||
/// finding the location of the dependency for ourselves to determine the
|
||||
/// location of our own file. This is, of course, brittle, but there does not
|
||||
/// seem to be a better way at the moment, and we need this to be able to import
|
||||
/// modules appropriately.
|
||||
///
|
||||
/// For development of this process directly, we'll allow a build option to
|
||||
/// override this process completely, because during development it's easier
|
||||
/// for the example build.zig to simply import the file directly than it is
|
||||
/// to pull from a download location and update hashes every time we change
|
||||
fn findFileLocation(b: *std.Build) ![]const u8 {
|
||||
if (module_root) |r| return b.pathJoin(&[_][]const u8{ r, "src" });
|
||||
const build_root = b.option(
|
||||
[]const u8,
|
||||
"universal_lambda_build_root",
|
||||
"Build root for universal lambda (development of universal lambda only)",
|
||||
);
|
||||
if (build_root) |br| {
|
||||
return b.pathJoin(&[_][]const u8{ br, "src" });
|
||||
}
|
||||
// This is introduced post 0.11. Once it is available, we can skip the
|
||||
// access check, and instead check the end of the path matches the dependency
|
||||
// hash
|
||||
// for (b.available_deps) |dep| {
|
||||
// std.debug.print("{}", .{dep});
|
||||
// // if (std.
|
||||
// }
|
||||
const ulb_root = outer_blk: {
|
||||
// trigger initlialization if it hasn't been initialized already
|
||||
_ = b.dependency("universal_lambda_build", .{}); //b.args);
|
||||
var str_iterator = b.initialized_deps.iterator();
|
||||
while (str_iterator.next()) |entry| {
|
||||
const br = entry.key_ptr.*;
|
||||
const marker_found = blk: {
|
||||
// Check for a file that should only exist in our package
|
||||
std.fs.accessAbsolute(b.pathJoin(&[_][]const u8{ br, "src", "flexilib.zig" }), .{}) catch break :blk false;
|
||||
break :blk true;
|
||||
};
|
||||
if (marker_found) break :outer_blk br;
|
||||
}
|
||||
return error.CannotFindUniversalLambdaBuildRoot;
|
||||
pub fn addImports(b: *std.Build, cs: *std.Build.Step.Compile, universal_lambda_zig_dep: ?*std.Build.Dependency) void {
|
||||
const Modules = struct {
|
||||
aws_lambda_runtime: *std.Build.Module,
|
||||
flexilib_interface: *std.Build.Module,
|
||||
universal_lambda_interface: *std.Build.Module,
|
||||
universal_lambda_handler: *std.Build.Module,
|
||||
universal_lambda_build_options: *std.Build.Module,
|
||||
};
|
||||
return b.pathJoin(&[_][]const u8{ ulb_root, "src" });
|
||||
const modules =
|
||||
if (universal_lambda_zig_dep) |d|
|
||||
Modules{
|
||||
.aws_lambda_runtime = d.module("aws_lambda_runtime"),
|
||||
.flexilib_interface = d.module("flexilib-interface"),
|
||||
.universal_lambda_interface = d.module("universal_lambda_interface"),
|
||||
.universal_lambda_handler = d.module("universal_lambda_handler"),
|
||||
.universal_lambda_build_options = createOptionsModule(d.builder, cs),
|
||||
}
|
||||
else
|
||||
Modules{
|
||||
.aws_lambda_runtime = b.modules.get("aws_lambda_runtime").?,
|
||||
.flexilib_interface = b.modules.get("flexilib-interface").?,
|
||||
.universal_lambda_interface = b.modules.get("universal_lambda_interface").?,
|
||||
.universal_lambda_handler = b.modules.get("universal_lambda_handler").?,
|
||||
.universal_lambda_build_options = createOptionsModule(b, cs),
|
||||
};
|
||||
|
||||
cs.root_module.addImport("universal_lambda_build_options", modules.universal_lambda_build_options);
|
||||
cs.root_module.addImport("flexilib-interface", modules.flexilib_interface);
|
||||
cs.root_module.addImport("universal_lambda_interface", modules.universal_lambda_interface);
|
||||
cs.root_module.addImport("universal_lambda_handler", modules.universal_lambda_handler);
|
||||
cs.root_module.addImport("aws_lambda_runtime", modules.aws_lambda_runtime);
|
||||
|
||||
// universal lambda handler also needs these imports
|
||||
modules.universal_lambda_handler.addImport("universal_lambda_interface", modules.universal_lambda_interface);
|
||||
modules.universal_lambda_handler.addImport("flexilib-interface", modules.flexilib_interface);
|
||||
modules.universal_lambda_handler.addImport("universal_lambda_build_options", modules.universal_lambda_build_options);
|
||||
modules.universal_lambda_handler.addImport("aws_lambda_runtime", modules.aws_lambda_runtime);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// Make our target platform visible to runtime through an import
|
||||
/// called "universal_lambda_build_options". This will also be available to the consuming
|
||||
/// executable if needed
|
||||
pub fn createOptionsModule(b: *std.Build, cs: *std.Build.Step.Compile) !*std.Build.Module {
|
||||
pub fn createOptionsModule(b: *std.Build, cs: *std.Build.Step.Compile) *std.Build.Module {
|
||||
if (b.modules.get("universal_lambda_build_options")) |m| return m;
|
||||
|
||||
// We need to go through the command line args, look for argument(s)
|
||||
// between "build" and anything prefixed with "-". First take, blow up
|
||||
// if there is more than one. That's the step we're rolling with
|
||||
// These frameworks I believe are inextricably tied to both build and
|
||||
// run behavior.
|
||||
//
|
||||
var args = try std.process.argsAlloc(b.allocator);
|
||||
const args = std.process.argsAlloc(b.allocator) catch @panic("OOM");
|
||||
defer b.allocator.free(args);
|
||||
const options = b.addOptions();
|
||||
options.addOption(BuildType, "build_type", findBuildType(args) orelse .exe_run);
|
||||
cs.addOptions("universal_lambda_build_options", options);
|
||||
return cs.modules.get("universal_lambda_build_options").?;
|
||||
// The normal way to do this is with module.addOptions, but that actually just does
|
||||
// an import, even though the parameter there is "module_name". addImport takes
|
||||
// a module, but in zig 0.12.0, that's using options.createModule(), which creates
|
||||
// a private module. This is a good default for them, but doesn't work for us
|
||||
const module = b.addModule("universal_lambda_build_options", .{
|
||||
.root_source_file = options.getOutput(),
|
||||
});
|
||||
cs.root_module.addImport("universal_lambda_build_options", module);
|
||||
return module;
|
||||
}
|
||||
|
||||
fn findBuildType(build_args: [][:0]u8) ?BuildType {
|
||||
|
|
Loading…
Reference in New Issue
Block a user