@fireemu/darwin-arm64 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ """The HTTP server for `onRequest`, `onCall`, task queue and Auth blocking functions.
2
+
3
+ The daemon proxies `/<project>/<region>/<name>/...` here with the `x-fireemu-runner-secret` header
4
+ (`crates/fireemu-adapter-functions/src/http.rs`, `tools/runner-node/http-admission.mjs`). The
5
+ function sees the path below its mount point, as it does in production, and answers through the same
6
+ `flask.Request` handlers the SDK wraps. An unhandled error is functions-framework's crash response.
7
+ """
8
+
9
+ import contextlib
10
+ import hmac
11
+ import os
12
+ import sys
13
+
14
+ import flask
15
+ from functions_framework import crash_handler, read_request
16
+ from werkzeug.serving import WSGIRequestHandler, make_server
17
+
18
+ from .diagnostics import ASYNC_HANDLER, describe, is_async_handler
19
+ from .environment import EnvironmentGate
20
+ from .manifest import DEFAULT_REGION
21
+
22
+ SECRET_ENVIRON_KEY = "HTTP_X_FIREEMU_RUNNER_SECRET"
23
+ # Task queue and Auth blocking functions enter through the HTTP wrapper as well (the Node runner's `signatureType`).
24
+ HTTP_TRIGGERS = frozenset(("http", "tasks", "blockingAuth"))
25
+ _METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
26
+
27
+
28
+ def _flask_app() -> flask.Flask:
29
+ app = flask.Flask("fireemu-runner")
30
+ app.register_error_handler(500, crash_handler)
31
+ app.after_request(read_request)
32
+
33
+ def run(path):
34
+ return flask.request.environ["fireemu.function"](flask.request._get_current_object())
35
+
36
+ # "OPTIONS" is listed so that Flask does not answer a callable's preflight itself.
37
+ app.add_url_rule("/", endpoint="run", view_func=run, defaults={"path": ""}, methods=_METHODS)
38
+ app.add_url_rule("/<path:path>", endpoint="run", view_func=run, methods=_METHODS)
39
+ return app
40
+
41
+
42
+ class ResponseBodyFailed(Exception):
43
+ """A response body failed while it was being sent. The details were logged inside the function's environment."""
44
+
45
+
46
+ def _log_failure(name: str, error: BaseException) -> None:
47
+ diagnostic, _ = describe(error)
48
+ sys.stderr.write(f"[functions] {name}: {diagnostic[:8192]}\n")
49
+
50
+
51
+ class _HeldBody:
52
+ """A response body that keeps its function's environment until the body is finished, whatever happens to it.
53
+
54
+ Closing the body, and running the callbacks `Response.call_on_close` registered, is user code: if it raises the
55
+ gate must still be released, or the process is locked with a secret in its environment. Errors are described
56
+ here, while the environment is still held, and the server sees only a plain exception.
57
+ """
58
+
59
+ def __init__(self, body, held: contextlib.ExitStack, name: str):
60
+ self._body = body
61
+ self._iterator = iter(body)
62
+ self._held = held
63
+ self._name = name
64
+
65
+ def __iter__(self):
66
+ return self
67
+
68
+ def __next__(self):
69
+ try:
70
+ return next(self._iterator)
71
+ except StopIteration:
72
+ raise
73
+ except BaseException as error:
74
+ _log_failure(self._name, error)
75
+ raise ResponseBodyFailed(type(error).__name__) from None
76
+
77
+ def close(self):
78
+ try:
79
+ close = getattr(self._body, "close", None)
80
+ if close is not None:
81
+ close()
82
+ except BaseException as error: # the response has been sent; there is nobody to tell but the log
83
+ _log_failure(self._name, error)
84
+ finally:
85
+ self._held.close()
86
+
87
+
88
+ class RunnerRouter:
89
+ """Admission and routing in front of the Flask app."""
90
+
91
+ def __init__(self, functions: dict, manifest: dict, project: str, secret: str, gate: EnvironmentGate | None = None):
92
+ self._app = _flask_app()
93
+ self._gate = gate if gate is not None else EnvironmentGate({})
94
+ self._project = project
95
+ self._secret = secret.encode()
96
+ self._routes = {}
97
+ for spec in manifest["functions"]:
98
+ if spec["trigger"]["type"] in HTTP_TRIGGERS:
99
+ self._routes[spec["name"]] = (
100
+ spec.get("region", DEFAULT_REGION),
101
+ functions[spec["entryPoint"]],
102
+ spec["entryPoint"],
103
+ spec.get("platformOptions", {}).get("secrets"),
104
+ )
105
+
106
+ @staticmethod
107
+ def _plain(start_response, status: str, text: str):
108
+ body = text.encode()
109
+ start_response(status, [("Content-Type", "text/plain; charset=utf-8"), ("Content-Length", str(len(body)))])
110
+ return [body]
111
+
112
+ def __call__(self, environ, start_response):
113
+ presented = environ.pop(SECRET_ENVIRON_KEY, "").encode()
114
+ if not self._secret:
115
+ # A runner with no secret would accept any request without the header (an empty value equals an empty
116
+ # secret). The Node runner answers every request this way (`http-admission.mjs`).
117
+ return self._plain(start_response, "500 Internal Server Error", "FIREEMU_RUNNER_SECRET is required")
118
+ if not hmac.compare_digest(presented, self._secret):
119
+ return self._plain(start_response, "403 Forbidden", "forbidden")
120
+ parts = environ.get("PATH_INFO", "").split("/")
121
+ # ["", project, region, name, *rest]
122
+ if len(parts) < 4 or parts[0] != "" or parts[1] != self._project:
123
+ return self._plain(start_response, "404 Not Found", "no HTTP function")
124
+ region, name = parts[2], parts[3]
125
+ route = self._routes.get(name)
126
+ if route is None or route[0] != region:
127
+ return self._plain(start_response, "404 Not Found", f"no HTTP function {parts[1]}/{region}/{name}")
128
+ environ["SCRIPT_NAME"] = environ.get("SCRIPT_NAME", "") + "/".join(parts[:4])
129
+ environ["PATH_INFO"] = "/" + "/".join(parts[4:])
130
+ environ["fireemu.function"] = route[1]
131
+ if is_async_handler(route[1]):
132
+ # Production answers 500 as well (the coroutine is not a response); this says why.
133
+ sys.stderr.write(f"[functions] {name}: {ASYNC_HANDLER}\n")
134
+ return self._plain(start_response, "500 Internal Server Error", ASYNC_HANDLER)
135
+ # The environment this function is entitled to is held until its response is finished, a streamed body
136
+ # included: the secrets it declares are in the process environment for exactly that long.
137
+ held = contextlib.ExitStack()
138
+ held.enter_context(self._gate.hold(route[3]))
139
+ try:
140
+ # The variables a production instance has from its start name the function it serves. One process
141
+ # serves the whole codebase here, so they name the call that started most recently, as for events.
142
+ os.environ["FUNCTION_TARGET"] = route[2]
143
+ os.environ["FUNCTION_SIGNATURE_TYPE"] = "http"
144
+ os.environ["K_SERVICE"] = name
145
+ body = self._app(environ, start_response)
146
+ except BaseException as error:
147
+ _log_failure(name, error)
148
+ held.close()
149
+ raise ResponseBodyFailed(type(error).__name__) from None
150
+ return _HeldBody(body, held, name)
151
+
152
+
153
+ class _QuietHandler(WSGIRequestHandler):
154
+ def log_request(self, *args, **kwargs): # the daemon logs requests; the runner's stderr is for user output
155
+ pass
156
+
157
+
158
+ def start_server(functions: dict, manifest: dict, secret: str, gate: EnvironmentGate | None = None):
159
+ """Starts the server on a free loopback port; returns `(server, port)`."""
160
+ router = RunnerRouter(functions, manifest, os.environ.get("GCLOUD_PROJECT", ""), secret, gate)
161
+ server = make_server("127.0.0.1", 0, router, threaded=True, request_handler=_QuietHandler)
162
+ import threading
163
+
164
+ threading.Thread(target=server.serve_forever, daemon=True, name="http").start()
165
+ return server, server.server_port
@@ -0,0 +1,19 @@
1
+ """Entry point of the fireemu Python runner: `python runner.py --source <dir> --codebase <name>`.
2
+
3
+ It is started by the fireemu daemon with the interpreter of the codebase's `venv` and speaks the
4
+ frame protocol on stdin and stdout (see `fireemu_runner/framing.py`).
5
+ """
6
+
7
+ import os
8
+ import sys
9
+
10
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
11
+
12
+ from fireemu_runner.main import run # noqa: E402
13
+
14
+ if __name__ == "__main__":
15
+ code = run(sys.argv[1:])
16
+ sys.stdout.flush()
17
+ sys.stderr.flush()
18
+ # Handlers may have left non-daemon threads behind; the daemon already decided to stop us.
19
+ os._exit(code)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fireemu/darwin-arm64",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "The fireemu daemon for darwin-arm64",
5
5
  "homepage": "https://t-k.github.io/fireemu/",
6
6
  "repository": {