alroyso 1 年之前
父節點
當前提交
3d746a2f1b
共有 11 個文件被更改,包括 516 次插入0 次删除
  1. 4 0
      .gitignore
  2. 8 0
      .idea/.gitignore
  3. 9 0
      .idea/clash-for-flutter-service.iml
  4. 8 0
      .idea/modules.xml
  5. 6 0
      .idea/vcs.xml
  6. 50 0
      Makefile
  7. 5 0
      constant/constant.go
  8. 10 0
      go.mod
  9. 7 0
      go.sum
  10. 179 0
      main.go
  11. 230 0
      server/server.go

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+bin
+.DS_Store
+
+vendor

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 9 - 0
.idea/clash-for-flutter-service.iml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="Go" enabled="true" />
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/clash-for-flutter-service.iml" filepath="$PROJECT_DIR$/.idea/clash-for-flutter-service.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 50 - 0
Makefile

@@ -0,0 +1,50 @@
+# reference https://github.com/Dreamacro/clash/blob/master/Makefile
+
+NAME=naiyou-service
+BINDIR=bin
+VERSION=$(shell git describe --tags || echo "unknown version")
+GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-w -s -X "github.com/alroyso/clash-for-flutter-service/constant.Version=$(VERSION)"'
+
+PLATFORM_LIST = \
+	darwin-amd64 \
+	darwin-arm64 \
+	linux-amd64 \
+	linux-arm64
+
+WINDOWS_ARCH_LIST = \
+	windows-amd64 \
+	windows-arm64
+
+all: darwin-amd64 darwin-arm64 linux-amd64 linux-arm64 windows-amd64 windows-arm64
+
+darwin-amd64:
+	GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+darwin-arm64:
+	GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-amd64:
+	GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-arm64:
+	GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+windows-amd64:
+	GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
+
+windows-arm64:
+	GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
+
+
+
+gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
+zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
+
+$(gz_releases): %.gz : %
+	chmod +x $(BINDIR)/$(NAME)-$(basename $@)
+	gzip -f -S -$(VERSION).gz $(BINDIR)/$(NAME)-$(basename $@)
+
+$(zip_releases): %.zip : %
+	zip -m -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe
+
+releases: $(gz_releases) $(zip_releases)

+ 5 - 0
constant/constant.go

@@ -0,0 +1,5 @@
+package constant
+
+var (
+	Version string = "0.0.0"
+)

+ 10 - 0
go.mod

@@ -0,0 +1,10 @@
+module github.com/alroyso/clash-for-flutter-service
+
+go 1.17
+
+require (
+	github.com/gorilla/websocket v1.5.0
+	github.com/kardianos/service v1.2.1
+)
+
+require golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect

+ 7 - 0
go.sum

@@ -0,0 +1,7 @@
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/kardianos/service v1.2.1 h1:AYndMsehS+ywIS6RB9KOlcXzteWUzxgMgBymJD7+BYk=
+github.com/kardianos/service v1.2.1/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
+golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 179 - 0
main.go

