• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gameap / gameap / 32674281665

23 Aug 2026 11:39PM UTC coverage: 85.345% (+0.08%) from 85.269%
32674281665

Pull #71

github

et-nik
update PLUGIN_MAX_MODULE_SIZE_MB
Pull Request #71: plugin system fixes and updates

1348 of 1590 new or added lines in 38 files covered. (84.78%)

14 existing lines in 6 files now uncovered.

57899 of 67841 relevant lines covered (85.35%)

29512.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.53
/pkg/plugin/wrapper.go
1
package plugin
2

3
import (
4
        "context"
5
        "time"
6

7
        "github.com/gameap/gameap/pkg/plugin/proto"
8
        "github.com/gameap/gameap/pkg/plugin/sdk/nodefs"
9
        "github.com/gameap/gameap/pkg/plugin/sdk/scheduler"
10
        "github.com/pkg/errors"
11
        "github.com/tetratelabs/wazero/api"
12
)
13

14
// defaultCallTimeout caps guest calls whose caller did not set a deadline,
15
// so a runaway plugin cannot hold the per-plugin call gate forever.
16
const defaultCallTimeout = 30 * time.Second
17

18
// pluginServiceWrapper wraps WASM module calls to implement proto.PluginService.
19
type pluginServiceWrapper struct {
20
        // gate serializes guest calls; a channel instead of a mutex so queued
21
        // callers can abandon the wait when their context ends.
22
        gate                chan struct{}
23
        module              api.Module
24
        malloc              api.Function
25
        free                api.Function
26
        getinfo             api.Function
27
        initialize          api.Function
28
        shutdown            api.Function
29
        handleevent         api.Function
30
        getsubscribedevents api.Function
31
        gethttproutes       api.Function
32
        handlehttprequest   api.Function
33
        getfrontendbundle   api.Function
34
        getserverabilities  api.Function
35
        getassets           api.Function
36
        getrconprotocols    api.Function
37
        getqueryprotocols   api.Function
38
        rconopen            api.Function
39
        rconexecute         api.Function
40
        rconclose           api.Function
41
        queryserver         api.Function
42
        parseplayers        api.Function
43
        handlescheduledtask api.Function
44

45
        handlearchiveprogress  api.Function
46
        handlearchivecompleted api.Function
47

48
        // guestLogs is flushed after every call so a partial stdout/stderr
49
        // line written by the guest reaches the log; nil in tests.
50
        guestLogs *guestLogs
51
        // onClosed fires when a call finds the module closed by the guest
52
        // itself (proc_exit); deadline closes are reported by the callers.
53
        onClosed func(err error)
54

55
        // observer receives the outcome and duration of every guest call;
56
        // pluginID labels them (0 for transient loads). Set by the manager.
57
        observer Observer
58
        pluginID uint64
59
}
60

