serverless-offline 14.6.0 → 14.7.1
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.
- package/README.md +41 -47
- package/package.json +13 -31
- package/src/config/commandOptions.js +5 -0
- package/src/config/defaultOptions.js +1 -0
- package/src/events/http/HttpServer.js +3 -7
- package/src/lambda/handler-runner/HandlerRunner.js +1 -1
- package/src/lambda/handler-runner/ruby-runner/RubyRunner.js +332 -49
- package/src/lambda/handler-runner/ruby-runner/invoke.rb +46 -34
- package/src/utils/generateHapiCookie.js +70 -0
- package/src/utils/index.js +1 -0
package/README.md
CHANGED
|
@@ -32,23 +32,14 @@
|
|
|
32
32
|
<a href="https://www.npmjs.com/package/serverless-offline">
|
|
33
33
|
<img src="https://img.shields.io/npm/v/serverless-offline.svg?style=flat-square">
|
|
34
34
|
</a>
|
|
35
|
-
<a href="https://github.com/dherault/serverless-offline/actions/workflows/integrate.yml">
|
|
36
|
-
<img src="https://img.shields.io/github/workflow/status/dherault/serverless-offline/Integrate">
|
|
37
|
-
</a>
|
|
38
35
|
<img src="https://img.shields.io/node/v/serverless-offline.svg?style=flat-square">
|
|
39
36
|
<a href="https://github.com/serverless/serverless">
|
|
40
37
|
<img src="https://img.shields.io/npm/dependency-version/serverless-offline/peer/serverless.svg?style=flat-square">
|
|
41
38
|
</a>
|
|
42
|
-
<a href="https://github.com/prettier/prettier">
|
|
43
|
-
<img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square">
|
|
44
|
-
</a>
|
|
45
39
|
<img src="https://img.shields.io/npm/l/serverless-offline.svg?style=flat-square">
|
|
46
40
|
<a href="#contributing">
|
|
47
41
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square">
|
|
48
42
|
</a>
|
|
49
|
-
<a href="https://gitter.im/serverless-offline/community">
|
|
50
|
-
<img src="https://badges.gitter.im/serverless-offline.png">
|
|
51
|
-
</a>
|
|
52
43
|
</p>
|
|
53
44
|
|
|
54
45
|
This [Serverless](https://github.com/serverless/serverless) plugin emulates [AWS λ](https://aws.amazon.com/lambda) and [API Gateway](https://aws.amazon.com/api-gateway) on your local machine to speed up your development cycles.
|
|
@@ -59,7 +50,7 @@ To do so, it starts an HTTP server that handles the request's lifecycle like API
|
|
|
59
50
|
- [Node.js](https://nodejs.org), [Python](https://www.python.org), [Ruby](https://www.ruby-lang.org), [Go](https://golang.org), [Java](https://www.java.com) (incl. [Kotlin](https://kotlinlang.org), [Groovy](https://groovy-lang.org), [Scala](https://www.scala-lang.org)) λ runtimes.
|
|
60
51
|
- Velocity templates support.
|
|
61
52
|
- Lazy loading of your handler files.
|
|
62
|
-
- And more: integrations, authorizers, proxies, timeouts, responseParameters, HTTPS, CORS, etc
|
|
53
|
+
- And more: integrations, authorizers, proxies, timeouts, responseParameters, HTTPS, CORS, etc.
|
|
63
54
|
|
|
64
55
|
This plugin is updated by its users, I just do maintenance and ensure that PRs are relevant to the community. In other words, if you [find a bug or want a new feature](https://github.com/dherault/serverless-offline/issues), please help us by becoming one of the [contributors](https://github.com/dherault/serverless-offline/graphs/contributors) :v: ! See the [contributing section](#contributing).
|
|
65
56
|
|
|
@@ -104,7 +95,7 @@ First, add Serverless Offline to your project:
|
|
|
104
95
|
|
|
105
96
|
`npm install serverless-offline --save-dev`
|
|
106
97
|
|
|
107
|
-
Then inside your project's `serverless.yml` file add following entry to the plugins section: `serverless-offline`. If there is no plugin section you will need to add it to the file.
|
|
98
|
+
Then inside your project's `serverless.yml` file add the following entry to the plugins section: `serverless-offline`. If there is no plugin section you will need to add it to the file.
|
|
108
99
|
|
|
109
100
|
**Note that the "plugin" section for serverless-offline must be at root level on serverless.yml.**
|
|
110
101
|
|
|
@@ -119,15 +110,15 @@ You can check whether you have successfully installed the plugin by running the
|
|
|
119
110
|
|
|
120
111
|
`serverless --verbose`
|
|
121
112
|
|
|
122
|
-
|
|
113
|
+
The console should display _Offline_ as one of the plugins now available in your Serverless project.
|
|
123
114
|
|
|
124
115
|
## Usage and command line options
|
|
125
116
|
|
|
126
|
-
In your project root run:
|
|
117
|
+
In your project root, run:
|
|
127
118
|
|
|
128
119
|
`serverless offline` or `sls offline`.
|
|
129
120
|
|
|
130
|
-
|
|
121
|
+
To list all the options for the plugin, run:
|
|
131
122
|
|
|
132
123
|
`sls offline --help`
|
|
133
124
|
|
|
@@ -145,8 +136,8 @@ Default: '\*'
|
|
|
145
136
|
|
|
146
137
|
#### corsDisallowCredentials
|
|
147
138
|
|
|
148
|
-
When provided, the
|
|
149
|
-
Default: true
|
|
139
|
+
When provided, sets the Access-Control-Allow-Credentials header to 'false'.\
|
|
140
|
+
Default (without flag): credentials are allowed ('true')
|
|
150
141
|
|
|
151
142
|
#### corsExposedHeaders
|
|
152
143
|
|
|
@@ -181,7 +172,7 @@ Enforce secure cookies
|
|
|
181
172
|
|
|
182
173
|
#### host
|
|
183
174
|
|
|
184
|
-
|
|
175
|
+
Host name to listen on.<br />
|
|
185
176
|
Default: localhost
|
|
186
177
|
|
|
187
178
|
#### httpPort
|
|
@@ -191,7 +182,7 @@ Default: 3000
|
|
|
191
182
|
|
|
192
183
|
#### httpsProtocol
|
|
193
184
|
|
|
194
|
-
|
|
185
|
+
To enable HTTPS, specify directory (relative to your cwd, typically your project dir) for both cert.pem and key.pem files.
|
|
195
186
|
|
|
196
187
|
#### ignoreJWTSignature
|
|
197
188
|
|
|
@@ -205,7 +196,7 @@ Default: 3002
|
|
|
205
196
|
#### layersDir
|
|
206
197
|
|
|
207
198
|
The directory layers should be stored in.<br />
|
|
208
|
-
Default: ${codeDir}/.serverless-offline/layers'
|
|
199
|
+
Default: '${codeDir}/.serverless-offline/layers'
|
|
209
200
|
|
|
210
201
|
#### localEnvironment
|
|
211
202
|
|
|
@@ -226,17 +217,21 @@ Remove sponsor message from the output.
|
|
|
226
217
|
|
|
227
218
|
#### noTimeout
|
|
228
219
|
|
|
229
|
-
|
|
220
|
+
Disables the timeout feature.
|
|
230
221
|
|
|
231
222
|
#### prefix
|
|
232
223
|
|
|
233
|
-
|
|
224
|
+
Adds a prefix to every path, to send your requests to http://localhost:3000/[prefix]/[your_path] instead.<br />
|
|
234
225
|
Default: ''
|
|
235
226
|
|
|
236
227
|
#### reloadHandler
|
|
237
228
|
|
|
238
229
|
Reloads handler with each request.
|
|
239
230
|
|
|
231
|
+
#### rubyWatchDirs
|
|
232
|
+
|
|
233
|
+
List of directories to watch for Ruby file changes. Automatically restarts the Ruby process on change, enabling hot-reload during local development.
|
|
234
|
+
|
|
240
235
|
#### resourceRoutes
|
|
241
236
|
|
|
242
237
|
Turns on loading of your HTTP proxy settings from serverless.yml.
|
|
@@ -280,7 +275,7 @@ custom:
|
|
|
280
275
|
serverless-offline:
|
|
281
276
|
httpsProtocol: 'dev-certs'
|
|
282
277
|
httpPort: 4000
|
|
283
|
-
|
|
278
|
+
foo: 'bar'
|
|
284
279
|
```
|
|
285
280
|
|
|
286
281
|
Options passed on the command line override YAML options.
|
|
@@ -296,7 +291,7 @@ By default you can send your requests to `http://localhost:3000/`. Please note t
|
|
|
296
291
|
|
|
297
292
|
### node.js
|
|
298
293
|
|
|
299
|
-
Lambda handlers with `serverless-offline` for the `node.js` runtime can run in different execution modes and have some differences with a variety of pros and cons.
|
|
294
|
+
Lambda handlers with `serverless-offline` for the `node.js` runtime can run in different execution modes and have some differences with a variety of pros and cons. They are currently mutually exclusive and it's not possible to use a combination, e.g. use `in-process` for one Lambda, and `worker-threads` for another. It is planned to combine the flags into one single flag in the future and also add support for combining run modes.
|
|
300
295
|
|
|
301
296
|
#### worker-threads (default)
|
|
302
297
|
|
|
@@ -331,7 +326,7 @@ NOTE:
|
|
|
331
326
|
|
|
332
327
|
### Python, Ruby, Go, Java (incl. Kotlin, Groovy, Scala)
|
|
333
328
|
|
|
334
|
-
|
|
329
|
+
The Lambda handler process is running in a child process.
|
|
335
330
|
|
|
336
331
|
## Invoke Lambda
|
|
337
332
|
|
|
@@ -389,7 +384,7 @@ export async function handler() {
|
|
|
389
384
|
}
|
|
390
385
|
```
|
|
391
386
|
|
|
392
|
-
You can also invoke using the
|
|
387
|
+
You can also invoke using the AWS CLI by specifying `--endpoint-url`
|
|
393
388
|
|
|
394
389
|
```
|
|
395
390
|
aws lambda invoke /dev/null \
|
|
@@ -415,7 +410,7 @@ offline: Function names exposed for local invocation by aws-sdk:
|
|
|
415
410
|
```
|
|
416
411
|
|
|
417
412
|
To list the available manual invocation paths exposed for targeting
|
|
418
|
-
by `aws-sdk` and `aws-cli`, use `SLS_DEBUG=*` with `serverless offline`. After the invoke server starts up, full list of endpoints will be displayed:
|
|
413
|
+
by `aws-sdk` and `aws-cli`, use `SLS_DEBUG=*` with `serverless offline`. After the invoke server starts up, the full list of endpoints will be displayed:
|
|
419
414
|
|
|
420
415
|
```
|
|
421
416
|
SLS_DEBUG=* serverless offline
|
|
@@ -433,7 +428,7 @@ offline: Function names exposed for local invocation by aws-sdk:
|
|
|
433
428
|
|
|
434
429
|
You can manually target these endpoints with a REST client to debug your lambda
|
|
435
430
|
function if you want to. Your `POST` JSON body will be the `Payload` passed to your function if you were
|
|
436
|
-
to
|
|
431
|
+
to call it via `aws-sdk`.
|
|
437
432
|
|
|
438
433
|
## The `process.env.IS_OFFLINE` variable
|
|
439
434
|
|
|
@@ -441,7 +436,7 @@ Will be `"true"` in your handlers when using `serverless-offline`.
|
|
|
441
436
|
|
|
442
437
|
## Docker and Layers
|
|
443
438
|
|
|
444
|
-
To use layers with serverless-offline, you need to have the `useDocker` option set to true. This can either
|
|
439
|
+
To use layers with serverless-offline, you need to have the `useDocker` option set to true. This can be done either by using the `--useDocker` command, or in your serverless.yml like this:
|
|
445
440
|
|
|
446
441
|
```yml
|
|
447
442
|
custom:
|
|
@@ -453,7 +448,7 @@ This will allow the docker container to look up any information about layers, do
|
|
|
453
448
|
|
|
454
449
|
- AWS as a provider, it won't work with other provider types.
|
|
455
450
|
- Layers that are compatible with your runtime.
|
|
456
|
-
- ARNs for layers. Local layers
|
|
451
|
+
- ARNs for layers. Local layers are not yet supported.
|
|
457
452
|
- A local AWS account set-up that can query and download layers.
|
|
458
453
|
|
|
459
454
|
If you're using least-privilege principals for your AWS roles, this policy should get you by:
|
|
@@ -471,7 +466,7 @@ If you're using least-privilege principals for your AWS roles, this policy shoul
|
|
|
471
466
|
}
|
|
472
467
|
```
|
|
473
468
|
|
|
474
|
-
Once you run a function that boots up the Docker container, it'll look through the layers for that function, download them in order to your layers folder, and save a hash of your layers so it can be re-used in future. You'll only need to re-download your layers if they change in the future. If you want your layers to re-download, simply remove your layers folder.
|
|
469
|
+
Once you run a function that boots up the Docker container, it'll look through the layers for that function, download them in order to your layers folder, and save a hash of your layers so it can be re-used in the future. You'll only need to re-download your layers if they change in the future. If you want your layers to re-download, simply remove your layers folder.
|
|
475
470
|
|
|
476
471
|
You should then be able to invoke functions as normal, and they're executed against the layers in your docker container.
|
|
477
472
|
|
|
@@ -541,7 +536,7 @@ The plugin only supports retrieving Tokens from headers. You can configure the h
|
|
|
541
536
|
|
|
542
537
|
### Remote authorizers
|
|
543
538
|
|
|
544
|
-
You are able to mock the response from remote authorizers by setting the
|
|
539
|
+
You are able to mock the response from remote authorizers by setting the environment variable `AUTHORIZER` before running `sls offline start`
|
|
545
540
|
|
|
546
541
|
Example:
|
|
547
542
|
|
|
@@ -566,7 +561,7 @@ offline:
|
|
|
566
561
|
```
|
|
567
562
|
|
|
568
563
|
```js
|
|
569
|
-
// ./path/to/
|
|
564
|
+
// ./path/to/custom-authentication-provider.js
|
|
570
565
|
|
|
571
566
|
module.exports = function (endpoint, functionKey, method, path) {
|
|
572
567
|
return {
|
|
@@ -584,7 +579,7 @@ module.exports = function (endpoint, functionKey, method, path) {
|
|
|
584
579
|
}
|
|
585
580
|
```
|
|
586
581
|
|
|
587
|
-
A working example of injecting a custom authorization provider can be found in the
|
|
582
|
+
A working example of injecting a custom authorization provider can be found in the project's integration tests under the folder [`custom-authentication`](./tests/integration/custom-authentication).
|
|
588
583
|
|
|
589
584
|
## Custom headers
|
|
590
585
|
|
|
@@ -622,7 +617,7 @@ You can use [serverless-dotenv-plugin](https://github.com/colynb/serverless-dote
|
|
|
622
617
|
[Serverless doc](https://serverless.com/framework/docs/providers/aws/events/apigateway#request-templates)
|
|
623
618
|
~ [AWS doc](http://docs.aws.amazon.com/apigateway/latest/developerguide/models-mappings.html#models-mappings-mappings)
|
|
624
619
|
|
|
625
|
-
You can supply response and request templates for each function. This is optional. To do so you will have to place function
|
|
620
|
+
You can supply response and request templates for each function. This is optional. To do so, you will have to place function-specific template files in the same directory as your function file and add the .req.vm extension to the template filename.
|
|
626
621
|
For example,
|
|
627
622
|
if your function is in code-file: `helloworld.js`,
|
|
628
623
|
your response template should be in file: `helloworld.res.vm` and your request template in file `helloworld.req.vm`.
|
|
@@ -776,17 +771,17 @@ apiGatewayManagementApi.postToConnection({
|
|
|
776
771
|
|
|
777
772
|
Where the `event` is received in the lambda handler function.
|
|
778
773
|
|
|
779
|
-
There's support for [websocketsApiRouteSelectionExpression](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-selection-expressions.html) in
|
|
774
|
+
There's support for [websocketsApiRouteSelectionExpression](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-selection-expressions.html) in its basic form: `$request.body.x.y.z`, where the default value is `$request.body.action`.
|
|
780
775
|
|
|
781
776
|
## Debug process
|
|
782
777
|
|
|
783
|
-
Serverless offline plugin will respond to the overall framework settings and output additional information to the console in debug mode. In order to do this you will have to set the `SLS_DEBUG` environmental variable. You can run the following in the command line to switch to debug mode execution.
|
|
778
|
+
The Serverless offline plugin will respond to the overall framework settings and output additional information to the console in debug mode. In order to do this you will have to set the `SLS_DEBUG` environmental variable. You can run the following in the command line to switch to debug mode execution.
|
|
784
779
|
|
|
785
780
|
> Unix: `export SLS_DEBUG=*`
|
|
786
781
|
|
|
787
782
|
> Windows: `SET SLS_DEBUG=*`
|
|
788
783
|
|
|
789
|
-
Interactive debugging is also possible for your project if you have installed the node-inspector module and
|
|
784
|
+
Interactive debugging is also possible for your project if you have installed the node-inspector module and Chrome browser. You can then run the following command line inside your project's root.
|
|
790
785
|
|
|
791
786
|
Initial installation:
|
|
792
787
|
`npm install -g node-inspector`
|
|
@@ -800,7 +795,7 @@ Depending on the breakpoint, you may need to call the URL path for your function
|
|
|
800
795
|
|
|
801
796
|
### Interactive Debugging with Visual Studio Code (VSC)
|
|
802
797
|
|
|
803
|
-
With newer versions of
|
|
798
|
+
With newer versions of Node.js (6.3+) the node inspector is already part of your Node.js environment and you can take advantage of debugging inside your IDE with source-map support. Here is the example configuration to debug interactively with VSC. It has two steps.
|
|
804
799
|
|
|
805
800
|
#### Step 1 : Adding a launch configuration in IDE
|
|
806
801
|
|
|
@@ -818,9 +813,9 @@ Add a new [launch configuration](https://code.visualstudio.com/docs/editor/debug
|
|
|
818
813
|
}
|
|
819
814
|
```
|
|
820
815
|
|
|
821
|
-
####
|
|
816
|
+
#### Step 2: Adding a debug script
|
|
822
817
|
|
|
823
|
-
You will also need to add a `debug` script reference in your `package.json file
|
|
818
|
+
You will also need to add a `debug` script reference in your `package.json` file
|
|
824
819
|
|
|
825
820
|
Add this to the `scripts` section:
|
|
826
821
|
|
|
@@ -837,13 +832,13 @@ Example:
|
|
|
837
832
|
}
|
|
838
833
|
```
|
|
839
834
|
|
|
840
|
-
In VSC, you can, then, add breakpoints to your code. To start a debug
|
|
835
|
+
In VSC, you can, then, add breakpoints to your code. To start a debug session you can either start your script in `package.json` by clicking the hovering debug intellisense icon or by going to your debug pane and selecting the Debug Serverless Offline configuration.
|
|
841
836
|
|
|
842
837
|
## Resource permissions and AWS profile
|
|
843
838
|
|
|
844
|
-
Lambda functions assume an IAM role during execution: the framework creates this role and
|
|
839
|
+
Lambda functions assume an IAM role during execution: the framework creates this role and sets all the permissions provided in the `iamRoleStatements` section of `serverless.yml`.
|
|
845
840
|
|
|
846
|
-
However, serverless offline makes use of your local AWS profile credentials to run the lambda functions and that might result in a different set of permissions. By default, the aws-sdk would load credentials for
|
|
841
|
+
However, serverless offline makes use of your local AWS profile credentials to run the lambda functions and that might result in a different set of permissions. By default, the aws-sdk would load credentials for your default AWS profile specified in your configuration file.
|
|
847
842
|
|
|
848
843
|
You can change this profile directly in the code or by setting proper environment variables. Setting the `AWS_PROFILE` environment variable before calling `serverless` offline to a different profile would effectively change the credentials, e.g.
|
|
849
844
|
|
|
@@ -852,13 +847,13 @@ You can change this profile directly in the code or by setting proper environmen
|
|
|
852
847
|
## Simulation quality
|
|
853
848
|
|
|
854
849
|
This plugin simulates API Gateway for many practical purposes, good enough for development - but is not a perfect simulator.
|
|
855
|
-
Specifically, Lambda currently runs on Node.js
|
|
850
|
+
Specifically, Lambda currently runs on Node.js v18.x, v20.x and v22.x ([AWS Docs](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html)), whereas _Offline_ runs on your own runtime where no memory limits are enforced.
|
|
856
851
|
|
|
857
852
|
## Usage with other plugins
|
|
858
853
|
|
|
859
854
|
When combining this plugin with other plugins there are a few things that you need to keep in mind.
|
|
860
855
|
|
|
861
|
-
You should run `serverless offline start` instead of `serverless offline`. The `start` command fires the `offline:start:init` and `offline:start:end` lifecycle hooks which can be used by other plugins to process your code, add resources, perform cleanups, etc.
|
|
856
|
+
You should run `serverless offline start` instead of `serverless offline`. The `start` command fires the `offline:start:init` and `offline:start:end` lifecycle hooks, which can be used by other plugins to process your code, add resources, perform cleanups, etc.
|
|
862
857
|
|
|
863
858
|
The order in which plugins are added to `serverless.yml` is relevant.
|
|
864
859
|
Plugins are executed in order, so plugins that process your code or add resources should be added first so they are ready when this plugin starts.
|
|
@@ -873,7 +868,7 @@ plugins:
|
|
|
873
868
|
- serverless-offline # runs last so your code has been pre-processed and dynamo is ready
|
|
874
869
|
```
|
|
875
870
|
|
|
876
|
-
That works because all those plugins listen to the `offline:start:init` to do their processing.
|
|
871
|
+
That works because all those plugins listen to the `offline:start:init` hook to do their processing.
|
|
877
872
|
Similarly they listen to `offline:start:end` to perform cleanup (stop dynamo db, remove temporary files, etc).
|
|
878
873
|
|
|
879
874
|
## Credits and inspiration
|
|
@@ -889,7 +884,6 @@ MIT
|
|
|
889
884
|
Yes, thank you!
|
|
890
885
|
This plugin is community-driven, most of its features are from different authors.
|
|
891
886
|
Please update the docs and tests and add your name to the package.json file.
|
|
892
|
-
We try to follow [Airbnb's JavaScript Style Guide](https://github.com/airbnb/javascript).
|
|
893
887
|
|
|
894
888
|
## Contributors
|
|
895
889
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serverless-offline",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.7.1",
|
|
4
4
|
"description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"exports": {
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"lint": "eslint .",
|
|
15
15
|
"lint:fix": "eslint . --fix",
|
|
16
16
|
"list-contributors": "echo 'clone https://github.com/mgechev/github-contributors-list.git first, then run npm install' && cd ../github-contributors-list && node bin/githubcontrib --owner dherault --repo serverless-offline --sortBy contributions --sortOrder desc > contributors.md",
|
|
17
|
-
"prepare-release": "commit-and-tag-version && prettier --write CHANGELOG.md",
|
|
18
17
|
"prepublishOnly": "npm run lint",
|
|
18
|
+
"version": "auto-changelog -p && git add CHANGELOG.md",
|
|
19
19
|
"prettier": "prettier --check .",
|
|
20
20
|
"prettier:fix": "prettier --write .",
|
|
21
21
|
"test": "mocha --require ./tests/mochaHooks.cjs",
|
|
@@ -51,32 +51,14 @@
|
|
|
51
51
|
"engines": {
|
|
52
52
|
"node": ">=20.0.0"
|
|
53
53
|
},
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"types": [
|
|
60
|
-
{
|
|
61
|
-
"type": "feat",
|
|
62
|
-
"section": "Features"
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
"type": "fix",
|
|
66
|
-
"section": "Bug Fixes"
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
"type": "perf",
|
|
70
|
-
"section": "Performance Improvements"
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
"type": "refactor",
|
|
74
|
-
"section": "Maintenance Improvements"
|
|
75
|
-
}
|
|
76
|
-
]
|
|
54
|
+
"auto-changelog": {
|
|
55
|
+
"output": "CHANGELOG.md",
|
|
56
|
+
"unreleased": false,
|
|
57
|
+
"commitLimit": false,
|
|
58
|
+
"hideCredit": true
|
|
77
59
|
},
|
|
78
60
|
"dependencies": {
|
|
79
|
-
"@aws-sdk/client-lambda": "^3.
|
|
61
|
+
"@aws-sdk/client-lambda": "^3.1052.0",
|
|
80
62
|
"@hapi/boom": "^10.0.1",
|
|
81
63
|
"@hapi/h2o2": "^10.0.4",
|
|
82
64
|
"@hapi/hapi": "^21.4.9",
|
|
@@ -98,24 +80,24 @@
|
|
|
98
80
|
"node-schedule": "^2.1.1",
|
|
99
81
|
"p-memoize": "^7.1.1",
|
|
100
82
|
"tree-kill": "^1.2.2",
|
|
101
|
-
"tsx": "^4.
|
|
83
|
+
"tsx": "^4.22.3",
|
|
102
84
|
"velocityjs": "^2.1.6",
|
|
103
|
-
"ws": "^8.
|
|
85
|
+
"ws": "^8.21.0"
|
|
104
86
|
},
|
|
105
87
|
"devDependencies": {
|
|
106
88
|
"@istanbuljs/esm-loader-hook": "^0.3.0",
|
|
107
89
|
"archiver": "^7.0.1",
|
|
108
|
-
"
|
|
90
|
+
"auto-changelog": "^2.5.1",
|
|
109
91
|
"eslint": "^8.57.0",
|
|
110
92
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
111
93
|
"eslint-config-prettier": "^9.1.0",
|
|
112
94
|
"eslint-plugin-import": "^2.32.0",
|
|
113
95
|
"eslint-plugin-prettier": "^5.5.5",
|
|
114
96
|
"eslint-plugin-unicorn": "^54.0.0",
|
|
115
|
-
"mocha": "^
|
|
97
|
+
"mocha": "^11.7.6",
|
|
116
98
|
"nyc": "^17.0.0",
|
|
117
99
|
"prettier": "^3.8.3",
|
|
118
|
-
"serverless": "^4.
|
|
100
|
+
"serverless": "^4.36.1"
|
|
119
101
|
},
|
|
120
102
|
"peerDependencies": {
|
|
121
103
|
"serverless": "^4.0.0"
|
|
@@ -116,6 +116,11 @@ export default {
|
|
|
116
116
|
type: "boolean",
|
|
117
117
|
usage: "Turns on loading of your HTTP proxy settings from serverless.yml.",
|
|
118
118
|
},
|
|
119
|
+
rubyWatchDirs: {
|
|
120
|
+
type: "string",
|
|
121
|
+
usage:
|
|
122
|
+
"Comma-separated list of directories to watch for Ruby (.rb) file changes. When set, the persistent Ruby process is automatically restarted on change for hot reload.",
|
|
123
|
+
},
|
|
119
124
|
terminateIdleLambdaTime: {
|
|
120
125
|
type: "string",
|
|
121
126
|
usage:
|
|
@@ -24,6 +24,7 @@ import logRoutes from "../../utils/logRoutes.js"
|
|
|
24
24
|
import {
|
|
25
25
|
createApiKey,
|
|
26
26
|
detectEncoding,
|
|
27
|
+
generateHapiCookie,
|
|
27
28
|
generateHapiPath,
|
|
28
29
|
getApiKeysValues,
|
|
29
30
|
getHttpApiCorsConfig,
|
|
@@ -883,13 +884,8 @@ export default class HttpServer {
|
|
|
883
884
|
log.debug("headers", headers)
|
|
884
885
|
|
|
885
886
|
const parseCookies = (headerValue) => {
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
h.state(cookieName, cookieValue, {
|
|
890
|
-
encoding: "none",
|
|
891
|
-
strictHeader: false,
|
|
892
|
-
})
|
|
887
|
+
const hapiCookie = generateHapiCookie(headerValue)
|
|
888
|
+
h.state(hapiCookie.name, hapiCookie.value, hapiCookie.options)
|
|
893
889
|
}
|
|
894
890
|
|
|
895
891
|
entries(headers).forEach(([headerKey, headerValue]) => {
|
|
@@ -79,7 +79,7 @@ export default class HandlerRunner {
|
|
|
79
79
|
if (supportedRuby.has(runtime)) {
|
|
80
80
|
const { default: RubyRunner } = await import("./ruby-runner/index.js")
|
|
81
81
|
|
|
82
|
-
return new RubyRunner(this.#funOptions, this.#env)
|
|
82
|
+
return new RubyRunner(this.#funOptions, this.#env, this.#options)
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
if (supportedJava.has(runtime)) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { watch } from "node:fs"
|
|
1
3
|
import { EOL, platform } from "node:os"
|
|
2
|
-
import { relative } from "node:path"
|
|
3
|
-
import { cwd } from "node:process"
|
|
4
|
+
import { resolve, relative } from "node:path"
|
|
5
|
+
import process, { cwd, nextTick } from "node:process"
|
|
6
|
+
import { createInterface } from "node:readline"
|
|
4
7
|
import { join } from "desm"
|
|
5
|
-
import { execa } from "execa"
|
|
6
8
|
import { log } from "../../../utils/log.js"
|
|
7
9
|
import { splitHandlerPathAndName } from "../../../utils/index.js"
|
|
8
10
|
|
|
@@ -12,28 +14,199 @@ const { hasOwn } = Object
|
|
|
12
14
|
export default class RubyRunner {
|
|
13
15
|
static #payloadIdentifier = "__offline_payload__"
|
|
14
16
|
|
|
17
|
+
static #errorIdentifier = "__offline_error__"
|
|
18
|
+
|
|
15
19
|
#env = null
|
|
16
20
|
|
|
17
|
-
#
|
|
21
|
+
#handlerProcess = null
|
|
22
|
+
|
|
23
|
+
#readline = null
|
|
24
|
+
|
|
25
|
+
#runtime = null
|
|
26
|
+
|
|
27
|
+
#spawnArgs = null
|
|
28
|
+
|
|
29
|
+
#spawnError = null
|
|
30
|
+
|
|
31
|
+
#spawnOptions = null
|
|
32
|
+
|
|
33
|
+
#watchers = []
|
|
34
|
+
|
|
35
|
+
#debounceTimer = null
|
|
36
|
+
|
|
37
|
+
#busy = false
|
|
38
|
+
|
|
39
|
+
#restartQueued = false
|
|
40
|
+
|
|
41
|
+
#watchDirs = []
|
|
18
42
|
|
|
19
|
-
|
|
43
|
+
// Serializes concurrent run() calls so writes to the shared Ruby stdin
|
|
44
|
+
// and reads from stdout cannot interleave.
|
|
45
|
+
#queue = Promise.resolve()
|
|
20
46
|
|
|
21
|
-
constructor(
|
|
47
|
+
// Spawn a persistent Ruby process in the constructor (mirrors PythonRunner).
|
|
48
|
+
// The process stays alive across invocations and communicates via stdin/stdout.
|
|
49
|
+
// File changes trigger an automatic restart when rubyWatchDirs is configured.
|
|
50
|
+
constructor(funOptions, env, options = {}) {
|
|
22
51
|
const [handlerPath, handlerName] = splitHandlerPathAndName(
|
|
23
52
|
funOptions.handler,
|
|
24
53
|
)
|
|
25
54
|
|
|
26
55
|
this.#env = env
|
|
27
|
-
this.#
|
|
28
|
-
|
|
56
|
+
this.#runtime = platform() === "win32" ? "ruby.exe" : "ruby"
|
|
57
|
+
|
|
58
|
+
this.#spawnArgs = [
|
|
59
|
+
join(import.meta.url, "invoke.rb"),
|
|
60
|
+
relative(cwd(), handlerPath),
|
|
61
|
+
handlerName,
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
this.#spawnOptions = {
|
|
65
|
+
env: options.localEnvironment
|
|
66
|
+
? { ...process.env, ...this.#env }
|
|
67
|
+
: { ...this.#env },
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rawWatchDirs = options.rubyWatchDirs ?? []
|
|
71
|
+
this.#watchDirs =
|
|
72
|
+
typeof rawWatchDirs === "string"
|
|
73
|
+
? rawWatchDirs
|
|
74
|
+
.split(",")
|
|
75
|
+
.map((dir) => dir.trim())
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
: rawWatchDirs
|
|
78
|
+
|
|
79
|
+
this.#spawnProcess()
|
|
80
|
+
|
|
81
|
+
if (this.#watchDirs.length > 0) {
|
|
82
|
+
this.#setupFileWatcher()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#spawnProcess() {
|
|
87
|
+
this.#spawnError = null
|
|
88
|
+
this.#readline = null
|
|
89
|
+
|
|
90
|
+
this.#handlerProcess = spawn(
|
|
91
|
+
this.#runtime,
|
|
92
|
+
this.#spawnArgs,
|
|
93
|
+
this.#spawnOptions,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// Persistent error listener so an async spawn failure (e.g., Ruby not
|
|
97
|
+
// on PATH) does not crash serverless-offline with an unhandled "error"
|
|
98
|
+
// event. The stored error is surfaced from the next run() call.
|
|
99
|
+
this.#handlerProcess.on("error", (err) => {
|
|
100
|
+
this.#spawnError = err
|
|
101
|
+
log.error(`Ruby process error: ${err.message}`)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// When spawn fails synchronously the returned ChildProcess can have
|
|
105
|
+
// null stdio streams. Mark a spawn error immediately so the next run()
|
|
106
|
+
// rejects with a useful message instead of letting createInterface or
|
|
107
|
+
// stderr.on() throw on null streams.
|
|
108
|
+
if (!this.#handlerProcess.stdout || !this.#handlerProcess.stderr) {
|
|
109
|
+
this.#spawnError = new Error(
|
|
110
|
+
`Failed to spawn Ruby process "${this.#runtime}". Is Ruby installed and on PATH?`,
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.#readline = createInterface({
|
|
116
|
+
input: this.#handlerProcess.stdout,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#setupFileWatcher() {
|
|
121
|
+
const watchDirs = this.#watchDirs.map((dir) => resolve(cwd(), dir))
|
|
122
|
+
|
|
123
|
+
for (const dir of watchDirs) {
|
|
124
|
+
try {
|
|
125
|
+
const watcher = watch(
|
|
126
|
+
dir,
|
|
127
|
+
{ recursive: true },
|
|
128
|
+
(_eventType, filename) => {
|
|
129
|
+
if (!filename?.endsWith(".rb")) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.#onFileChanged(filename)
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
this.#watchers.push(watcher)
|
|
138
|
+
} catch (err) {
|
|
139
|
+
log.warning(
|
|
140
|
+
`Ruby hot-reload watcher could not be enabled for "${dir}": ${err.message}. ` +
|
|
141
|
+
"Recursive fs.watch may not be supported on this platform.",
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#onFileChanged(filename) {
|
|
148
|
+
if (this.#debounceTimer) {
|
|
149
|
+
clearTimeout(this.#debounceTimer)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.#debounceTimer = setTimeout(() => {
|
|
153
|
+
log.notice(`Ruby file changed: ${filename}, reloading handler...`)
|
|
154
|
+
this.#scheduleRestart()
|
|
155
|
+
}, 100)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#scheduleRestart() {
|
|
159
|
+
if (this.#busy) {
|
|
160
|
+
// Defer restart until the current invocation completes
|
|
161
|
+
this.#restartQueued = true
|
|
162
|
+
} else {
|
|
163
|
+
this.#restartProcess()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#restartProcess() {
|
|
168
|
+
this.#disposeProcess()
|
|
169
|
+
this.#spawnProcess()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#disposeProcess() {
|
|
173
|
+
if (this.#readline) {
|
|
174
|
+
this.#readline.close()
|
|
175
|
+
this.#readline = null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (this.#handlerProcess && this.#handlerProcess.exitCode == null) {
|
|
179
|
+
try {
|
|
180
|
+
this.#handlerProcess.kill()
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (err.code !== "ESRCH") {
|
|
183
|
+
log.warning(`Failed to kill Ruby process: ${err.message}`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.#handlerProcess = null
|
|
29
189
|
}
|
|
30
190
|
|
|
31
|
-
// no-op
|
|
32
191
|
// () => void
|
|
33
|
-
cleanup() {
|
|
192
|
+
cleanup() {
|
|
193
|
+
for (const watcher of this.#watchers) {
|
|
194
|
+
watcher.close()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.#watchers = []
|
|
198
|
+
|
|
199
|
+
if (this.#debounceTimer) {
|
|
200
|
+
clearTimeout(this.#debounceTimer)
|
|
201
|
+
this.#debounceTimer = null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.#disposeProcess()
|
|
205
|
+
}
|
|
34
206
|
|
|
35
207
|
#parsePayload(value) {
|
|
36
208
|
let payload
|
|
209
|
+
let error
|
|
37
210
|
|
|
38
211
|
for (const item of value.split(EOL)) {
|
|
39
212
|
let json
|
|
@@ -46,61 +219,171 @@ export default class RubyRunner {
|
|
|
46
219
|
// no-op
|
|
47
220
|
}
|
|
48
221
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
222
|
+
if (json && typeof json === "object") {
|
|
223
|
+
if (hasOwn(json, RubyRunner.#errorIdentifier)) {
|
|
224
|
+
error = json[RubyRunner.#errorIdentifier]
|
|
225
|
+
} else if (hasOwn(json, RubyRunner.#payloadIdentifier)) {
|
|
226
|
+
payload = json[RubyRunner.#payloadIdentifier]
|
|
227
|
+
} else {
|
|
228
|
+
log.notice(item)
|
|
229
|
+
}
|
|
56
230
|
} else {
|
|
57
231
|
log.notice(item)
|
|
58
232
|
}
|
|
59
233
|
}
|
|
60
234
|
|
|
61
|
-
return payload
|
|
235
|
+
return { error, payload }
|
|
62
236
|
}
|
|
63
237
|
|
|
64
238
|
// invokeLocalRuby, loosely based on:
|
|
65
239
|
// https://github.com/serverless/serverless/blob/v1.50.0/lib/plugins/aws/invokeLocal/index.js#L556
|
|
66
|
-
// invoke.rb, copy/pasted entirely as is:
|
|
67
|
-
// https://github.com/serverless/serverless/blob/v1.50.0/lib/plugins/aws/invokeLocal/invoke.rb
|
|
68
240
|
async run(event, context) {
|
|
69
|
-
|
|
241
|
+
// Chain onto the queue so each invocation has exclusive access to the
|
|
242
|
+
// shared stdin/stdout channel. Errors in the chain must not poison
|
|
243
|
+
// subsequent runs.
|
|
244
|
+
const result = this.#queue.then(() => this.#runOne(event, context))
|
|
245
|
+
this.#queue = result.then(
|
|
246
|
+
() => {},
|
|
247
|
+
() => {},
|
|
248
|
+
)
|
|
249
|
+
return result
|
|
250
|
+
}
|
|
70
251
|
|
|
71
|
-
|
|
252
|
+
async #runOne(event, context) {
|
|
253
|
+
// Respawn if the Ruby process died (handler crash, OOM kill, etc.) or
|
|
254
|
+
// failed to spawn previously. Without this, subsequent runs would fail
|
|
255
|
+
// with EPIPE forever.
|
|
256
|
+
if (
|
|
257
|
+
this.#handlerProcess == null ||
|
|
258
|
+
this.#handlerProcess.exitCode != null ||
|
|
259
|
+
this.#spawnError != null ||
|
|
260
|
+
this.#readline == null
|
|
261
|
+
) {
|
|
262
|
+
this.#disposeProcess()
|
|
263
|
+
this.#spawnProcess()
|
|
264
|
+
}
|
|
72
265
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
266
|
+
// If respawn also failed (e.g., Ruby is still missing), bail out with
|
|
267
|
+
// the stored spawn error rather than touching null streams below.
|
|
268
|
+
if (this.#spawnError != null || this.#readline == null) {
|
|
269
|
+
throw this.#spawnError ?? new Error("Ruby process is not running")
|
|
270
|
+
}
|
|
76
271
|
|
|
77
|
-
|
|
78
|
-
context: _context,
|
|
79
|
-
event,
|
|
80
|
-
})
|
|
272
|
+
this.#busy = true
|
|
81
273
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
join(import.meta.url, "invoke.rb"),
|
|
88
|
-
relative(cwd(), this.#handlerPath),
|
|
89
|
-
this.#handlerName,
|
|
90
|
-
],
|
|
91
|
-
{
|
|
92
|
-
env: this.#env,
|
|
93
|
-
input,
|
|
94
|
-
// shell: true,
|
|
95
|
-
},
|
|
96
|
-
)
|
|
274
|
+
try {
|
|
275
|
+
return await new Promise((res, rej) => {
|
|
276
|
+
// https://docs.aws.amazon.com/lambda/latest/dg/ruby-context.html
|
|
277
|
+
// exclude callbackWaitsForEmptyEventLoop, don't mutate context
|
|
278
|
+
const { callbackWaitsForEmptyEventLoop, ..._context } = context
|
|
97
279
|
|
|
98
|
-
|
|
99
|
-
|
|
280
|
+
const input = stringify({
|
|
281
|
+
context: _context,
|
|
282
|
+
event,
|
|
283
|
+
})
|
|
100
284
|
|
|
101
|
-
|
|
102
|
-
|
|
285
|
+
const handlerProcess = this.#handlerProcess
|
|
286
|
+
const readline = this.#readline
|
|
287
|
+
|
|
288
|
+
let onLine
|
|
289
|
+
let onErr
|
|
290
|
+
let onProcessError
|
|
291
|
+
let onProcessExit
|
|
292
|
+
|
|
293
|
+
const cleanupListeners = () => {
|
|
294
|
+
// Defensive null guards: readline/stderr should be present here
|
|
295
|
+
// because #runOne() bails out before listener attachment when
|
|
296
|
+
// they are null, but a process can crash mid-flight.
|
|
297
|
+
readline?.removeListener("line", onLine)
|
|
298
|
+
handlerProcess.stderr?.removeListener("data", onErr)
|
|
299
|
+
handlerProcess.removeListener("error", onProcessError)
|
|
300
|
+
handlerProcess.removeListener("exit", onProcessExit)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const settleResolve = (value) => {
|
|
304
|
+
cleanupListeners()
|
|
305
|
+
res(value)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const settleReject = (err) => {
|
|
309
|
+
cleanupListeners()
|
|
310
|
+
rej(err)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
onErr = (data) => {
|
|
314
|
+
// TODO
|
|
103
315
|
|
|
104
|
-
|
|
316
|
+
log.notice(data.toString())
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
onProcessError = (err) => {
|
|
320
|
+
settleReject(err)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
onProcessExit = (code, signal) => {
|
|
324
|
+
settleReject(
|
|
325
|
+
new Error(
|
|
326
|
+
`Ruby process exited unexpectedly (code=${code}, signal=${signal}) before responding`,
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
onLine = (line) => {
|
|
332
|
+
try {
|
|
333
|
+
const { error, payload } = this.#parsePayload(line.toString())
|
|
334
|
+
|
|
335
|
+
if (error !== undefined) {
|
|
336
|
+
const err = new Error(error.errorMessage ?? "Ruby handler error")
|
|
337
|
+
err.name = error.errorType ?? "RubyHandlerError"
|
|
338
|
+
if (error.stackTrace) {
|
|
339
|
+
err.stack = `${err.name}: ${err.message}\n${
|
|
340
|
+
Array.isArray(error.stackTrace)
|
|
341
|
+
? error.stackTrace.join("\n")
|
|
342
|
+
: error.stackTrace
|
|
343
|
+
}`
|
|
344
|
+
}
|
|
345
|
+
settleReject(err)
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (payload !== undefined) {
|
|
350
|
+
settleResolve(payload)
|
|
351
|
+
}
|
|
352
|
+
} catch (err) {
|
|
353
|
+
settleReject(err)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
readline.on("line", onLine)
|
|
358
|
+
handlerProcess.stderr.on("data", onErr)
|
|
359
|
+
handlerProcess.once("error", onProcessError)
|
|
360
|
+
handlerProcess.once("exit", onProcessExit)
|
|
361
|
+
|
|
362
|
+
nextTick(() => {
|
|
363
|
+
try {
|
|
364
|
+
handlerProcess.stdin.write(input, (writeErr) => {
|
|
365
|
+
if (writeErr) {
|
|
366
|
+
settleReject(writeErr)
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
handlerProcess.stdin.write("\n", (nlErr) => {
|
|
370
|
+
if (nlErr) {
|
|
371
|
+
settleReject(nlErr)
|
|
372
|
+
}
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
} catch (err) {
|
|
376
|
+
settleReject(err)
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
} finally {
|
|
381
|
+
this.#busy = false
|
|
382
|
+
|
|
383
|
+
if (this.#restartQueued) {
|
|
384
|
+
this.#restartQueued = false
|
|
385
|
+
this.#restartProcess()
|
|
386
|
+
}
|
|
387
|
+
}
|
|
105
388
|
}
|
|
106
389
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Persistent Ruby invoke script for serverless-offline.
|
|
2
|
+
# Mirrors the Python runner pattern: spawn once, loop forever via stdin/stdout.
|
|
3
|
+
#
|
|
4
|
+
# Original one-shot version was based on:
|
|
2
5
|
# https://github.com/serverless/serverless/blob/v1.50.0/lib/plugins/aws/invokeLocal/invoke.rb
|
|
3
6
|
|
|
4
7
|
require 'json'
|
|
@@ -28,26 +31,6 @@ class FakeLambdaContext
|
|
|
28
31
|
def get_remaining_time_in_millis
|
|
29
32
|
[@timeout*1000 - ((Time.now() - @created_time)*1000).round, 0].max
|
|
30
33
|
end
|
|
31
|
-
|
|
32
|
-
# def invoked_function_arn
|
|
33
|
-
# "arn:aws:lambda:serverless:#{function_name}"
|
|
34
|
-
# end
|
|
35
|
-
#
|
|
36
|
-
# def memory_limit_in_mb
|
|
37
|
-
# return @memory_limit_in_mb
|
|
38
|
-
# end
|
|
39
|
-
#
|
|
40
|
-
# def log_group_name
|
|
41
|
-
# return @log_group_name
|
|
42
|
-
# end
|
|
43
|
-
#
|
|
44
|
-
# def log_stream_name
|
|
45
|
-
# return Time.now.strftime('%Y/%m/%d') +'/[$' + function_version + ']58419525dade4d17a495dceeeed44708'
|
|
46
|
-
# end
|
|
47
|
-
#
|
|
48
|
-
# def log(message)
|
|
49
|
-
# puts message
|
|
50
|
-
# end
|
|
51
34
|
end
|
|
52
35
|
|
|
53
36
|
|
|
@@ -56,7 +39,7 @@ def attach_tty
|
|
|
56
39
|
$stdin.reopen "/dev/tty", "a+"
|
|
57
40
|
end
|
|
58
41
|
rescue
|
|
59
|
-
puts "tty unavailable"
|
|
42
|
+
$stderr.puts "tty unavailable"
|
|
60
43
|
end
|
|
61
44
|
|
|
62
45
|
if __FILE__ == $0
|
|
@@ -68,8 +51,7 @@ if __FILE__ == $0
|
|
|
68
51
|
handler_path = ARGV[0]
|
|
69
52
|
handler_name = ARGV[1]
|
|
70
53
|
|
|
71
|
-
|
|
72
|
-
|
|
54
|
+
# Load the handler module ONCE at startup
|
|
73
55
|
require("./#{handler_path}")
|
|
74
56
|
|
|
75
57
|
# handler name is either a global method or a static method in a class
|
|
@@ -77,16 +59,46 @@ if __FILE__ == $0
|
|
|
77
59
|
handler_method, handler_class = handler_name.split(".").reverse
|
|
78
60
|
handler_class ||= "Kernel"
|
|
79
61
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
context = FakeLambdaContext.new(context: input['context'])
|
|
83
|
-
result = Object.const_get(handler_class).send(handler_method, event: input['event'], context: context)
|
|
62
|
+
# Keep a reference to the original stdin for reading from the parent process
|
|
63
|
+
original_stdin = $stdin.dup
|
|
84
64
|
|
|
85
|
-
|
|
86
|
-
# just an identifier to distinguish between
|
|
87
|
-
# interesting data (result) and stdout/print
|
|
88
|
-
'__offline_payload__': result
|
|
89
|
-
}
|
|
65
|
+
attach_tty
|
|
90
66
|
|
|
91
|
-
|
|
67
|
+
# Persistent loop: read JSON from stdin, invoke handler, write result to stdout
|
|
68
|
+
while (line = original_stdin.gets)
|
|
69
|
+
line = line.strip
|
|
70
|
+
next if line.empty?
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
input = JSON.parse(line)
|
|
74
|
+
|
|
75
|
+
context = FakeLambdaContext.new(context: input['context'] || {})
|
|
76
|
+
result = Object.const_get(handler_class).send(handler_method, event: input['event'], context: context)
|
|
77
|
+
|
|
78
|
+
data = {
|
|
79
|
+
# just an identifier to distinguish between
|
|
80
|
+
# interesting data (result) and stdout/print
|
|
81
|
+
'__offline_payload__': result
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
$stdout.write(data.to_json)
|
|
85
|
+
$stdout.write("\n")
|
|
86
|
+
$stdout.flush
|
|
87
|
+
rescue => e
|
|
88
|
+
$stderr.write("#{e.class}: #{e.message}\n")
|
|
89
|
+
$stderr.write(e.backtrace.join("\n") + "\n")
|
|
90
|
+
$stderr.flush
|
|
91
|
+
|
|
92
|
+
error_data = {
|
|
93
|
+
'__offline_error__': {
|
|
94
|
+
'errorType' => e.class.name,
|
|
95
|
+
'errorMessage' => e.message,
|
|
96
|
+
'stackTrace' => e.backtrace
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
$stdout.write(error_data.to_json)
|
|
100
|
+
$stdout.write("\n")
|
|
101
|
+
$stdout.flush
|
|
102
|
+
end
|
|
103
|
+
end
|
|
92
104
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
function calculateTtl(attributes) {
|
|
2
|
+
// Try max-age first (in seconds, convert to milliseconds)
|
|
3
|
+
const maxAgeSeconds = Number(attributes["max-age"])
|
|
4
|
+
if (Number.isFinite(maxAgeSeconds)) {
|
|
5
|
+
return maxAgeSeconds * 1000
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Fall back to expires (absolute date)
|
|
9
|
+
if (attributes.expires) {
|
|
10
|
+
const expiresDate = new Date(attributes.expires)
|
|
11
|
+
const now = new Date()
|
|
12
|
+
const expiresMs = expiresDate.getTime() - now.getTime()
|
|
13
|
+
return Number.isFinite(expiresMs) ? expiresMs : undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function generateHapiCookie(cookieString) {
|
|
20
|
+
const equalsIndex = cookieString.indexOf("=")
|
|
21
|
+
if (equalsIndex === -1) {
|
|
22
|
+
return {
|
|
23
|
+
name: cookieString,
|
|
24
|
+
options: {
|
|
25
|
+
domain: undefined,
|
|
26
|
+
encoding: "none",
|
|
27
|
+
isHttpOnly: false,
|
|
28
|
+
isSameSite: false,
|
|
29
|
+
isSecure: false,
|
|
30
|
+
path: undefined,
|
|
31
|
+
strictHeader: false,
|
|
32
|
+
ttl: undefined,
|
|
33
|
+
},
|
|
34
|
+
value: "",
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const semicolonIndex = cookieString.indexOf(";")
|
|
38
|
+
const valueEndIndex =
|
|
39
|
+
semicolonIndex === -1 ? cookieString.length : semicolonIndex
|
|
40
|
+
const name = cookieString.slice(0, equalsIndex)
|
|
41
|
+
const value = cookieString.slice(equalsIndex + 1, valueEndIndex)
|
|
42
|
+
|
|
43
|
+
// Parse attributes into a map
|
|
44
|
+
const attributes = cookieString
|
|
45
|
+
.split(";")
|
|
46
|
+
.slice(1) // skip the name=value part
|
|
47
|
+
.reduce((acc, part) => {
|
|
48
|
+
const [key, val] = part.trim().split("=")
|
|
49
|
+
acc[key.trim().toLowerCase()] = val ? val.trim() : true
|
|
50
|
+
return acc
|
|
51
|
+
}, {})
|
|
52
|
+
|
|
53
|
+
// Calculate TTL from max-age or expires attribute
|
|
54
|
+
const ttl = calculateTtl(attributes)
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name,
|
|
58
|
+
options: {
|
|
59
|
+
domain: attributes.domain,
|
|
60
|
+
encoding: "none",
|
|
61
|
+
isHttpOnly: attributes.httponly === true ? true : undefined,
|
|
62
|
+
isSameSite: attributes.samesite || false,
|
|
63
|
+
isSecure: attributes.secure === true ? true : undefined,
|
|
64
|
+
path: attributes.path,
|
|
65
|
+
strictHeader: false,
|
|
66
|
+
ttl,
|
|
67
|
+
},
|
|
68
|
+
value,
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/utils/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { default as checkGoVersion } from "./checkGoVersion.js"
|
|
|
3
3
|
export { default as createApiKey } from "./createApiKey.js"
|
|
4
4
|
export { default as detectExecutable } from "./detectExecutable.js"
|
|
5
5
|
export { default as formatToClfTime } from "./formatToClfTime.js"
|
|
6
|
+
export { default as generateHapiCookie } from "./generateHapiCookie.js"
|
|
6
7
|
export { default as generateHapiPath } from "./generateHapiPath.js"
|
|
7
8
|
export { default as getApiKeysValues } from "./getApiKeysValues.js"
|
|
8
9
|
export { default as getHttpApiCorsConfig } from "./getHttpApiCorsConfig.js"
|