serverless-offline 14.5.0 → 14.7.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.
- package/LICENSE +1 -1
- package/README.md +106 -104
- package/package.json +25 -44
- package/src/config/commandOptions.js +5 -0
- package/src/config/defaultOptions.js +1 -0
- package/src/config/supportedRuntimes.js +7 -1
- package/src/events/alb/HttpServer.js +43 -40
- package/src/lambda/LambdaFunction.js +5 -5
- package/src/lambda/handler-runner/HandlerRunner.js +5 -7
- package/src/lambda/handler-runner/docker-runner/DockerContainer.js +6 -7
- package/src/lambda/handler-runner/ruby-runner/RubyRunner.js +332 -49
- package/src/lambda/handler-runner/ruby-runner/invoke.rb +46 -34
|
@@ -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
|