61
func (p *pluginServiceWrapper) callFunction(
62
        ctx context.Context,
63
        fn api.Function,
64
        request vtMarshaler,
65
) ([]byte, error) {
117✔
66
        start := time.Now()
117✔
67

117✔
68
        // The wait honors the caller's full context (deadline and cancellation):
117✔
69
        // the guest has not been invoked yet, so giving up here is always safe.
117✔
70
        select {
117✔
71
        case p.gate <- struct{}{}:
108✔
72
        case <-ctx.Done():
9✔
73
                p.observeGuestCall(fn, start, GuestCallResultBusy)
9✔
74

9✔
75
                return nil, errors.Wrapf(ErrPluginBusy, "%s", ctx.Err())
9✔
76
        }
77
        defer func() { <-p.gate }()
216✔
78
        defer p.guestLogs.Flush()
108✔
79

108✔
80
        // select picks randomly when both cases are ready.
108✔
81
        if ctx.Err() != nil {
108✔
NEW
82
                p.observeGuestCall(fn, start, GuestCallResultBusy)
×
NEW
83

×
84
                return nil, errors.Wrapf(ErrPluginBusy, "%s", ctx.Err())
×
85
        }
×
86

87
        // The runtime closes the module when the call context is done
88
        // (WithCloseOnContextDone), so caller cancellation (e.g. a client
89
        // dropping an HTTP request) must not reach the guest — only explicit
90
        // deadlines may interrupt it.
91
        var cancel context.CancelFunc
108✔
92
        if deadline, ok := ctx.Deadline(); ok {
109✔
93
                ctx, cancel = context.WithDeadline(context.WithoutCancel(ctx), deadline)
1✔
94
        } else {
108✔
95
                ctx, cancel = context.WithTimeout(context.WithoutCancel(ctx), defaultCallTimeout)
107✔
96
        }
107✔
97
        defer cancel()
108✔
98

108✔
99
        data, err := request.MarshalVT()
108✔
100
        if err != nil {
108✔
101
                return nil, err
×
102
        }
×
103

104
        dataSize := uint64(len(data))
108✔
105

108✔
106
        var dataPtr uint64
108✔
107
        if dataSize != 0 {
147✔
108
                results, callErr := p.malloc.Call(ctx, dataSize)
39✔
109
                if callErr != nil {
42✔
110
                        p.observeCallError(callErr)
3✔
111

3✔
112
                        return nil, callErr
3✔
113
                }
3✔
114

115
                dataPtr = results[0]
36✔
116
                defer p.free.Call(ctx, dataPtr) //nolint:errcheck
36✔
117

36✔
118
                if !p.module.Memory().Write(uint32(dataPtr), data) { //nolint:gosec
36✔
119
                        return nil, errors.Wrapf(ErrMemoryOutOfRange, "write(%d, %d), size=%d",
×
120
                                dataPtr, dataSize, p.module.Memory().Size())
×
121
                }
×
122
        }
123

124
        ptrSize, err := fn.Call(ctx, dataPtr, dataSize)
105✔
125
        if err != nil {
110✔
126
                p.observeCallError(err)
5✔
127
                p.observeGuestCall(fn, start, guestCallResult(err))
5✔
128

5✔
129
                return nil, err
5✔
130
        }
5✔
131

132
        resPtr := uint32(ptrSize[0] >> 32)
100✔
133
        resSize := uint32(ptrSize[0]) //nolint:gosec
100✔
134
        isErrResponse := (resSize & (1 << 31)) > 0
100✔
135

100✔
136
        if isErrResponse {
100✔
137
                resSize &^= (1 << 31)
×
138
        }
×
139

140
        if resPtr != 0 {
147✔
141
                defer p.free.Call(ctx, uint64(resPtr)) //nolint:errcheck
47✔
142
        }
47✔
143

144
        bytes, ok := p.module.Memory().Read(resPtr, resSize)
100✔
145
        if !ok {
100✔
146
                return nil, errors.Wrapf(ErrMemoryOutOfRange, "read(%d, %d), size=%d",
×
147
                        resPtr, resSize, p.module.Memory().Size())
×
148
        }
×
149

150
        if isErrResponse {
100✔
NEW
151
                p.observeGuestCall(fn, start, GuestCallResultError)
×
NEW
152

×
153
                return nil, errors.WithMessage(ErrPluginReturnedError, string(bytes))
×
154
        }
×
155

156
        p.observeGuestCall(fn, start, GuestCallResultOK)
100✔
157

100✔
158
        return bytes, nil
100✔
159
}
160

161
// observeGuestCall reports one guest call to the observer under the
162
// function's export name.
163
func (p *pluginServiceWrapper) observeGuestCall(fn api.Function, start time.Time, result string) {
114✔
164
        if p.observer == nil {
123✔
165
                return
9✔
166
        }
9✔
167

168
        export := ""
105✔
169
        if names := fn.Definition().ExportNames(); len(names) > 0 {
210✔
170
                export = names[0]
105✔
171
        }
105✔
172

173
        p.observer.GuestCall(p.pluginID, export, time.Since(start), result)
105✔
174
}
175

