Skip to content

Commit

Permalink
Merge pull request #1527 from xushiwei/rpc
Browse files Browse the repository at this point in the history
langserver: support Changed files
  • Loading branch information
xushiwei committed Nov 7, 2023
2 parents bf9166f + d7fdfc4 commit f97f50f
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 31 deletions.
16 changes: 10 additions & 6 deletions x/jsonrpc2/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ func (c *Connection) Notify(ctx context.Context, method string, params interface
}()

if debugCall {
log.Println("==> Connection.Notify", method, "params:", params)
log.Println("Notify", method, "params:", params)
}

c.updateInFlight(func(s *inFlightState) {
Expand Down Expand Up @@ -315,7 +315,7 @@ func (c *Connection) Notify(ctx context.Context, method string, params interface

err = c.write(ctx, notify)
if debugCall {
log.Println("==> Connection.write:", notify.Method, err)
log.Println("Connection.write", notify.Method, err)
}
return
}
Expand All @@ -327,7 +327,7 @@ func (c *Connection) Notify(ctx context.Context, method string, params interface
// If sending the call failed, the response will be ready and have the error in it.
func (c *Connection) Call(ctx context.Context, method string, params interface{}) *AsyncCall {
if debugCall {
log.Println("==> Connection.Call", method, "params:", params)
log.Println("Call", method, "params:", params)
}
// Generate a new request identifier.
id := Int64ID(atomic.AddInt64(&c.seq, 1))
Expand Down Expand Up @@ -362,7 +362,7 @@ func (c *Connection) Call(ctx context.Context, method string, params interface{}

err = c.write(ctx, call)
if debugCall {
log.Println("==> Connection.write:", call.ID, call.Method, err)
log.Println("Connection.write", call.ID, call.Method, err)
}
if err != nil {
// Sending failed. We will never get a response, so deliver a fake one if it
Expand Down Expand Up @@ -693,7 +693,11 @@ func (c *Connection) handleAsync() {

if debugCall {
req := req.Request
log.Println("==> handleAsync", req.ID, req.Method, string(req.Params))
id := req.ID
if !id.IsValid() {
id = StringID("")
}
log.Println("handleAsync", id, req.Method, string(req.Params))
}
result, err := c.handler.Handle(req.ctx, req.Request)
c.processResult(c.handler, req, result, err)
Expand Down Expand Up @@ -721,7 +725,7 @@ func (c *Connection) processResult(from interface{}, req *incomingRequest, resul
if req.IsCall() {
response, respErr := NewResponse(req.ID, result, err)
if debugCall {
log.Println("==> processResult", response.ID, string(response.Result), response.Error)
log.Println("processResult", response.ID, string(response.Result), response.Error)
}

// The caller could theoretically reuse the request's ID as soon as we've
Expand Down
112 changes: 91 additions & 21 deletions x/langserver/serve_dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ package langserver
import (
"context"
"io"
"io/fs"
"log"
"os"
"os/exec"
"strconv"
"strings"
"time"

"github.com/goplus/gop/x/jsonrpc2/stdio"
)
Expand All @@ -34,43 +38,93 @@ func fatal(err error) {

// ServeAndDialConfig represents the configuration of ServeAndDial.
type ServeAndDialConfig struct {
// LogFile is where the LangServer application log saves to (optional).
// Default is ~/.gop/serve.log
LogFile string

// OnError is to customize how to process errors (optional).
// It should panic in any case.
OnError func(err error)
}

const (
logPrefix = "serve-"
logSuffix = ".log"
)

func logFileOf(gopDir string, pid int) string {
return gopDir + logPrefix + strconv.Itoa(pid) + logSuffix
}

func isLog(fname string) bool {
return strings.HasSuffix(fname, logSuffix) && strings.HasPrefix(fname, logPrefix)
}

func pidByName(fname string) int {
pidText := fname[len(logPrefix) : len(fname)-len(logSuffix)]
if ret, err := strconv.ParseInt(pidText, 10, 0); err == nil {
return int(ret)
}
return -1
}

func killByPid(pid int) {
if pid < 0 {
return
}
if proc, err := os.FindProcess(pid); err == nil {
proc.Kill()
}
}

func tooOld(d fs.DirEntry) bool {
if fi, e := d.Info(); e == nil {
lastHour := time.Now().Add(-time.Hour)
return fi.ModTime().Before(lastHour)
}
return false
}

// ServeAndDial executes a command as a LangServer, makes a new connection to it
// and returns a client of the LangServer based on the connection.
func ServeAndDial(conf *ServeAndDialConfig, cmd string, args ...string) Client {
func ServeAndDial(conf *ServeAndDialConfig, gopCmd string, args ...string) Client {
if conf == nil {
conf = new(ServeAndDialConfig)
}
onErr := conf.OnError
if onErr == nil {
onErr = fatal
}
logFile := conf.LogFile
if logFile == "" {
home, err := os.UserHomeDir()
if err != nil {
onErr(err)
}
gopDir := home + "/.gop"
err = os.MkdirAll(gopDir, 0755)
if err != nil {
onErr(err)
}
logFile = gopDir + "/serve.log"

home, err := os.UserHomeDir()
if err != nil {
onErr(err)
}
gopDir := home + "/.gop/"
err = os.MkdirAll(gopDir, 0755)
if err != nil {
onErr(err)
}

// logFile is where the LangServer application log saves to.
// default is ~/.gop/serve-{pid}.log
logFile := logFileOf(gopDir, os.Getpid())

// clean too old logfiles, and kill old LangServer processes
go func() {
if fis, e := os.ReadDir(gopDir); e == nil {
for _, fi := range fis {
if fi.IsDir() {
continue
}
if fname := fi.Name(); isLog(fname) && tooOld(fi) {
os.Remove(gopDir + fname)
killByPid(pidByName(fname))
}
}
}
}()

in, w := io.Pipe()
r, out := io.Pipe()

ctx, cancel := context.WithCancel(context.Background())
var cmd *exec.Cmd
go func() {
defer r.Close()
defer w.Close()
Expand All @@ -81,14 +135,30 @@ func ServeAndDial(conf *ServeAndDialConfig, cmd string, args ...string) Client {
}
defer f.Close()

cmd := exec.CommandContext(ctx, cmd, args...)
cmd = exec.Command(gopCmd, args...)
cmd.Stdin = r
cmd.Stdout = w
cmd.Stderr = f
cmd.Run()
err = cmd.Start()
if err != nil {
onErr(err)
}

newLogFile := logFileOf(gopDir, cmd.Process.Pid)
os.Rename(logFile, newLogFile)

cmd.Wait()
}()

c, err := Open(ctx, stdio.Dialer(in, out), cancel)
c, err := Open(context.Background(), stdio.Dialer(in, out), func() {
if cmd == nil {
return
}
if proc := cmd.Process; proc != nil {
log.Println("==> ServeAndDial: kill", proc.Pid, gopCmd, args)
proc.Kill()
}
})
if err != nil {
onErr(err)
}
Expand Down
50 changes: 46 additions & 4 deletions x/langserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ package langserver
import (
"context"
"encoding/json"
"log"
"path/filepath"
"sync"
"time"

"github.com/goplus/gop"
"github.com/goplus/gop/x/gopprojs"
Expand All @@ -43,32 +45,72 @@ type Config struct {

// NewServer creates a new LangServer and returns it.
func NewServer(ctx context.Context, listener Listener, conf *Config) (ret *Server) {
h := new(handler)
h := newHandle()
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
// ret.OnInternalError = h.OnInternalError
return
}))
h.server = ret
go h.runLoop()
return
}

// -----------------------------------------------------------------------------

type none = struct{}

type handler struct {
mutex sync.Mutex
dirty map[string]none

server *Server
}

func newHandle() *handler {
return &handler{
dirty: make(map[string]none),
}
}

/*
func (p *handler) OnInternalError(err error) {
panic("jsonrpc2: " + err.Error())
}
*/

func (p *handler) runLoop() {
const (
duration = time.Second / 100
)
for {
var dir string
p.mutex.Lock()
for dir = range p.dirty {
delete(p.dirty, dir)
break
}
p.mutex.Unlock()
if dir == "" {
time.Sleep(duration)
continue
}
gop.GenGoEx(dir, nil, true, gop.GenFlagPrompt)
}
}

func (p *handler) Changed(files []string) {
log.Println("Changed:", files)
p.mutex.Lock()
defer p.mutex.Unlock()

for _, file := range files {
dir := filepath.Dir(file)
p.dirty[dir] = none{}
}
}

func (p *handler) Handle(ctx context.Context, req *jsonrpc2.Request) (result interface{}, err error) {
Expand Down

0 comments on commit f97f50f

Please sign in to comment.