@@ -0,0 +1,179 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/alroyso/clash-for-flutter-service/constant"
+	"github.com/alroyso/clash-for-flutter-service/server"
+
+	"github.com/kardianos/service"
+)
+
+var Service service.Service
+
+type program struct{}
+
+var (
+	port int64
+)
+
+func (p *program) Start(s service.Service) error {
+	go p.run()
+	fmt.Println("Start success")
+	return nil
+}
+
+func (p *program) run() {
+	err := server.StartServer(port)
+	if err != nil {
+		fmt.Println(err)
+		if strings.Contains(err.Error(), "http: Server closed") {
+			return
+		}
+		if strings.Contains(err.Error(), "bind: address already in use") {
+			if os.Args[1] == "service-mode" {
+				Service.Stop()
+			} else {
+				p.Stop(Service)
+				os.Exit(101)
+			}
+		}
+
+	}
+}
+
+func (p *program) Stop(s service.Service) error {
+	if server.Server != nil {
+		server.Server.Shutdown(context.TODO())
+	}
+	if server.Cmd != nil {
+		server.Cmd.Process.Kill()
+		server.Cmd.Process.Wait()
+	}
+	fmt.Println("Stop success")
+	return nil
+}
+
+func main() {
+	//// 使用 flag 包为全局变量 port 定义命令行参数
+	flag.Int64Var(&port, "port", 9899, "The port number to listen on")
+	// 解析命令行参数
+	flag.Parse()
+
+	args := flag.Args()
+	if len(args) <= 1 {
+		fmt.Println("Please use command: install, uninstall, status, start, stop, restart, version, service-mode, user-mode")
+		return
+	}
+
+	svcConfig := &service.Config{
+		Name:        "naiyou-core-service",
+		DisplayName: "naiyou core Service",
+		Description: "This is a naiyou core Service.",
+		Arguments:   []string{"service-mode"},
+	}
+
+	prg := &program{}
+	s, err := service.New(prg, svcConfig)
+	Service = s
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	for _, it := range args {
+		switch it {
+		case "install":
+			handleInstall(s)
+		case "uninstall":
+			handleUnInstall(s)
+		case "status":
+			handleStatus(s)
+		case "start":
+			handleStart(s)
+		case "stop":
+			handleStop(s)
+		case "restart":
+			handleRestart(s)
+		case "version":
+			handleVersion(s)
+		case "service-mode":
+			handleRun(s)
+		case "user-mode":
+			handleRun(s)
+		default:
+			fmt.Println("Command does not exist")
+		}
+	}
+
+}
+
+func handleInstall(s service.Service) {
+	err := s.Install()
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println("Install Service Success")
+	}
+}
+
+func handleUnInstall(s service.Service) {
+	err := s.Uninstall()
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println("UnInstall Service Success")
+	}
+}
+func handleStatus(s service.Service) {
+	status, err := s.Status()
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		switch status {
+		case service.StatusRunning:
+			fmt.Println("Service Status is runing")
+		case service.StatusStopped:
+			fmt.Println("Service Status is stoped")
+		default:
+			fmt.Println("Service Status is unknow")
+		}
+	}
+}
+func handleStart(s service.Service) {
+	err := s.Start()
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println("Service Start Success ")
+	}
+}
+func handleStop(s service.Service) {
+	err := s.Stop()
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println("Service Stop Success")
+	}
+}
+func handleRestart(s service.Service) {
+	err := s.Restart()
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println("Service Restart Success")
+	}
+}
+func handleVersion(s service.Service) {
+	fmt.Println(constant.Version)
+}
+func handleRun(s service.Service) {
+	err := s.Run()
+	if err != nil {
+		fmt.Println(err)
+	}
+}

+ 230 - 0
server/server.go