176
// guestCallResult classifies a failed guest call for the observer.
177
func guestCallResult(err error) string {
5✔
178
        switch {
5✔
179
        case errors.Is(err, context.DeadlineExceeded):
1✔
180
                return GuestCallResultTimeout
1✔
NEW
181
        case errors.Is(err, ErrPluginBusy):
×
NEW
182
                return GuestCallResultBusy
×
183
        default:
4✔
184
                return GuestCallResultError
4✔
185
        }
186
}
187

188
// observeCallError reports a module the guest closed on its own. A close
189
// caused by the call deadline is left to the caller, which knows what the
190
// guest was doing and disables the plugin with that reason.
191
func (p *pluginServiceWrapper) observeCallError(err error) {
8✔
192
        if p.onClosed == nil || !p.module.IsClosed() {
9✔
193
                return
1✔
194
        }
1✔
195

196
        if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
9✔
197
                return
2✔
198
        }
2✔
199

200
        p.onClosed(err)
5✔
201
}
202

203
// MemorySize reports the module's linear memory size between guest calls;
204
// false while a call is in flight (the memory may be growing) or once the
205
// module is closed.
206
func (p *pluginServiceWrapper) MemorySize() (uint64, bool) {
1✔
207
        select {
1✔
208
        case p.gate <- struct{}{}:
1✔
209
        default:
×
210
                return 0, false
×
211
        }
212
        defer func() { <-p.gate }()
2✔
213

214
        if p.module.IsClosed() {
1✔
215
                return 0, false
×
216
        }
×
217

218
        memory := p.module.Memory()
1✔
219
        if memory == nil {
1✔
220
                return 0, false
×
221
        }
×
222

223
        return uint64(memory.Size()), true
1✔
224
}
225

226
type vtMarshaler interface {
227
        MarshalVT() ([]byte, error)
228
}
229

230
func (p *pluginServiceWrapper) GetInfo(
231
        ctx context.Context,
232
        request *proto.GetInfoRequest,
233
) (*proto.PluginInfo, error) {
19✔
234
        bytes, err := p.callFunction(ctx, p.getinfo, request)
19✔
235
        if err != nil {
19✔
236
                return nil, err
×
237
        }
×
238

239
        response := new(proto.PluginInfo)
19✔
240
        if err = response.UnmarshalVT(bytes); err != nil {
19✔
241
                return nil, err
×
242
        }
×
243

244
        return response, nil
19✔
245
}
246

247
func (p *pluginServiceWrapper) Initialize(
248
        ctx context.Context,
249
        request *proto.InitializeRequest,
250
) (*proto.InitializeResponse, error) {
17✔
251
        bytes, err := p.callFunction(ctx, p.initialize, request)
17✔
252
        if err != nil {
17✔
253
                return nil, err
×
254
        }
×
255

256
        response := new(proto.InitializeResponse)
17✔
257
        if err = response.UnmarshalVT(bytes); err != nil {
17✔
258
                return nil, err
×
259
        }
×
260

261
        return response, nil
17✔
262
}
263

264
func (p *pluginServiceWrapper) Shutdown(
265
        ctx context.Context,
266
        request *proto.ShutdownRequest,
267
) (*proto.ShutdownResponse, error) {
9✔
268
        bytes, err := p.callFunction(ctx, p.shutdown, request)
9✔
269
        if err != nil {
12✔
270
                return nil, err
3✔
271
        }
3✔
272

273
        response := new(proto.ShutdownResponse)
6✔
274
        if err = response.UnmarshalVT(bytes); err != nil {
6✔
275
                return nil, err
×
276
        }
×
277

278
        return response, nil
6✔
279
}
280

281
func (p *pluginServiceWrapper) HandleEvent(
282
        ctx context.Context,
283
        request *proto.Event,
284
) (*proto.EventResult, error) {
8✔
285
        bytes, err := p.callFunction(ctx, p.handleevent, request)
8✔
286
        if err != nil {
12✔
287
                return nil, err
4✔
288
        }
4✔
289

290
        response := new(proto.EventResult)
4✔
291
        if err = response.UnmarshalVT(bytes); err != nil {
4✔
292
                return nil, err
×
293
        }
×
294

295
        return response, nil
4✔
296
}
297

