-
Notifications
You must be signed in to change notification settings - Fork 7
feat(gateway): scaffold standalone gateway IPC with core.ping #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "flag" | ||
| "fmt" | ||
| "os" | ||
| "os/signal" | ||
| "syscall" | ||
|
|
||
| "neo-code/internal/gateway" | ||
| "neo-code/internal/gateway/adapters" | ||
| ) | ||
|
|
||
| // main 负责启动独立 Gateway 进程并监听本地 IPC 请求。 | ||
| func main() { | ||
| transportMode := flag.String("transport", "auto", "IPC transport mode: auto|npipe|uds") | ||
| endpoint := flag.String("endpoint", "", "IPC endpoint path (socket path or named pipe)") | ||
| flag.Parse() | ||
|
|
||
| ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | ||
| defer stop() | ||
|
|
||
| bootstrap, err := gateway.NewBootstrap(gateway.BootstrapConfig{ | ||
| Transport: *transportMode, | ||
| Endpoint: *endpoint, | ||
| }, adapters.NewCoreMock()) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "neocode-gateway: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| defer func() { | ||
| _ = bootstrap.Close() | ||
| }() | ||
|
|
||
| fmt.Fprintf(os.Stdout, "neocode-gateway listening on %s\n", bootstrap.Endpoint()) | ||
| if err := bootstrap.Run(ctx); err != nil && !errors.Is(err, context.Canceled) { | ||
| fmt.Fprintf(os.Stderr, "neocode-gateway: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package adapters | ||
|
|
||
| import "context" | ||
|
|
||
| const defaultPongMessage = "Pong" | ||
|
|
||
| // CoreClient 定义网关访问核心能力的最小端口接口。 | ||
| type CoreClient interface { | ||
| Ping(ctx context.Context) (string, error) | ||
| } | ||
|
|
||
| // CoreMock 是 CoreClient 的最小可运行 mock 实现。 | ||
| type CoreMock struct { | ||
| pong string | ||
| } | ||
|
|
||
| // NewCoreMock 创建默认返回 Pong 的核心 mock。 | ||
| func NewCoreMock() *CoreMock { | ||
| return &CoreMock{pong: defaultPongMessage} | ||
| } | ||
|
|
||
| // NewCoreMockWithPong 创建可自定义 Pong 文案的核心 mock。 | ||
| func NewCoreMockWithPong(pong string) *CoreMock { | ||
| if pong == "" { | ||
| pong = defaultPongMessage | ||
| } | ||
| return &CoreMock{pong: pong} | ||
| } | ||
|
|
||
| // Ping 返回 mock 端固定的 pong 结果。 | ||
| func (m *CoreMock) Ping(_ context.Context) (string, error) { | ||
| if m == nil { | ||
| return defaultPongMessage, nil | ||
| } | ||
| return m.pong, nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package adapters | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestCoreMockPing(t *testing.T) { | ||
| mock := NewCoreMock() | ||
| pong, err := mock.Ping(context.Background()) | ||
| if err != nil { | ||
| t.Fatalf("Ping() error = %v", err) | ||
| } | ||
| if pong != "Pong" { | ||
| t.Fatalf("Ping() = %s, want Pong", pong) | ||
| } | ||
| } | ||
|
|
||
| func TestCoreMockPingCustomMessage(t *testing.T) { | ||
| mock := NewCoreMockWithPong("Custom") | ||
| pong, err := mock.Ping(context.Background()) | ||
| if err != nil { | ||
| t.Fatalf("Ping() error = %v", err) | ||
| } | ||
| if pong != "Custom" { | ||
| t.Fatalf("Ping() = %s, want Custom", pong) | ||
| } | ||
| } | ||
|
|
||
| func TestCoreMockPingNilReceiver(t *testing.T) { | ||
| var mock *CoreMock | ||
| pong, err := mock.Ping(context.Background()) | ||
| if err != nil { | ||
| t.Fatalf("Ping() error = %v", err) | ||
| } | ||
| if pong != "Pong" { | ||
| t.Fatalf("Ping() = %s, want Pong", pong) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| package gateway | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "neo-code/internal/gateway/adapters" | ||
| "neo-code/internal/gateway/handlers" | ||
| "neo-code/internal/gateway/protocol" | ||
| "neo-code/internal/gateway/transport" | ||
| ) | ||
|
|
||
| // BootstrapConfig 描述网关进程启动时的最小配置。 | ||
| type BootstrapConfig struct { | ||
| Transport string | ||
| Endpoint string | ||
| } | ||
|
|
||
| // Bootstrap 负责装配并驱动 Gateway IPC 服务器。 | ||
| type Bootstrap struct { | ||
| server *transport.Server | ||
| } | ||
|
|
||
| // NewBootstrap 根据配置与 Core 端口装配可运行的网关实例。 | ||
| func NewBootstrap(cfg BootstrapConfig, core adapters.CoreClient) (*Bootstrap, error) { | ||
| if core == nil { | ||
| core = adapters.NewCoreMock() | ||
| } | ||
|
|
||
| router := protocol.NewRouter() | ||
| pingHandler := handlers.NewPingHandler(core) | ||
| router.Register(protocol.MethodCorePing, pingHandler.Handle) | ||
|
|
||
| server, err := transport.NewServer(transport.Config{ | ||
| Mode: transport.Mode(strings.TrimSpace(cfg.Transport)), | ||
| Endpoint: strings.TrimSpace(cfg.Endpoint), | ||
| }, router.HandleRaw) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("build gateway server: %w", err) | ||
| } | ||
|
|
||
| return &Bootstrap{server: server}, nil | ||
| } | ||
|
|
||
| // Endpoint 返回网关当前监听地址。 | ||
| func (b *Bootstrap) Endpoint() string { | ||
| if b == nil || b.server == nil { | ||
| return "" | ||
| } | ||
| return b.server.Endpoint() | ||
| } | ||
|
|
||
| // Run 启动网关服务并在上下文取消后退出。 | ||
| func (b *Bootstrap) Run(ctx context.Context) error { | ||
| if b == nil || b.server == nil { | ||
| return errors.New("gateway bootstrap is not initialized") | ||
| } | ||
| return b.server.Serve(ctx) | ||
| } | ||
|
|
||
| // Close 主动关闭网关服务监听器。 | ||
| func (b *Bootstrap) Close() error { | ||
| if b == nil || b.server == nil { | ||
| return nil | ||
| } | ||
| return b.server.Close() | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| package gateway | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "net" | ||
| "path/filepath" | ||
| "runtime" | ||
| "testing" | ||
| "time" | ||
| ) | ||
|
|
||
| func TestBootstrapPingPongOverIPC(t *testing.T) { | ||
| if runtime.GOOS == "windows" { | ||
| t.Skip("uds integration test runs on non-windows") | ||
| } | ||
|
|
||
| socketPath := filepath.Join(t.TempDir(), "gateway.sock") | ||
| bootstrap, err := NewBootstrap(BootstrapConfig{ | ||
| Transport: "uds", | ||
| Endpoint: socketPath, | ||
| }, nil) | ||
| if err != nil { | ||
| t.Fatalf("NewBootstrap() error = %v", err) | ||
| } | ||
| defer func() { | ||
| _ = bootstrap.Close() | ||
| }() | ||
|
|
||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| runDone := make(chan error, 1) | ||
| go func() { | ||
| runDone <- bootstrap.Run(ctx) | ||
| }() | ||
|
|
||
| conn := waitForGatewayDial(t, bootstrap.Endpoint()) | ||
| defer conn.Close() | ||
|
|
||
| encoder := json.NewEncoder(conn) | ||
| decoder := json.NewDecoder(conn) | ||
| if err := encoder.Encode(map[string]any{ | ||
| "jsonrpc": "2.0", | ||
| "id": "1", | ||
| "method": "core.ping", | ||
| }); err != nil { | ||
| t.Fatalf("Encode() error = %v", err) | ||
| } | ||
|
|
||
| var response map[string]any | ||
| if err := decoder.Decode(&response); err != nil { | ||
| t.Fatalf("Decode() error = %v", err) | ||
| } | ||
|
|
||
| if response["error"] != nil { | ||
| t.Fatalf("response error = %v, want nil", response["error"]) | ||
| } | ||
| if got := response["result"]; got != "Pong" { | ||
| t.Fatalf("response result = %v, want Pong", got) | ||
| } | ||
|
|
||
| cancel() | ||
| select { | ||
| case err := <-runDone: | ||
| if err != nil { | ||
| t.Fatalf("Run() error = %v", err) | ||
| } | ||
| case <-time.After(2 * time.Second): | ||
| t.Fatal("Run() did not exit after cancel") | ||
| } | ||
| } | ||
|
|
||
| func waitForGatewayDial(t *testing.T, endpoint string) net.Conn { | ||
| t.Helper() | ||
|
|
||
| deadline := time.Now().Add(2 * time.Second) | ||
| for time.Now().Before(deadline) { | ||
| conn, err := net.Dial("unix", endpoint) | ||
| if err == nil { | ||
| return conn | ||
| } | ||
| time.Sleep(20 * time.Millisecond) | ||
| } | ||
| t.Fatalf("dial gateway endpoint %s timeout", endpoint) | ||
| return nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package handlers | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "neo-code/internal/gateway/adapters" | ||
| "neo-code/internal/gateway/protocol" | ||
| ) | ||
|
|
||
| // PingHandler 负责处理 core.ping 请求并返回基础连通性结果。 | ||
| type PingHandler struct { | ||
| core adapters.CoreClient | ||
| } | ||
|
|
||
| // NewPingHandler 创建 ping 请求处理器。 | ||
| func NewPingHandler(core adapters.CoreClient) *PingHandler { | ||
| return &PingHandler{core: core} | ||
| } | ||
|
|
||
| // Handle 执行 ping 请求并把结果映射为 JSON-RPC 响应。 | ||
| func (h *PingHandler) Handle(ctx context.Context, req protocol.Request) protocol.Response { | ||
| if h == nil || h.core == nil { | ||
| return protocol.NewErrorResponse(req.ID, protocol.ErrorCodeInternalError, "core client unavailable") | ||
| } | ||
|
|
||
| pong, err := h.core.Ping(ctx) | ||
| if err != nil { | ||
| return protocol.NewErrorResponse(req.ID, protocol.ErrorCodeInternalError, "core ping failed") | ||
| } | ||
|
|
||
| return protocol.NewSuccessResponse(req.ID, pong) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package handlers | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "neo-code/internal/gateway/protocol" | ||
| ) | ||
|
|
||
| type coreClientStub struct { | ||
| pong string | ||
| err error | ||
| } | ||
|
|
||
| func (s coreClientStub) Ping(_ context.Context) (string, error) { | ||
| if s.err != nil { | ||
| return "", s.err | ||
| } | ||
| return s.pong, nil | ||
| } | ||
|
|
||
| func TestPingHandlerHandleSuccess(t *testing.T) { | ||
| handler := NewPingHandler(coreClientStub{pong: "Pong"}) | ||
| response := handler.Handle(context.Background(), protocol.Request{ID: []byte(`"1"`)}) | ||
|
|
||
| if response.Error != nil { | ||
| t.Fatalf("Handle() error = %+v, want nil", response.Error) | ||
| } | ||
| if response.Result != "Pong" { | ||
| t.Fatalf("Handle() result = %v, want Pong", response.Result) | ||
| } | ||
| } | ||
|
|
||
| func TestPingHandlerHandleCoreError(t *testing.T) { | ||
| handler := NewPingHandler(coreClientStub{err: errors.New("boom")}) | ||
| response := handler.Handle(context.Background(), protocol.Request{ID: []byte(`"1"`)}) | ||
|
|
||
| if response.Error == nil { | ||
| t.Fatal("Handle() error = nil, want internal error") | ||
| } | ||
| if response.Error.Code != protocol.ErrorCodeInternalError { | ||
| t.Fatalf("Handle() error code = %d, want %d", response.Error.Code, protocol.ErrorCodeInternalError) | ||
| } | ||
| } | ||
|
|
||
| func TestPingHandlerHandleNilCore(t *testing.T) { | ||
| handler := NewPingHandler(nil) | ||
| response := handler.Handle(context.Background(), protocol.Request{}) | ||
|
|
||
| if response.Error == nil { | ||
| t.Fatal("Handle() error = nil, want internal error") | ||
| } | ||
| if response.Error.Code != protocol.ErrorCodeInternalError { | ||
| t.Fatalf("Handle() error code = %d, want %d", response.Error.Code, protocol.ErrorCodeInternalError) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR adds a new runnable binary and flags (
neocode-gateway,--transport,--endpoint) but does not update user-facing docs. Per repo rules, please add README/docs entries for startup command and platform-specific default endpoints.