@@ -0,0 +1,230 @@
+package server
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	"github.com/alroyso/clash-for-flutter-service/constant"
+	"github.com/gorilla/websocket"
+)
+
+var (
+	StatusRunning string = "running"
+	StatusStopped string = "stopped"
+)
+
+var (
+	Server      *http.Server
+	MaxLog      int                        = 1000
+	Status      string                     = StatusStopped
+	Logs        []string                   = make([]string, 0)
+	logsClients map[string]*websocket.Conn = make(map[string]*websocket.Conn)
+	Cmd         *exec.Cmd
+	upgrader    websocket.Upgrader = websocket.Upgrader{
+		ReadBufferSize:  1024,
+		WriteBufferSize: 1024,
+		CheckOrigin: func(r *http.Request) bool {
+			return true
+		},
+	}
+)
+
+type startArgs struct {
+	Args []string `json:"args"`
+}
+
+func StartServer(port int64) error {
+	addr := fmt.Sprintf("127.0.0.1:%d", port)
+	Server = &http.Server{Addr: addr}
+	http.HandleFunc("/start", handleReqStart)
+	http.HandleFunc("/stop", handleReqStop)
+	http.HandleFunc("/info", hanldeReqInfo)
+	http.HandleFunc("/logs", hanldeReqLogs)
+	err := Server.ListenAndServe()
+	return err
+}
+
+func checkRequest(w http.ResponseWriter, r *http.Request) (reject bool) {
+	reject = !strings.Contains((r.Header.Get("User-Agent")), "naiyou-for-flutter/")
+	if reject {
+		w.WriteHeader(403)
+	}
+	return reject
+}
+
+func FileIsExist(path string) bool {
+	_, err := os.Lstat(path)
+	return !os.IsNotExist(err)
+}
+
+func hanldeReqInfo(w http.ResponseWriter, r *http.Request) {
+	defer r.Body.Close()
+	reject := checkRequest(w, r)
+
+	if reject {
+		return
+	}
+
+	res := make(map[string]interface{})
+	res["code"] = 0
+	res["status"] = Status
+	res["mode"] = os.Args[1]
+	res["version"] = constant.Version
+	json, _ := json.Marshal(res)
+	w.Header().Set("Content-Type", "application/json")
+	w.Write([]byte(json))
+}
+
+func hanldeReqLogs(w http.ResponseWriter, r *http.Request) {
+	reject := checkRequest(w, r)
+	if reject {
+		return
+	}
+
+	c, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		fmt.Print("upgrade:", err)
+		return
+	}
+
+	logsClients[r.RemoteAddr] = c
+	fmt.Println("Connect Success:", r.RemoteAddr, "Connect Count:", len(logsClients))
+
+	c.WriteMessage(websocket.TextMessage, []byte("Connect Success"))
+	if len(Logs) > 0 {
+		c.WriteMessage(websocket.TextMessage, []byte(strings.Join(Logs, "\n")))
+	}
+
+	for {
+		_, msg, err := c.ReadMessage()
+
+		if string(msg) == "PING" {
+			c.WriteMessage(websocket.TextMessage, []byte("PONG"))
+		}
+
+		if err != nil {
+			break
+		}
+	}
+
+	c.Close()
+	delete(logsClients, r.RemoteAddr)
+	fmt.Println("UnConnect:", r.RemoteAddr, "Connect Count:", len(logsClients))
+}
+
+func handleReqStop(w http.ResponseWriter, r *http.Request) {
+	defer r.Body.Close()
+	reject := checkRequest(w, r)
+	if reject {
+		return
+	}
+	res := make(map[string]interface{})
+	if Status == StatusRunning && Cmd != nil {
+		Cmd.Process.Kill()
+		Cmd.Process.Wait()
+		Status = StatusStopped
+		res["code"] = 0
+	} else {
+		res["code"] = 1
+		res["msg"] = "core is not running"
+	}
+	json, _ := json.Marshal(res)
+	w.Header().Set("Content-Type", "application/json")
+	w.Write(json)
+}
+
+func handleReqStart(w http.ResponseWriter, r *http.Request) {
+	defer r.Body.Close()
+	reject := checkRequest(w, r)
+	if reject {
+		return
+	}
+	res := make(map[string]interface{})
+
+	if Status == StatusRunning {
+		res["code"] = 1
+		res["msg"] = "core is running"
+	} else {
+		body, _ := ioutil.ReadAll(r.Body)
+		parmas := &startArgs{}
+		json.Unmarshal(body, parmas)
+		startClashCore(parmas.Args)
+		res["code"] = 0
+	}
+	json, _ := json.Marshal(res)
+	w.Header().Set("Content-Type", "application/json")
+	w.Write(json)
+}
+
+func startClashCore(args []string) {
+	file, _ := os.Executable()
+	path := filepath.Dir(file)
+
+	ext := ""
+	if runtime.GOOS == "windows" {
+		ext = ".exe"
+	}
+	name := fmt.Sprintf("clash-%s-%s%s", runtime.GOOS, runtime.GOARCH, ext)
+	clashCorePath := filepath.Join(path, name)
+	EchoLog(" Core Path Is: ", clashCorePath)
+	if !FileIsExist(clashCorePath) {
+		// for dev
+		d, _ := os.Getwd()
+		EchoLog("pwd is: ", d)
+		clashCorePath = filepath.Join(d, name)
+		if !FileIsExist(clashCorePath) {
+			EchoLog(" Core Is Not Exist")
+			return
+		}
+		EchoLog(" Core Path Is: ", clashCorePath)
+	}
+
+	EchoLog("Args Is: ", args)
+
+	Cmd = exec.Command(clashCorePath, args...)
+	stdout, _ := Cmd.StdoutPipe()
+	stderr, _ := Cmd.StderrPipe()
+	go listenLog(&stderr)
+	go listenLog(&stdout)
+	Cmd.Start()
+	Status = StatusRunning
+	go func() {
+		s, _ := Cmd.Process.Wait()
+		Status = StatusStopped
+		Cmd = nil
+		EchoLog(" Core exit with code: ", s.ExitCode())
+	}()
+}
+
+func listenLog(i *io.ReadCloser) {
+	reader := bufio.NewReader(*i)
+	for {
+		line, err := reader.ReadString('\n')
+		if err != nil {
+			break
+		}
+		EchoLog(strings.TrimSpace(line))
+		// fmt.Printf("len=%d cap=%d slice=%v\n", len(Logs), cap(Logs), Logs)
+	}
+}
+
+func EchoLog(logs ...interface{}) {
+	log := fmt.Sprint(logs...)
+	Logs = append(Logs, log)
+	if len(Logs) > MaxLog {
+		Logs = Logs[1:]
+	}
+	for _, c := range logsClients {
+		c.WriteMessage(websocket.TextMessage, []byte(log))
+	}
+	fmt.Println(log)
+}