298
func (p *pluginServiceWrapper) GetSubscribedEvents(
299
        ctx context.Context,
300
        request *proto.GetSubscribedEventsRequest,
301
) (*proto.GetSubscribedEventsResponse, error) {
17✔
302
        bytes, err := p.callFunction(ctx, p.getsubscribedevents, request)
17✔
303
        if err != nil {
17✔
304
                return nil, err
×
305
        }
×
306

307
        response := new(proto.GetSubscribedEventsResponse)
17✔
308
        if err = response.UnmarshalVT(bytes); err != nil {
17✔
309
                return nil, err
×
310
        }
×
311

312
        return response, nil
17✔
313
}
314

315
func (p *pluginServiceWrapper) GetHTTPRoutes(
316
        ctx context.Context,
317
        request *proto.GetHTTPRoutesRequest,
318
) (*proto.GetHTTPRoutesResponse, error) {
17✔
319
        bytes, err := p.callFunction(ctx, p.gethttproutes, request)
17✔
320
        if err != nil {
17✔
321
                return nil, err
×
322
        }
×
323

324
        response := new(proto.GetHTTPRoutesResponse)
17✔
325
        if err = response.UnmarshalVT(bytes); err != nil {
17✔
326
                return nil, err
×
327
        }
×
328

329
        return response, nil
17✔
330
}
331

332
func (p *pluginServiceWrapper) HandleHTTPRequest(
333
        ctx context.Context,
334
        request *proto.HTTPRequest,
335
) (*proto.HTTPResponse, error) {
3✔
336
        bytes, err := p.callFunction(ctx, p.handlehttprequest, request)
3✔
337
        if err != nil {
4✔
338
                return nil, err
1✔
339
        }
1✔
340

341
        response := new(proto.HTTPResponse)
2✔
342
        if err = response.UnmarshalVT(bytes); err != nil {
2✔
343
                return nil, err
×
344
        }
×
345

346
        return response, nil
2✔
347
}
348

349
func (p *pluginServiceWrapper) GetFrontendBundle(
350
        ctx context.Context,
351
        request *proto.GetFrontendBundleRequest,
352
) (*proto.GetFrontendBundleResponse, error) {
18✔
353
        if p.getfrontendbundle == nil {
30✔
354
                return &proto.GetFrontendBundleResponse{HasBundle: false}, nil
12✔
355
        }
12✔
356

357
        bytes, err := p.callFunction(ctx, p.getfrontendbundle, request)
6✔
358
        if err != nil {
6✔
359
                return nil, err
×
360
        }
×
361

362
        response := new(proto.GetFrontendBundleResponse)
6✔
363
        if err = response.UnmarshalVT(bytes); err != nil {
6✔
364
                return nil, err
×
365
        }
×
366

367
        return response, nil
6✔
368
}
369

370
func (p *pluginServiceWrapper) GetServerAbilities(
371
        ctx context.Context,
372
        request *proto.GetServerAbilitiesRequest,
373
) (*proto.GetServerAbilitiesResponse, error) {
18✔
374
        if p.getserverabilities == nil {
30✔
375
                return &proto.GetServerAbilitiesResponse{Abilities: nil}, nil
12✔
376
        }
12✔
377

378
        bytes, err := p.callFunction(ctx, p.getserverabilities, request)
6✔
379
        if err != nil {
6✔
380
                return nil, err
×
381
        }
×
382

383
        response := new(proto.GetServerAbilitiesResponse)
6✔
384
        if err = response.UnmarshalVT(bytes); err != nil {
6✔
385
                return nil, err
×
386
        }
×
387

388
        return response, nil
6✔
389
}
390

