mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-30 22:07:28 +00:00
refactor(sdk)!: 废除自定协议号,版本身份只由版本号本身承载
兼容性判据原有三处自造版本号,全部删除:
- procProtocolVersion(模板侧握手常量)
- shmVersion / shmOffVersion(共享段 header)
- unifiedVersion(SuperBlock)
为什么废除:语义版本变动频繁(修 bug、加字段),再要求人去升一个额外的号,
那个号必然漏升而腐烂。实证:ProtocolVersion 常量在多次布局变更里只升过一次,
而对应的守卫测试(TestProcTemplate_ShmLayoutMatchesKernel)长期处于失败状态
无人察觉 —— 它当时期望 shmStageFieldCount=18/shmVersion=1,实际已是 19/2。
取而代之的两个判据都是**结构的派生事实**,不需要谁记得去同步:
1. 主版本号:内核 meta.Version ↔ 插件 sdk.SDKVersion 的主版本段比对。
主版本跃迁正是破坏性变更的定义(1.0.0 从 C ABI 改子进程即属此列),
所以它天然是正确分界。
2. field_count:StageContext 描述符个数,决定标志位基址。
顺带修正了 shmFlagsOff:原实现用本插件的编译期常量推算标志位基址,
内核加字段时会静默错位。现改为从 header 读取内核下发的描述符个数。
SuperBlock 的 version 槽位保留(不动其余偏移),值不再写入也不再校验。
判据:TestProcTemplate_RejectsIncompatiblePeer(含 banned 列表,
防这些号被重新引入)。
This commit is contained in:
18
README.md
18
README.md
@ -32,14 +32,14 @@ HomeAgent 插件开发 SDK,用于构建与 HomeAgent 平台交互的智能插
|
||||
|
||||
**1.0.x 插件升到 1.1.x:不需要改代码,也不需要重编。** 1.1.0 的新增全部是
|
||||
「插件调用、内核实现」方向,不调就不受影响(已用 SDK 0.9.2 编的旧 `plugin.bin`
|
||||
实测验证:在新内核上直接建链通过,因为握手校验的是 `ProtocolVersion`、不是 SDK 版本)。
|
||||
实测验证:在新内核上直接建链通过,因为握手校验的是主版本号、不是 SDK 全等版本)。
|
||||
想用新字段时重编即可。
|
||||
|
||||
**1.1.x 插件升到 1.2.x:接口纯追加,但必须重编。** 公开接口没有签名变更(新增
|
||||
`InjectOptions` 与六个 `*Opts` 变体、`ChannelDef.ContextPolicy`),不调新能力就不受影响;
|
||||
但内核的**插件运行协议升到了 2**(统一共享内存区的 fd3 布局改变,**不支持滚动升级**),
|
||||
所以 `plugin.bin` 必须用配套的 `hmapdev` 重编后与内核**同批**安装——否则握手时协议版本
|
||||
不匹配会被拒绝(错误信息会明确提示用配套 hmapdev 重编,不会静默降级)。
|
||||
**1.1.x 插件升到 1.2.x:接口纯追加;若内核共享区布局改了则需重编。** 公开接口没有签名变更
|
||||
(新增 `InjectOptions` 与六个 `*Opts` 变体、`ChannelDef.ContextPolicy`),不调新能力就不受影响。
|
||||
1.2.0 同时把 fd3 换成了统一共享区(SuperBlock + StageContext + EvtRing),
|
||||
这类**布局**变化由握手时的结构判据拦下(统一区魔数、`field_count` 描述符个数),
|
||||
而不是靠某个版本号——它们都是二进制里的派生事实,不会「忘同步」。
|
||||
|
||||
## 1.3.0 新增:注入优先级与动态输出通道
|
||||
|
||||
@ -624,9 +624,9 @@ cd tools/vscode-hmapdev && npm install && npm run compile # 然后在 VSCode
|
||||
| [sanitizer](example/sanitizer) | Go | 内容清洗/安全过滤 |
|
||||
|
||||
**发版时附带预编译示例产物**:SDK 的 release 除 5 平台 `hmapdev` 外,还包含各示例插件的
|
||||
`.hmap` 与 `SHA256SUMS`/`MANIFEST.txt`。原因是插件二进制与内核**协议绑定**(`ProtocolVersion`
|
||||
+ 共享内存区魔数),只发工具链不发示例产物,很容易拿旧产物去装而握手失败——那看起来像
|
||||
「插件坏了」而不是「版本不配套」。
|
||||
`.hmap` 与 `SHA256SUMS`/`MANIFEST.txt`。原因是插件二进制与内核在**布局上绑定**(主版本号
|
||||
+ 统一共享区魔数 + `field_count`),只发工具链不发示例产物,很容易拿旧产物去装而握手失败
|
||||
——那看起来像「插件坏了」而不是「版本不配套」。
|
||||
|
||||
## Remote Device SDK
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@ has been withdrawn — any SDK tag with a non-zero patch position is wrong.)
|
||||
**Upgrading a 1.0.x plugin to 1.1.x: no code changes, no rebuild.** Everything added in 1.1.0 is
|
||||
in the "plugin calls, kernel implements" direction, so not calling it means not being affected
|
||||
(verified with an old `plugin.bin` built against SDK 0.9.2: it handshakes fine on the new kernel,
|
||||
because the handshake validates `ProtocolVersion`, not the SDK version). Rebuild only when you want
|
||||
because the handshake validates the **major** version, not the exact SDK version). Rebuild only when you want
|
||||
the new fields.
|
||||
|
||||
**Upgrading a 1.1.x plugin to 1.2.x: the interface is purely additive, but a rebuild is required.**
|
||||
@ -526,8 +526,8 @@ Internal plugins (platform built-in) have full SDK access including SocialAPI wr
|
||||
|
||||
**Prebuilt example artifacts ship with every release**: besides the 5-platform `hmapdev`, an SDK
|
||||
release contains the example plugins' `.hmap` files plus `SHA256SUMS`/`MANIFEST.txt`. The reason is
|
||||
that plugin binaries are **protocol-bound** to the kernel (`ProtocolVersion` + the shared-memory
|
||||
magic), so shipping the toolchain without matching artifacts invites installing an old artifact —
|
||||
that plugin binaries are **layout-bound** to the kernel (major version + unified-region magic +
|
||||
`field_count`), so shipping the toolchain without matching artifacts invites installing an old artifact —
|
||||
which fails the handshake and looks like "the plugin is broken" rather than "the versions don't
|
||||
match".
|
||||
|
||||
|
||||
@ -32,17 +32,33 @@
|
||||
想用新字段的重编即可。
|
||||
|
||||
!!! tip "什么时候必须重编"
|
||||
只有两种情况:① 内核跨了中版本(如 1.1 → 1.2)且你用了新 API;
|
||||
② 内核的 RPC 协议版本变了(`.hmap` 里的 `protocol` 字段与内核不匹配)。
|
||||
后者的错配**不会静默失效** —— 握手时会显式拦下。
|
||||
只有一种情况:**内核跨了主版本**(如 1.x → 2.x)。
|
||||
主版本跃迁是破坏性变更的定义,此后的帧格式与共享段布局都不兼容,
|
||||
必须用配套 `hmapdev` 重编。
|
||||
|
||||
## RPC 协议版本
|
||||
**中版本跃迁(如 1.1 → 1.2)不需要重编**:1.x 内的新增全部是
|
||||
「插件调用、内核实现」,不调新面就不受影响。
|
||||
|
||||
插件包里带 `protocol` 字段,必须等于内核的 `ProtocolVersion`(当前 **2**)。
|
||||
## 协议兼容性:只认主版本号
|
||||
|
||||
协议 v2 引入了调用帧(tool / cleaner / output)与 `blocks_ref` 媒体块。
|
||||
v1 插件遇上 v2 内核会拿到空参数,反过来 v2 插件发 `blocks_ref` 会被 v1 内核静默忽略 ——
|
||||
**两边错配都不报错、只是静默失效**,所以协议版本在握手上显式校验。
|
||||
**没有独立的协议版本号。** 兼容性由双方版本号的**主版本段**表态:
|
||||
内核的 `meta.Version` 与插件的 `sdk_version`(插件包里的 `sdk_version` 字段,
|
||||
即编译时链接的 SDK 版本)。
|
||||
|
||||
| 内核 | 插件 SDK | 结果 |
|
||||
|---|---|---|
|
||||
| 1.4.0 | 1.0.0 / 1.1.0 / 1.3.0 | ✅ 建链(同主版本) |
|
||||
| 1.4.0 | 2.0.0 | ❌ 拒绝(跨主版本) |
|
||||
|
||||
**为什么不设独立的协议号**:语义版本变动频繁(修 bug、加字段),如果每个
|
||||
这类改动都要人去升一个额外的号,那个号必然会被漏升而腐烂(历史实证:
|
||||
曾有一个 `ProtocolVersion` 常量,多次布局变更里只升过一次,而对应的守卫
|
||||
测试长期处于失败状态、无人察觉)。让兼容性只挂在**始终会被发的那个号**上,
|
||||
就没有「忘同步」这个失效模式。
|
||||
|
||||
跨主版本错配**不会静默失效**——握手时会显式拦下,并提示用配套 `hmapdev` 重编。
|
||||
此外握手还会比对 `field_count`(StageContext 描述符个数):它决定标志位基址,
|
||||
不一致会让插件读到错位字段,因此也在握手上显式拒绝。
|
||||
|
||||
## 怎么确认自己在用什么
|
||||
|
||||
|
||||
10
meta/meta.go
10
meta/meta.go
@ -103,15 +103,15 @@ func FullVersion() string {
|
||||
return SDKName + " v" + Version + " (" + Commit + ")"
|
||||
}
|
||||
|
||||
// ---- 协议版本 ----
|
||||
// ---- 协议兼容性 ----
|
||||
//
|
||||
// 子进程 RPC 的协议版本是一个独立的小整数,与 SDK/内核语义版本解耦:
|
||||
// 语义版本变动频繁(修 bug、加字段),而 wire 协议只在**帧格式或握手语义**
|
||||
// 变化时才升。当前值见核心仓 internal/plugin/proc/protocol.go 的 ProtocolVersion。
|
||||
// **不自定协议号**:兼容性只认双方版本号的**主版本段**。
|
||||
// 主版本相同即兼容(1.x 内全是纯追加);跨主版本则是破坏性变更
|
||||
// (如 1.0.0 从 C ABI 改子进程),握手时显式拒绝并提示重编。
|
||||
//
|
||||
// C ABI 时代的 ABIVersion / CABINum / 51 个 Core<Method> 整数 ID 已随
|
||||
// Part 6.2 删除 internal/plugin/cabi/ 一并退场:
|
||||
// - 整数 method id 平移为 method 名字符串(proc/protocol.go 的 Method* 常量)
|
||||
// - 版本协商改为握手帧里的 protocol 字段
|
||||
// - 版本协商改为握手帧里的 core_version / sdk_version 主版本比对
|
||||
//
|
||||
// 保留那些常量只会让人以为它们还在生效。
|
||||
|
||||
@ -26,6 +26,21 @@ func loadProcTemplate(t *testing.T) string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// extractShmFlagsOff 取出模板里 shmFlagsOff 函数的函数体,供断言其取值来源。
|
||||
func extractShmFlagsOff(t *testing.T, src string) string {
|
||||
t.Helper()
|
||||
const sig = "func shmFlagsOff() uint32 {"
|
||||
i := strings.Index(src, sig)
|
||||
if i < 0 {
|
||||
t.Fatal("模板缺少 shmFlagsOff()")
|
||||
}
|
||||
rest := src[i+len(sig):]
|
||||
if j := strings.Index(rest, "\n}"); j >= 0 {
|
||||
return rest[:j]
|
||||
}
|
||||
return rest
|
||||
}
|
||||
|
||||
// stripComments 去掉源码中的注释(用空白填充以保持偏移),只留可执行代码。
|
||||
func stripComments(t *testing.T, src string) string {
|
||||
t.Helper()
|
||||
@ -218,17 +233,18 @@ func TestProcTemplate_ShmLayoutMatchesKernel(t *testing.T) {
|
||||
|
||||
// 与内核 shm.go 的 offXxx 常量对齐(值比较,不依赖 gofmt 的对齐空白)
|
||||
layout := map[string]string{
|
||||
"shmOffMagic": "0",
|
||||
"shmOffVersion": "4",
|
||||
"shmOffArenaBase": "8",
|
||||
"shmOffArenaCap": "12",
|
||||
"shmOffArenaUsed": "16",
|
||||
"shmOffCtxBase": "20",
|
||||
"shmOffSeq": "24",
|
||||
// 与内核 stageFieldCount / sliceSize 对齐
|
||||
"shmStageFieldCount": "18",
|
||||
"shmOffMagic": "0",
|
||||
"shmOffFieldCount": "4",
|
||||
"shmOffArenaBase": "8",
|
||||
"shmOffArenaCap": "12",
|
||||
"shmOffArenaUsed": "16",
|
||||
"shmOffCtxBase": "20",
|
||||
"shmOffSeq": "24",
|
||||
// 与内核 stageFieldCount / sliceSize 对齐。
|
||||
// 注意:这里不校验人工版本号 —— 它已被删除,布局一致性
|
||||
// 改由描述符个数这种派生事实校验(见下方全局规约断言)。
|
||||
"shmStageFieldCount": "19",
|
||||
"shmSliceSize": "8",
|
||||
"shmVersion": "1",
|
||||
}
|
||||
constRe := func(name, want string) bool {
|
||||
// gofmt 会对齐常量块,故容许 name 与 = 之间有任意空白
|
||||
@ -241,6 +257,26 @@ func TestProcTemplate_ShmLayoutMatchesKernel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 布局必须自描述:标志位基址由内核下发的描述符个数推出,
|
||||
// 不能用本插件的编译期常量。否则内核加字段时会静默错位。
|
||||
if !strings.Contains(src, "func shmFieldCount()") {
|
||||
t.Error("模板缺少 shmFieldCount():标志位基址必须从 header 读取,不能自行推算")
|
||||
}
|
||||
flagsOff := extractShmFlagsOff(t, src)
|
||||
if strings.Contains(flagsOff, "shmStageFieldCount") {
|
||||
t.Errorf("shmFlagsOff 不得使用编译期常量 shmStageFieldCount(应经 shmFieldCount() 从 header 读取):\n%s", flagsOff)
|
||||
}
|
||||
if !strings.Contains(flagsOff, "shmFieldCount()") {
|
||||
t.Errorf("shmFlagsOff 应基于 shmFieldCount():\n%s", flagsOff)
|
||||
}
|
||||
|
||||
// 不得再出现人工版本号(内核与模板都不应有)。
|
||||
for _, banned := range []string{"shmVersion", "shmOffVersion"} {
|
||||
if strings.Contains(src, banned) {
|
||||
t.Errorf("模板不得再有 %s:布局一致性由描述符个数校验,不靠人工版本号", banned)
|
||||
}
|
||||
}
|
||||
|
||||
// 字段枚举顺序:内核 stageField 的前若干项
|
||||
fieldOrder := []string{
|
||||
"fRawMessage = iota", "fUserID", "fGroupID", "fLLMText",
|
||||
@ -343,15 +379,24 @@ func TestProcTemplate_DispatchesRequestsConcurrently(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 协议与共享内存区域版本/魔数不匹配必须拒绝,不得半兼容运行。
|
||||
func TestProcTemplate_RejectsVersionMismatch(t *testing.T) {
|
||||
// 主版本与共享内存区域结构不匹配必须拒绝,不得半兼容运行。
|
||||
//
|
||||
// 注意:这里不再检查任何「协议版本」/「共享段版本」自定号 ——
|
||||
// 它们已废除。兼容性只认双方版本号的**主版本段**(跨主版本即破坏性变更),
|
||||
// 布局一致性由描述符个数把关。
|
||||
func TestProcTemplate_RejectsIncompatiblePeer(t *testing.T) {
|
||||
src := loadProcTemplate(t)
|
||||
// §13.1 起共享段合并为单一「统一区域」,魔数校验文案随之更新。
|
||||
for _, want := range []string{"协议版本不匹配", "共享段版本不匹配", "统一区域魔数不匹配"} {
|
||||
for _, want := range []string{"主版本不匹配", "字段数不匹配", "统一区域魔数不匹配"} {
|
||||
if !strings.Contains(src, want) {
|
||||
t.Errorf("握手应校验并拒绝 %q", want)
|
||||
}
|
||||
}
|
||||
// 自定版本号必须已彻底移除,否则会出现「谁负责升这个号」的腐烂面。
|
||||
for _, banned := range []string{"procProtocolVersion", "shmVersion", "unifiedVersion"} {
|
||||
if strings.Contains(src, banned) {
|
||||
t.Errorf("模板不得再有自定版本号 %s", banned)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 全平台统一产出 plugin.bin。
|
||||
|
||||
@ -18,30 +18,38 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
sdk "github.com/JianFeeeee/homeagentsdk/sdk"
|
||||
)
|
||||
|
||||
// ---- 协议常量(须与内核 internal/plugin/proc/protocol.go 一致)----
|
||||
// majorVersion 取版本号的主版本段("1.4.0" → "1")。
|
||||
// 兼容性只认主版本:1.x 内的中版本跃迁都是纯追加,不调新面就不受影响;
|
||||
// 主版本跃迁才是破坏性变更(如 1.0.0 从 C ABI 改子进程)。
|
||||
func majorVersion(v string) string {
|
||||
v = strings.TrimPrefix(strings.TrimSpace(v), "v")
|
||||
if i := strings.IndexByte(v, '.'); i >= 0 {
|
||||
return v[:i]
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// procProtocolVersion 必须与内核的 proc.ProtocolVersion 完全一致。
|
||||
// ---- 协议常量(须与内核 internal/plugin/proc/protocol.go 一致)----
|
||||
//
|
||||
// v2:内核→插件的 payload 改用调用帧(tool/cleaner/output),媒体块改走
|
||||
// blocks_ref。v1 插件只读内联 args,遇上 v2 内核会拿到空参数;反过来 v2
|
||||
// 插件发 blocks_ref,v1 内核也会静默忽略。两边错配都不报错、只是静默失效,
|
||||
// 所以靠这个常量在握手上显式拦下。
|
||||
const procProtocolVersion = 2
|
||||
// 不自定协议号:兼容性由双方版本号的**主版本**表态(内核 meta.Version、
|
||||
// 插件 sdk.SDKVersion)。主版本相同即兼容(1.x 内全是纯追加),
|
||||
// 跨主版本则拒绝建链(主版本跃迁正是破坏性变更的定义)。
|
||||
// 布局一致性另由 field_count(描述符个数)把关。
|
||||
|
||||
// ---- 统一共享内存区域布局(与内核 internal/plugin/proc/unified.go 一致)----
|
||||
|
||||
const (
|
||||
unifiedMagic = 0x554D5352 // "UMSR" — Unified Memory Shared Region
|
||||
unifiedVersion = 1
|
||||
unifiedMagic = 0x554D5352 // "UMSR" — Unified Memory Shared Region
|
||||
superBlockSize = 64
|
||||
|
||||
sbOffMagic = 0
|
||||
sbOffVersion = 4
|
||||
sbOffReserved0 = 4 // 保留(原 version 槽位,布局不变以免动其余偏移)
|
||||
sbOffGeneration = 8
|
||||
sbOffCapacity = 16
|
||||
sbOffCtxOff = 20
|
||||
@ -59,16 +67,15 @@ const (
|
||||
shmStageFieldCount = 19
|
||||
shmSliceSize = 8
|
||||
|
||||
shmOffMagic = 0
|
||||
shmOffVersion = 4
|
||||
shmOffArenaBase = 8
|
||||
shmOffArenaCap = 12
|
||||
shmOffArenaUsed = 16
|
||||
shmOffCtxBase = 20
|
||||
shmOffSeq = 24
|
||||
shmOffMagic = 0
|
||||
shmOffFieldCount = 4 // 描述符个数,插件据此定位标志位(不再用自身常量推算)
|
||||
shmOffArenaBase = 8
|
||||
shmOffArenaCap = 12
|
||||
shmOffArenaUsed = 16
|
||||
shmOffCtxBase = 20
|
||||
shmOffSeq = 24
|
||||
|
||||
shmMagic = 0x48415348
|
||||
shmVersion = 2
|
||||
shmMagic = 0x48415348
|
||||
)
|
||||
|
||||
// 字段索引(顺序须与内核 stageField 枚举一致)
|
||||
@ -430,6 +437,13 @@ func shmArenaBase() uint32 { return shmU32(shmOffArenaBase) }
|
||||
func shmArenaCap() uint32 { return shmU32(shmOffArenaCap) }
|
||||
func shmCtxBase() uint32 { return shmU32(shmOffCtxBase) }
|
||||
|
||||
// shmFieldCount 返回内核写入的描述符个数。
|
||||
//
|
||||
// 标志位紧跟在描述符数组之后,其偏移**必须**由内核下发的个数推出,
|
||||
// 不能用本插件的编译期常量:内核加字段时两者会不一致,
|
||||
// 自行推算会静默错位(而握手时已按此值做过一致性检查)。
|
||||
func shmFieldCount() uint32 { return shmU32(shmOffFieldCount) }
|
||||
|
||||
func shmDescOff(field int) uint32 {
|
||||
return shmCtxBase() + uint32(field*shmSliceSize)
|
||||
}
|
||||
@ -446,7 +460,7 @@ func shmSetDesc(field int, off, ln uint32) {
|
||||
}
|
||||
|
||||
func shmFlagsOff() uint32 {
|
||||
return shmCtxBase() + uint32(shmStageFieldCount*shmSliceSize)
|
||||
return shmCtxBase() + shmFieldCount()*shmSliceSize
|
||||
}
|
||||
|
||||
func shmGetFlag(bit int) bool { return shm[shmFlagsOff()+uint32(bit)] != 0 }
|
||||
@ -1511,21 +1525,24 @@ func handleKernelRequest(req *rpcRequest) {
|
||||
|
||||
func handleHandshake(req *rpcRequest) {
|
||||
var p struct {
|
||||
Protocol int `json:"protocol"`
|
||||
ShmVersion uint32 `json:"shm_version"`
|
||||
CoreVersion string `json:"core_version"`
|
||||
FieldCount uint32 `json:"field_count"`
|
||||
ShmSize int `json:"shm_size"`
|
||||
PluginName string `json:"plugin_name"`
|
||||
EvtRingSize int `json:"evt_ring_size,omitempty"`
|
||||
}
|
||||
json.Unmarshal(req.Params, &p)
|
||||
|
||||
if p.Protocol != procProtocolVersion {
|
||||
respondErr(req.ID, fmt.Errorf("协议版本不匹配(内核 %d,插件 %d)——请用配套 plugindev 重编",
|
||||
p.Protocol, procProtocolVersion))
|
||||
// 主版本号是唯一兼容判据:跨主版本必是被破坏性变更分开的两端。
|
||||
if mine, theirs := majorVersion(sdk.SDKVersion), majorVersion(p.CoreVersion); mine != theirs {
|
||||
respondErr(req.ID, fmt.Errorf("主版本不匹配(内核 %s,插件 SDK %s)——请用配套 hmapdev 重编",
|
||||
p.CoreVersion, sdk.SDKVersion))
|
||||
return
|
||||
}
|
||||
if p.ShmVersion != shmVersion {
|
||||
respondErr(req.ID, fmt.Errorf("共享段版本不匹配(内核 %d,插件 %d)", p.ShmVersion, shmVersion))
|
||||
// 字段个数必须与编译期一致:它决定标志位基址,不一致会静默错位。
|
||||
if p.FieldCount != shmStageFieldCount {
|
||||
respondErr(req.ID, fmt.Errorf("StageContext 字段数不匹配(内核 %d,插件 %d)——请用配套 hmapdev 重编",
|
||||
p.FieldCount, shmStageFieldCount))
|
||||
return
|
||||
}
|
||||
if p.PluginName != "" {
|
||||
@ -1573,7 +1590,6 @@ func handleHandshake(req *rpcRequest) {
|
||||
}
|
||||
|
||||
respond(req.ID, map[string]interface{}{
|
||||
"protocol": procProtocolVersion,
|
||||
"sdk_version": sdk.SDKVersion,
|
||||
"plugin_name": pluginName,
|
||||
"pid": os.Getpid(),
|
||||
|
||||
Reference in New Issue
Block a user