Repository: goplus/gop
Pull request #1522 (2023-11-07 04:38:58)

gop serve; jsonrpc2/stdio fix


Patch:
diff --git a/cmd/gop/main.go b/cmd/gop/main.go
index 9e106151..fd8a0d84 100644
--- a/cmd/gop/main.go
+++ b/cmd/gop/main.go
@@ -38,6 +38,7 @@ import (
 	"github.com/goplus/gop/cmd/internal/install"
 	"github.com/goplus/gop/cmd/internal/mod"
 	"github.com/goplus/gop/cmd/internal/run"
+	"github.com/goplus/gop/cmd/internal/serve"
 	"github.com/goplus/gop/cmd/internal/test"
 	"github.com/goplus/gop/cmd/internal/version"
 	"github.com/goplus/gop/cmd/internal/watch"
@@ -63,6 +64,7 @@ func init() {
 		clean.Cmd,
 		// list.Cmd,
 		// deps.Cmd,
+		serve.Cmd,
 		watch.Cmd,
 		env.Cmd,
 		c2go.Cmd,
diff --git a/cmd/internal/serve/serve.go b/cmd/internal/serve/serve.go
new file mode 100644
index 00000000..42f5f94f
--- /dev/null
+++ b/cmd/internal/serve/serve.go
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2023 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Package serve implements the “gop serve command.
+package serve
+
+import (
+	"context"
+
+	"github.com/goplus/gop/cmd/internal/base"
+	"github.com/goplus/gop/x/jsonrpc2"
+	"github.com/goplus/gop/x/jsonrpc2/stdio"
+	"github.com/goplus/gop/x/langserver"
+	"github.com/qiniu/x/log"
+)
+
+// gop serve
+var Cmd = &base.Command{
+	UsageLine: "gop serve [flags]",
+	Short:     "Serve as a Go+ LangServer",
+}
+
+var (
+	flag        = &Cmd.Flag
+	flagVerbose = flag.Bool("v", false, "print verbose information")
+)
+
+func init() {
+	Cmd.Run = runCmd
+}
+
+func runCmd(cmd *base.Command, args []string) {
+	err := flag.Parse(args)
+	if err != nil {
+		log.Fatalln("parse input arguments failed:", err)
+	}
+
+	if *flagVerbose {
+		jsonrpc2.SetDebug(jsonrpc2.DbgFlagCall)
+	}
+
+	listener := stdio.Listener(false)
+	defer listener.Close()
+
+	server := langserver.NewServer(context.Background(), listener, nil)
+	server.Wait()
+}
+
+// -----------------------------------------------------------------------------
diff --git a/x/jsonrpc2/stdio/client.go b/x/jsonrpc2/stdio/client.go
new file mode 100644
index 00000000..9a8b86cd
--- /dev/null
+++ b/x/jsonrpc2/stdio/client.go
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2023 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package stdio
+
+import (
+	"context"
+	"io"
+	"log"
+	"net"
+	"sync/atomic"
+
+	"github.com/goplus/gop/x/jsonrpc2"
+)
+
+// -----------------------------------------------------------------------------
+
+type dialConn struct {
+	in     io.ReadCloser
+	out    io.WriteCloser
+	closed bool
+}
+
+func (p *dialConn) Read(b []byte) (n int, err error) {
+	if p.closed {
+		return 0, net.ErrClosed
+	}
+	return p.in.Read(b)
+}
+
+func (p *dialConn) Write(b []byte) (n int, err error) {
+	if p.closed {
+		return 0, net.ErrClosed
+	}
+	return p.out.Write(b)
+}
+
+var (
+	dailCnt int32
+)
+
+func (p *dialConn) Close() error {
+	if jsonrpc2.Verbose {
+		log.Println("==> stdio.dialConn.Close")
+	}
+	if p.closed {
+		return net.ErrClosed
+	}
+	p.closed = true
+	atomic.AddInt32(&dailCnt, -1)
+	p.in.Close()
+	p.out.Close()
+	return nil
+}
+
+// Dial returns a new communication byte stream to a listening server.
+func (p *dialConn) Dial(ctx context.Context) (io.ReadWriteCloser, error) {
+	if atomic.AddInt32(&dailCnt, 1) != 1 {
+		atomic.AddInt32(&dailCnt, -1)
+		return nil, ErrTooManyConnections
+	}
+	return p, nil
+}
+
+// Dialer returns a jsonrpc2.Dialer based on stdin and stdout.
+func Dialer(in io.ReadCloser, out io.WriteCloser) jsonrpc2.Dialer {
+	return &dialConn{in: in, out: out}
+}
+
+// Dial makes a new connection based on stdin and stdout, wraps the returned
+// reader and writer using the framer to make a stream, and then builds a
+// connection on top of that stream using the binder.
+//
+// The returned Connection will operate independently using the Preempter and/or
+// Handler provided by the Binder, and will release its own resources when the
+// connection is broken, but the caller may Close it earlier to stop accepting
+// (or sending) new requests.
+func Dial(in io.ReadCloser, out io.WriteCloser, binder jsonrpc2.Binder) (*jsonrpc2.Connection, error) {
+	return jsonrpc2.Dial(context.Background(), Dialer(in, out), binder)
+}
+
+// -----------------------------------------------------------------------------
diff --git a/x/jsonrpc2/stdio/stdio.go b/x/jsonrpc2/stdio/server.go
similarity index 72%
rename from x/jsonrpc2/stdio/stdio.go
rename to x/jsonrpc2/stdio/server.go
index c1f2c6e5..9e4969dd 100644
--- a/x/jsonrpc2/stdio/stdio.go
+++ b/x/jsonrpc2/stdio/server.go
@@ -39,11 +39,11 @@ var (
 	connCnt int32
 )
 
-type conn struct {
+type serverConn struct {
 	closed bool
 }
 
-func (p *conn) Read(b []byte) (n int, err error) {
+func (p *serverConn) Read(b []byte) (n int, err error) {
 	if p.closed {
 		return 0, net.ErrClosed
 	}
@@ -55,14 +55,14 @@ func (p *conn) Read(b []byte) (n int, err error) {
 	return
 }
 
-func (p *conn) Write(b []byte) (n int, err error) {
+func (p *serverConn) Write(b []byte) (n int, err error) {
 	if p.closed {
 		return 0, net.ErrClosed
 	}
 	return os.Stdout.Write(b)
 }
 
-func (p *conn) Close() error {
+func (p *serverConn) Close() error {
 	if jsonrpc2.Verbose {
 		log.Println("==> stdio.conn.Close")
 	}
@@ -74,15 +74,6 @@ func (p *conn) Close() error {
 	return nil
 }
 
-// Dial returns a new communication byte stream to a listening server.
-func (p *conn) Dial(ctx context.Context) (io.ReadWriteCloser, error) {
-	if atomic.AddInt32(&connCnt, 1) != 1 {
-		atomic.AddInt32(&connCnt, -1)
-		return nil, ErrTooManyConnections
-	}
-	return p, nil
-}
-
 // -----------------------------------------------------------------------------
 
 type newConn struct {
@@ -148,13 +139,9 @@ func (p *listener) Accept(context.Context) (io.ReadWriteCloser, error) {
 		atomic.AddInt32(&connCnt, -1)
 		return nil, ErrTooManyConnections
 	}
-	return new(conn), nil
+	return new(serverConn), nil
 }
 
-var (
-	dailCnt int32
-)
-
 // Dial returns a new communication byte stream to a listening server.
 func (p *listener) Dial(ctx context.Context) (ret io.ReadWriteCloser, err error) {
 	if atomic.AddInt32(&dailCnt, 1) != 1 {
@@ -183,27 +170,6 @@ func (p *listener) Dialer() jsonrpc2.Dialer {
 	return nil
 }
 
-// -----------------------------------------------------------------------------
-
-// Dialer returns a jsonrpc2.Dialer based on stdin and stdout.
-func Dialer() jsonrpc2.Dialer {
-	return new(conn)
-}
-
-// Dial makes a new connection based on stdin and stdout, wraps the returned
-// reader and writer using the framer to make a stream, and then builds a
-// connection on top of that stream using the binder.
-//
-// The returned Connection will operate independently using the Preempter and/or
-// Handler provided by the Binder, and will release its own resources when the
-// connection is broken, but the caller may Close it earlier to stop accepting
-// (or sending) new requests.
-func Dial(binder jsonrpc2.Binder) (*jsonrpc2.Connection, error) {
-	return jsonrpc2.Dial(context.Background(), Dialer(), binder)
-}
-
-// -----------------------------------------------------------------------------
-
 // Listener returns a jsonrpc2.Listener based on stdin and stdout.
 func Listener(allowLocalDail bool) jsonrpc2.Listener {
 	l := &listener{localDail: allowLocalDail}
diff --git a/x/langserver/client.go b/x/langserver/client.go
new file mode 100644
index 00000000..37db9e1c
--- /dev/null
+++ b/x/langserver/client.go
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2023 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package langserver
+
+import (
+	"context"
+
+	"github.com/goplus/gop/x/jsonrpc2"
+)
+
+// -----------------------------------------------------------------------------
+
+const (
+	methodGenGo    = "gengo"
+	methodChanged  = "changed"
+	methodShutdown = "shutdown"
+)
+
+// -----------------------------------------------------------------------------
+
+// Dialer is used by clients to dial a server.
+type Dialer = jsonrpc2.Dialer
+
+type AsyncCall = jsonrpc2.AsyncCall
+
+// Client is a client of the language server.
+type Client struct {
+	conn *jsonrpc2.Connection
+}
+
+// Dial uses the dialer to make a new connection and returns a client of the language server
+// based on the connection.
+func Dial(ctx context.Context, dialer Dialer) (ret Client, err error) {
+	c, err := jsonrpc2.Dial(ctx, dialer, jsonrpc2.BinderFunc(
+		func(ctx context.Context, c *jsonrpc2.Connection) (ret jsonrpc2.ConnectionOptions) {
+			return
+		}))
+	if err != nil {
+		return
+	}
+	ret = Client{c}
+	return
+}
+
+func (p Client) AsyncGenGo(ctx context.Context, pattern ...string) *AsyncCall {
+	return p.conn.Call(ctx, methodGenGo, pattern)
+}
+
+func (p Client) GenGo(ctx context.Context, pattern ...string) (err error) {
+	return p.AsyncGenGo(ctx, pattern...).Await(ctx, nil)
+}
+
+func (p Client) Changed(ctx context.Context, files ...string) (err error) {
+	return p.conn.Notify(ctx, methodChanged, files)
+}
+
+func (p Client) Shutdown(ctx context.Context) (err error) {
+	return p.conn.Notify(ctx, methodShutdown, nil)
+}
+
+// -----------------------------------------------------------------------------
diff --git a/x/langserver/langserver.go b/x/langserver/langserver.go
new file mode 100644
index 00000000..551fee12
--- /dev/null
+++ b/x/langserver/langserver.go
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2023 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package langserver
+
+import (
+	"context"
+	"encoding/json"
+	"log"
+
+	"github.com/goplus/gop"
+	"github.com/goplus/gop/x/gopprojs"
+	"github.com/goplus/gop/x/jsonrpc2"
+)
+
+// -----------------------------------------------------------------------------
+
+// Listener is implemented by protocols to accept new inbound connections.
+type Listener = jsonrpc2.Listener
+
+// Server is a running server that is accepting incoming connections.
+type Server = jsonrpc2.Server
+
+// Config holds the options for new connections.
+type Config struct {
+	// Framer allows control over the message framing and encoding.
+	// If nil, HeaderFramer will be used.
+	Framer jsonrpc2.Framer
+}
+
+func NewServer(ctx context.Context, listener Listener, conf *Config) (ret *Server) {
+	h := new(handler)
+	ret = jsonrpc2.NewServer(ctx, listener, jsonrpc2.BinderFunc(
+		func(ctx context.Context, c *jsonrpc2.Connection) (ret jsonrpc2.ConnectionOptions) {
+			if conf != nil {
+				ret.Framer = conf.Framer
+			}
+			ret.Handler = h
+			ret.OnInternalError = h.OnInternalError
+			return
+		}))
+	h.server = ret
+	return
+}
+
+// -----------------------------------------------------------------------------
+
+type handler struct {
+	server *Server
+}
+
+func (p *handler) OnInternalError(err error) {
+	panic("jsonrpc2: " + err.Error())
+}
+
+func (p *handler) Changed(files []string) {
+	log.Println("Changed:", files)
+}
+
+func (p *handler) Handle(ctx context.Context, req *jsonrpc2.Request) (result interface{}, err error) {
+	switch req.Method {
+	case methodChanged:
+		var files []string
+		err = json.Unmarshal(req.Params, &files)
+		if err != nil {
+			return
+		}
+		p.Changed(files)
+	case methodGenGo:
+		var pattern []string
+		err = json.Unmarshal(req.Params, &pattern)
+		if err != nil {
+			return
+		}
+		err = GenGo(pattern...)
+	case methodShutdown:
+		p.server.Shutdown()
+	}
+	return
+}
+
+func GenGo(pattern ...string) (err error) {
+	projs, err := gopprojs.ParseAll(pattern...)
+	if err != nil {
+		return
+	}
+	for _, proj := range projs {
+		switch v := proj.(type) {
+		case *gopprojs.DirProj:
+			gop.GenGoEx(v.Dir, nil, true, 0)
+		case *gopprojs.PkgPathProj:
+			if v.Path == "builtin" {
+				continue
+			}
+			gop.GenGoPkgPathEx("", v.Path, nil, true, 0)
+		}
+	}
+	return
+}
+
+// -----------------------------------------------------------------------------
diff --git a/x/typesutil/check.go b/x/typesutil/check.go
index ae5947fd..92ee7ddd 100644
--- a/x/typesutil/check.go
+++ b/x/typesutil/check.go
@@ -164,6 +164,7 @@ func (p *Checker) Files(goFiles []*goast.File, gopFiles []*ast.File) (err error)
 			log.Println("typesutil.Check err:", err)
 			log.SingleStack()
 		}
+		return
 	}
 	if len(files) > 0 {
 		scope := pkgTypes.Scope()


Test patch:
diff --git a/x/jsonrpc2/stdio/jsonrpc2_test.go b/x/jsonrpc2/stdio/jsonrpc2_test.go
index d3207ebc..b8fd7888 100644
--- a/x/jsonrpc2/stdio/jsonrpc2_test.go
+++ b/x/jsonrpc2/stdio/jsonrpc2_test.go
@@ -6,6 +6,7 @@ package stdio_test
 
 import (
 	"context"
+	"io"
 	"testing"
 
 	"github.com/goplus/gop/x/jsonrpc2"
@@ -14,14 +15,18 @@ import (
 )
 
 func TestStdio(t *testing.T) {
-	jsonrpc2.SetDebug(jsonrpc2.DbgFlagAll)
 	ctx := context.Background()
 	listener := stdio.Listener(true)
 	cases.Test(t, ctx, listener, nil, false)
 }
 
 func TestDial(t *testing.T) {
-	stdio.Dial(jsonrpc2.BinderFunc(func(ctx context.Context, c *jsonrpc2.Connection) jsonrpc2.ConnectionOptions {
-		return jsonrpc2.ConnectionOptions{}
-	}))
+	r, w := io.Pipe()
+	stdio.Dial(r, w, jsonrpc2.BinderFunc(
+		func(ctx context.Context, c *jsonrpc2.Connection) jsonrpc2.ConnectionOptions {
+			return jsonrpc2.ConnectionOptions{}
+		}))
+	if _, err := stdio.Dialer(r, w).Dial(context.Background()); err != stdio.ErrTooManyConnections {
+		t.Fatal("stdio.Dialer:", err)
+	}
 }
diff --git a/x/jsonrpc2/stdio/stdio_test.go b/x/jsonrpc2/stdio/stdio_test.go
index d9f02927..b5bc4f72 100644
--- a/x/jsonrpc2/stdio/stdio_test.go
+++ b/x/jsonrpc2/stdio/stdio_test.go
@@ -18,15 +18,38 @@ package stdio
 
 import (
 	"context"
+	"io"
 	"net"
+	"sync/atomic"
 	"testing"
 
 	"github.com/goplus/gop/x/jsonrpc2"
 )
 
-func TestConn(t *testing.T) {
+func init() {
 	jsonrpc2.SetDebug(jsonrpc2.DbgFlagAll)
-	c := new(conn)
+}
+
+func TestServerConn(t *testing.T) {
+	c := &serverConn{}
+	atomic.AddInt32(&connCnt, 1)
+	if err := c.Close(); err != nil {
+		t.Fatal("Close failed:", err)
+	}
+	if err := c.Close(); err != net.ErrClosed {
+		t.Fatal("Close:", err)
+	}
+	if _, err := c.Read(nil); err != net.ErrClosed {
+		t.Fatal("Read:", err)
+	}
+	if _, err := c.Write(nil); err != net.ErrClosed {
+		t.Fatal("Write:", err)
+	}
+}
+
+func TestDialConn(t *testing.T) {
+	r, w := io.Pipe()
+	c := &dialConn{in: r, out: w}
 	c.Dial(context.Background())
 	if err := c.Close(); err != nil {
 		t.Fatal("Close failed:", err)
@@ -40,6 +63,8 @@ func TestConn(t *testing.T) {
 	if _, err := c.Write(nil); err != net.ErrClosed {
 		t.Fatal("Write:", err)
 	}
+	c.closed = false
+	c.Write(nil)
 }
 
 func TestListener(t *testing.T) {