github.com/goplus/gop@v1.2.6/x/langserver/serve_dial.go (about)

     1  /*
     2   * Copyright (c) 2023 The GoPlus Authors (goplus.org). All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package langserver
    18  
    19  import (
    20  	"context"
    21  	"io"
    22  	"io/fs"
    23  	"log"
    24  	"os"
    25  	"os/exec"
    26  	"strconv"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/goplus/gop/x/jsonrpc2/stdio"
    31  )
    32  
    33  func fatal(err error) {
    34  	log.Fatalln(err)
    35  }
    36  
    37  // -----------------------------------------------------------------------------
    38  
    39  // ServeAndDialConfig represents the configuration of ServeAndDial.
    40  type ServeAndDialConfig struct {
    41  	// OnError is to customize how to process errors (optional).
    42  	// It should panic in any case.
    43  	OnError func(err error)
    44  }
    45  
    46  const (
    47  	logPrefix = "serve-"
    48  	logSuffix = ".log"
    49  )
    50  
    51  func logFileOf(gopDir string, pid int) string {
    52  	return gopDir + logPrefix + strconv.Itoa(pid) + logSuffix
    53  }
    54  
    55  func isLog(fname string) bool {
    56  	return strings.HasSuffix(fname, logSuffix) && strings.HasPrefix(fname, logPrefix)
    57  }
    58  
    59  func pidByName(fname string) int {
    60  	pidText := fname[len(logPrefix) : len(fname)-len(logSuffix)]
    61  	if ret, err := strconv.ParseInt(pidText, 10, 0); err == nil {
    62  		return int(ret)
    63  	}
    64  	return -1
    65  }
    66  
    67  func killByPid(pid int) {
    68  	if pid < 0 {
    69  		return
    70  	}
    71  	if proc, err := os.FindProcess(pid); err == nil {
    72  		proc.Kill()
    73  	}
    74  }
    75  
    76  func tooOld(d fs.DirEntry) bool {
    77  	if fi, e := d.Info(); e == nil {
    78  		lastHour := time.Now().Add(-time.Hour)
    79  		return fi.ModTime().Before(lastHour)
    80  	}
    81  	return false
    82  }
    83  
    84  // ServeAndDial executes a command as a LangServer, makes a new connection to it
    85  // and returns a client of the LangServer based on the connection.
    86  func ServeAndDial(conf *ServeAndDialConfig, gopCmd string, args ...string) Client {
    87  	if conf == nil {
    88  		conf = new(ServeAndDialConfig)
    89  	}
    90  	onErr := conf.OnError
    91  	if onErr == nil {
    92  		onErr = fatal
    93  	}
    94  
    95  	home, err := os.UserHomeDir()
    96  	if err != nil {
    97  		onErr(err)
    98  	}
    99  	gopDir := home + "/.gop/"
   100  	err = os.MkdirAll(gopDir, 0755)
   101  	if err != nil {
   102  		onErr(err)
   103  	}
   104  
   105  	// logFile is where the LangServer application log saves to.
   106  	// default is ~/.gop/serve-{pid}.log
   107  	logFile := logFileOf(gopDir, os.Getpid())
   108  
   109  	// clean too old logfiles, and kill old LangServer processes
   110  	go func() {
   111  		if fis, e := os.ReadDir(gopDir); e == nil {
   112  			for _, fi := range fis {
   113  				if fi.IsDir() {
   114  					continue
   115  				}
   116  				if fname := fi.Name(); isLog(fname) && tooOld(fi) {
   117  					os.Remove(gopDir + fname)
   118  					killByPid(pidByName(fname))
   119  				}
   120  			}
   121  		}
   122  	}()
   123  
   124  	in, w := io.Pipe()
   125  	r, out := io.Pipe()
   126  
   127  	var cmd *exec.Cmd
   128  	go func() {
   129  		defer r.Close()
   130  		defer w.Close()
   131  
   132  		f, err := os.Create(logFile)
   133  		if err != nil {
   134  			onErr(err)
   135  		}
   136  		defer f.Close()
   137  
   138  		cmd = exec.Command(gopCmd, args...)
   139  		cmd.Stdin = r
   140  		cmd.Stdout = w
   141  		cmd.Stderr = f
   142  		err = cmd.Start()
   143  		if err != nil {
   144  			onErr(err)
   145  		}
   146  
   147  		newLogFile := logFileOf(gopDir, cmd.Process.Pid)
   148  		os.Rename(logFile, newLogFile)
   149  
   150  		cmd.Wait()
   151  	}()
   152  
   153  	c, err := Open(context.Background(), stdio.Dialer(in, out), func() {
   154  		if cmd == nil {
   155  			return
   156  		}
   157  		if proc := cmd.Process; proc != nil {
   158  			log.Println("==> ServeAndDial: kill", proc.Pid, gopCmd, args)
   159  			proc.Kill()
   160  		}
   161  	})
   162  	if err != nil {
   163  		onErr(err)
   164  	}
   165  	return c
   166  }
   167  
   168  // -----------------------------------------------------------------------------