The contract with the extension¶
The extension talks to the core over JSON-RPC 2.0 on stdio, one message per
line. There is no Content-Length like in LSP: with no binary payload and no
streaming, the header would only add work on both ends.
extension ──► {"jsonrpc":"2.0","id":1,"method":"server.resolve","params":{…}}
extension ◄── {"jsonrpc":"2.0","id":1,"result":{…}}
extension ◄── {"jsonrpc":"2.0","method":"config.changed","params":{…}}
core.version returns the version and the list of methods the running
version answers. That is how the extension discovers what it may ask of an
older core, instead of trying and handling the error. A test guarantees every
name in that list is dispatchable: a method listed but not implemented would
break the contract silently.
The method groups¶
| Group | Methods | For what |
|---|---|---|
| Core | core.version |
Version and available methods |
| Configuration | config.set, config.delete, config.reload, config.open, config.ensureNamingFiles, config.inlineNamingLists, config.migrateNaming, config.backupNaming |
Read, write and migrate the config.json files and the .ban/.allow lists |
| State | state.get, state.updateServer |
Panel history and favourites, in .pawnpro/state.json |
| Compiler | compiler.detect, compiler.buildArgs, compiler.run |
Find pawncc, build the command line, and run it |
| Includes | includes.paths, includes.listFiles, includes.listNatives, includes.resolveSdk |
The roots, the .inc scan, and the SDK |
| Server | server.resolve, server.loadConfig, server.ping, server.pidsOnPort, server.projectServersOnPort, server.kill, server.readLog, server.sensitiveCommands, rcon.send |
Executable, log, ports, processes and RCON |
| Debugging | debug.preflight, debug.start |
Check the plugin and open a session |
| Engine | engine.start |
Start the engine and return the socket address |
| Logging | log.configure, log.write, log.clear |
Diagnostics in .pawnpro/logs/ |
| Project | project.changelogSection |
The changelog section for "What's New" |
Notifications go the other way, without an id:
| Notification | When |
|---|---|
config.changed |
A config.json or a list changed on disk — the extension refreshes its cache without asking |
core.subsystemStatus |
A subsystem started, crashed and came back, or gave up (see Supervision) |
What belongs to the core, and why¶
There is a single rule: whoever owns the resource answers for it. The rest follows from it.
- Configuration has one owner. The extension used to read
config.jsonand so did the engine; two readers of the same file are two possible answers. The core reads it, merges the scopes, resolves includes and SDK, and hands the result over — to the extension over RPC, to the engine over a typed channel. - Processes and ports have a single implementation, instead of one per
operating system scattered across
lsof,/proc,psandtaskkill. - The ownership policy also holds over RPC.
server.killrefuses a PID that is not this project's server: the extension cannot work around the rule by asking the core directly.
Slow work does not hold the loop¶
The message loop handles one request at a time, which is good for ordering and
bad for anything slow. Compiling takes seconds, and a compiler.run inside the
loop would leave the editor with no IntelliSense, no panel and no configuration
until pawncc finished.
That is why compiler.run is a job: it runs on a thread of its own and
answers when it is done, with the request's id. The loop keeps serving
everything else, and the order of the replies stops being the order of the
requests — which JSON-RPC already allows.
Errors¶
The codes are JSON-RPC's (-32600 and friends). A condition the interface has
to tell apart does not become prose: RCON, for instance, returns the named
failure (serverDown, disabled, invalidPassword, remoteBlocked,
timeout, io), and the extension picks the message. Error text cannot be
compared; an enum variant can.