391
func (p *pluginServiceWrapper) GetAssets(
392
        ctx context.Context,
393
        request *proto.GetAssetsRequest,
394
) (*proto.GetAssetsResponse, error) {
16✔
395
        if p.getassets == nil {
29✔
396
                return &proto.GetAssetsResponse{}, nil
13✔
397
        }
13✔
398

399
        bytes, err := p.callFunction(ctx, p.getassets, request)
3✔
400
        if err != nil {
3✔
401
                return nil, err
×
402
        }
×
403

404
        response := new(proto.GetAssetsResponse)
3✔
405
        if err = response.UnmarshalVT(bytes); err != nil {
3✔
406
                return nil, err
×
407
        }
×
408

409
        return response, nil
3✔
410
}
411

412
// HandleScheduledTask invokes the optional handler exported by plugins built
413
// with the sdk/scheduler module. Unlike the optional load-time queries above,
414
// a missing export is an error, not a benign empty response — silently
415
// succeeding would swallow a scheduled run.
416
func (p *pluginServiceWrapper) HandleScheduledTask(
417
        ctx context.Context,
418
        request *scheduler.HandleScheduledTaskRequest,
419
) (*scheduler.HandleScheduledTaskResponse, error) {
3✔
420
        if p.handlescheduledtask == nil {
5✔
421
                return nil, errors.WithMessage(ErrExportNotFound, "scheduled_task_handler_handle_scheduled_task")
2✔
422
        }
2✔
423

424
        bytes, err := p.callFunction(ctx, p.handlescheduledtask, request)
1✔
425
        if err != nil {
1✔
426
                return nil, err
×
427
        }
×
428

429
        response := new(scheduler.HandleScheduledTaskResponse)
1✔
430
        if err = response.UnmarshalVT(bytes); err != nil {
1✔
431
                return nil, err
×
432
        }
×
433

434
        return response, nil
1✔
435
}
436

437
func (p *pluginServiceWrapper) HasScheduledTaskHandler() bool {
3✔
438
        return p.handlescheduledtask != nil
3✔
439
}
3✔
440

441
// HandleArchiveProgress invokes the optional handler exported by plugins
442
// registering an ArchiveEventsHandler from the sdk/nodefs module. Like
443
// HandleScheduledTask, a missing export is an error — callers gate on
444
// HasArchiveEventsHandler and never deliver into a plugin without it.
445
func (p *pluginServiceWrapper) HandleArchiveProgress(
446
        ctx context.Context,
447
        request *nodefs.HandleArchiveProgressRequest,
448
) (*nodefs.HandleArchiveProgressResponse, error) {
2✔
449
        if p.handlearchiveprogress == nil {
3✔
450
                return nil, errors.WithMessage(ErrExportNotFound, "archive_events_handler_handle_archive_progress")
1✔
451
        }
1✔
452

453
        bytes, err := p.callFunction(ctx, p.handlearchiveprogress, request)
1✔
454
        if err != nil {
1✔
455
                return nil, err
×
456
        }
×
457

458
        response := new(nodefs.HandleArchiveProgressResponse)
1✔
459
        if err = response.UnmarshalVT(bytes); err != nil {
1✔
460
                return nil, err
×
461
        }
×
462

463
        return response, nil
1✔
464
}
465

466
func (p *pluginServiceWrapper) HandleArchiveCompleted(
467
        ctx context.Context,
468
        request *nodefs.HandleArchiveCompletedRequest,
469
) (*nodefs.HandleArchiveCompletedResponse, error) {
2✔
470
        if p.handlearchivecompleted == nil {
3✔
471
                return nil, errors.WithMessage(ErrExportNotFound, "archive_events_handler_handle_archive_completed")
1✔
472
        }
1✔
473

474
        bytes, err := p.callFunction(ctx, p.handlearchivecompleted, request)
1✔
475
        if err != nil {
1✔
476
                return nil, err
×
477
        }
×
478

479
        response := new(nodefs.HandleArchiveCompletedResponse)
1✔
480
        if err = response.UnmarshalVT(bytes); err != nil {
1✔
481
                return nil, err
×
482
        }
×
483

484
        return response, nil
1✔
485
}
486

487
func (p *pluginServiceWrapper) HasArchiveEventsHandler() bool {
6✔
488
        return p.handlearchiveprogress != nil && p.handlearchivecompleted != nil
6✔
489
}
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc