Skip to content
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

langserver: support Changed files #1527

Merged
merged 2 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading