mirror of
				https://github.com/TeaOSLab/EdgeAdmin.git
				synced 2025-11-04 21:50:28 +08:00 
			
		
		
		
	增加恢复模式
This commit is contained in:
		@@ -8,20 +8,22 @@ import (
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/nodes"
 | 
			
		||||
	_ "github.com/TeaOSLab/EdgeAdmin/internal/web"
 | 
			
		||||
	_ "github.com/iwind/TeaGo/bootstrap"
 | 
			
		||||
	"github.com/iwind/gosock/pkg/gosock"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	app := apps.NewAppCmd().
 | 
			
		||||
		Version(teaconst.Version).
 | 
			
		||||
		Product(teaconst.ProductName).
 | 
			
		||||
		Usage(teaconst.ProcessName+" [-v|start|stop|restart|service|daemon|reset]").
 | 
			
		||||
		Usage(teaconst.ProcessName+" [-v|start|stop|restart|service|daemon|reset|recover]").
 | 
			
		||||
		Option("-h", "show this help").
 | 
			
		||||
		Option("-v", "show version").
 | 
			
		||||
		Option("start", "start the service").
 | 
			
		||||
		Option("stop", "stop the service").
 | 
			
		||||
		Option("service", "register service into systemd").
 | 
			
		||||
		Option("daemon", "start the service with daemon").
 | 
			
		||||
		Option("reset", "reset configs")
 | 
			
		||||
		Option("reset", "reset configs").
 | 
			
		||||
		Option("recover", "enter recovery mode")
 | 
			
		||||
 | 
			
		||||
	app.On("daemon", func() {
 | 
			
		||||
		nodes.NewAdminNode().Daemon()
 | 
			
		||||
@@ -42,6 +44,19 @@ func main() {
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println("done")
 | 
			
		||||
	})
 | 
			
		||||
	app.On("recover", func() {
 | 
			
		||||
		sock := gosock.NewTmpSock(teaconst.ProcessName)
 | 
			
		||||
		if !sock.IsListening() {
 | 
			
		||||
			fmt.Println("[ERROR]the service not started yet, you should start the service first")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		_, err := sock.Send(&gosock.Command{Code: "recover"})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Println("[ERROR]enter recovery mode failed: " + err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println("enter recovery mode successfully")
 | 
			
		||||
	})
 | 
			
		||||
	app.Run(func() {
 | 
			
		||||
		adminNode := nodes.NewAdminNode()
 | 
			
		||||
		adminNode.Run()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								go.mod
									
									
									
									
									
								
							@@ -9,11 +9,11 @@ require (
 | 
			
		||||
	github.com/cespare/xxhash v1.1.0
 | 
			
		||||
	github.com/go-sql-driver/mysql v1.5.0
 | 
			
		||||
	github.com/go-yaml/yaml v2.1.0+incompatible
 | 
			
		||||
	github.com/golang/protobuf v1.5.2 // indirect
 | 
			
		||||
	github.com/golang/protobuf v1.5.2
 | 
			
		||||
	github.com/google/go-cmp v0.5.6 // indirect
 | 
			
		||||
	github.com/iwind/TeaGo v0.0.0-20210720011303-fc255c995afa
 | 
			
		||||
	github.com/iwind/gosock v0.0.0-20210720054405-4030f271aa3f
 | 
			
		||||
	github.com/miekg/dns v1.1.35
 | 
			
		||||
	github.com/shirou/gopsutil v3.21.5+incompatible // indirect
 | 
			
		||||
	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 | 
			
		||||
	github.com/tealeg/xlsx/v3 v3.2.3
 | 
			
		||||
	github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119
 | 
			
		||||
@@ -21,6 +21,5 @@ require (
 | 
			
		||||
	golang.org/x/sys v0.0.0-20210616094352-59db8d763f22
 | 
			
		||||
	google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced // indirect
 | 
			
		||||
	google.golang.org/grpc v1.38.0
 | 
			
		||||
	google.golang.org/protobuf v1.26.0 // indirect
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								go.sum
									
									
									
									
									
								
							@@ -44,7 +44,6 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
 | 
			
		||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 | 
			
		||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
 | 
			
		||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 | 
			
		||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
 | 
			
		||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 | 
			
		||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 | 
			
		||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 | 
			
		||||
@@ -55,7 +54,6 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
 | 
			
		||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
			
		||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
			
		||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
 | 
			
		||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 | 
			
		||||
@@ -63,13 +61,13 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 | 
			
		||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 | 
			
		||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
			
		||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 | 
			
		||||
github.com/iwind/TeaGo v0.0.0-20210411134150-ddf57e240c2f h1:r2O8PONj/KiuZjJHVHn7KlCePUIjNtgAmvLfgRafQ8o=
 | 
			
		||||
github.com/iwind/TeaGo v0.0.0-20210411134150-ddf57e240c2f/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
 | 
			
		||||
github.com/iwind/TeaGo v0.0.0-20210628135026-38575a4ab060 h1:qdLtK4PDXxk2vMKkTWl5Fl9xqYuRCukzWAgJbLHdfOo=
 | 
			
		||||
github.com/iwind/TeaGo v0.0.0-20210628135026-38575a4ab060/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
 | 
			
		||||
github.com/iwind/TeaGo v0.0.0-20210720011303-fc255c995afa h1:woN88uEmRRUNFD7pRZEtX9heDcjFn0ClMxjF5ButKow=
 | 
			
		||||
github.com/iwind/TeaGo v0.0.0-20210720011303-fc255c995afa/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
 | 
			
		||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
 | 
			
		||||
github.com/iwind/gosock v0.0.0-20210706152215-08fe64b7a8a3 h1:BVHIjJU5RUX0grmvDtr3ZZ2z0hX+ideJRpg4NtBXZfs=
 | 
			
		||||
github.com/iwind/gosock v0.0.0-20210706152215-08fe64b7a8a3/go.mod h1:H5Q7SXwbx3a97ecJkaS2sD77gspzE7HFUafBO0peEyA=
 | 
			
		||||
github.com/iwind/gosock v0.0.0-20210720054405-4030f271aa3f h1:+mKLTd5tCCLXK+iY5Xjz58hau6qFv9chGqcZ4FWUiIs=
 | 
			
		||||
github.com/iwind/gosock v0.0.0-20210720054405-4030f271aa3f/go.mod h1:H5Q7SXwbx3a97ecJkaS2sD77gspzE7HFUafBO0peEyA=
 | 
			
		||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 | 
			
		||||
github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
 | 
			
		||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 | 
			
		||||
@@ -80,11 +78,9 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 | 
			
		||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 | 
			
		||||
github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs=
 | 
			
		||||
github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 | 
			
		||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
 | 
			
		||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 | 
			
		||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 | 
			
		||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 | 
			
		||||
@@ -110,8 +106,6 @@ github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi
 | 
			
		||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 | 
			
		||||
github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa h1:2cO3RojjYl3hVTbEvJVqrMaFmORhL6O06qdW42toftk=
 | 
			
		||||
github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa/go.mod h1:Yjr3bdWaVWyME1kha7X0jsz3k2DgXNa1Pj3XGyUAbx8=
 | 
			
		||||
github.com/shirou/gopsutil v3.21.5+incompatible h1:OloQyEerMi7JUrXiNzy8wQ5XN+baemxSl12QgIzt0jc=
 | 
			
		||||
github.com/shirou/gopsutil v3.21.5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
 | 
			
		||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 | 
			
		||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
 | 
			
		||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
 | 
			
		||||
@@ -151,7 +145,6 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
 | 
			
		||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
			
		||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
			
		||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0=
 | 
			
		||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
			
		||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 | 
			
		||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
 | 
			
		||||
@@ -159,8 +152,8 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
			
		||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
 | 
			
		||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 | 
			
		||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
@@ -175,8 +168,6 @@ golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7w
 | 
			
		||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
@@ -185,7 +176,6 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7s
 | 
			
		||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 | 
			
		||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
			
		||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
@@ -203,15 +193,14 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK
 | 
			
		||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 | 
			
		||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced h1:c5geK1iMU3cDKtFrCVQIcjR3W+JOZMuhIyICMCTbtus=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
 | 
			
		||||
@@ -220,7 +209,6 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
 | 
			
		||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 | 
			
		||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 | 
			
		||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 | 
			
		||||
google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0=
 | 
			
		||||
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 | 
			
		||||
google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=
 | 
			
		||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
 | 
			
		||||
@@ -232,7 +220,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
 | 
			
		||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 | 
			
		||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 | 
			
		||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 | 
			
		||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
 | 
			
		||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 | 
			
		||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 | 
			
		||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
 | 
			
		||||
 
 | 
			
		||||
@@ -2,17 +2,19 @@ package apps
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/iwind/TeaGo/Tea"
 | 
			
		||||
	teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
 | 
			
		||||
	"github.com/iwind/TeaGo/logs"
 | 
			
		||||
	"github.com/iwind/TeaGo/maps"
 | 
			
		||||
	"github.com/iwind/TeaGo/types"
 | 
			
		||||
	"github.com/iwind/gosock/pkg/gosock"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// App命令帮助
 | 
			
		||||
// AppCmd App命令帮助
 | 
			
		||||
type AppCmd struct {
 | 
			
		||||
	product       string
 | 
			
		||||
	version       string
 | 
			
		||||
@@ -21,10 +23,14 @@ type AppCmd struct {
 | 
			
		||||
	appendStrings []string
 | 
			
		||||
 | 
			
		||||
	directives []*Directive
 | 
			
		||||
 | 
			
		||||
	sock *gosock.Sock
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAppCmd() *AppCmd {
 | 
			
		||||
	return &AppCmd{}
 | 
			
		||||
	return &AppCmd{
 | 
			
		||||
		sock: gosock.NewTmpSock(teaconst.ProcessName),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CommandHelpOption struct {
 | 
			
		||||
@@ -32,25 +38,25 @@ type CommandHelpOption struct {
 | 
			
		||||
	Description string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 产品
 | 
			
		||||
// Product 产品
 | 
			
		||||
func (this *AppCmd) Product(product string) *AppCmd {
 | 
			
		||||
	this.product = product
 | 
			
		||||
	return this
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 版本
 | 
			
		||||
// Version 版本
 | 
			
		||||
func (this *AppCmd) Version(version string) *AppCmd {
 | 
			
		||||
	this.version = version
 | 
			
		||||
	return this
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 使用方法
 | 
			
		||||
// Usage 使用方法
 | 
			
		||||
func (this *AppCmd) Usage(usage string) *AppCmd {
 | 
			
		||||
	this.usage = usage
 | 
			
		||||
	return this
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 选项
 | 
			
		||||
// Option 选项
 | 
			
		||||
func (this *AppCmd) Option(code string, description string) *AppCmd {
 | 
			
		||||
	this.options = append(this.options, &CommandHelpOption{
 | 
			
		||||
		Code:        code,
 | 
			
		||||
@@ -59,13 +65,13 @@ func (this *AppCmd) Option(code string, description string) *AppCmd {
 | 
			
		||||
	return this
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 附加内容
 | 
			
		||||
// Append 附加内容
 | 
			
		||||
func (this *AppCmd) Append(appendString string) *AppCmd {
 | 
			
		||||
	this.appendStrings = append(this.appendStrings, appendString)
 | 
			
		||||
	return this
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 打印
 | 
			
		||||
// Print 打印
 | 
			
		||||
func (this *AppCmd) Print() {
 | 
			
		||||
	fmt.Println(this.product + " v" + this.version)
 | 
			
		||||
 | 
			
		||||
@@ -104,7 +110,7 @@ func (this *AppCmd) Print() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 添加指令
 | 
			
		||||
// On 添加指令
 | 
			
		||||
func (this *AppCmd) On(arg string, callback func()) {
 | 
			
		||||
	this.directives = append(this.directives, &Directive{
 | 
			
		||||
		Arg:      arg,
 | 
			
		||||
@@ -112,7 +118,7 @@ func (this *AppCmd) On(arg string, callback func()) {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 运行
 | 
			
		||||
// Run 运行
 | 
			
		||||
func (this *AppCmd) Run(main func()) {
 | 
			
		||||
	// 获取参数
 | 
			
		||||
	args := os.Args[1:]
 | 
			
		||||
@@ -151,9 +157,6 @@ func (this *AppCmd) Run(main func()) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 记录PID
 | 
			
		||||
	_ = this.writePid()
 | 
			
		||||
 | 
			
		||||
	// 日志
 | 
			
		||||
	writer := new(LogWriter)
 | 
			
		||||
	writer.Init()
 | 
			
		||||
@@ -175,9 +178,9 @@ func (this *AppCmd) runHelp() {
 | 
			
		||||
 | 
			
		||||
// 启动
 | 
			
		||||
func (this *AppCmd) runStart() {
 | 
			
		||||
	proc := this.checkPid()
 | 
			
		||||
	if proc != nil {
 | 
			
		||||
		fmt.Println(this.product+" already started, pid:", proc.Pid)
 | 
			
		||||
	var pid = this.getPID()
 | 
			
		||||
	if pid > 0 {
 | 
			
		||||
		fmt.Println(this.product+" already started, pid:", pid)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -193,18 +196,15 @@ func (this *AppCmd) runStart() {
 | 
			
		||||
 | 
			
		||||
// 停止
 | 
			
		||||
func (this *AppCmd) runStop() {
 | 
			
		||||
	proc := this.checkPid()
 | 
			
		||||
	if proc == nil {
 | 
			
		||||
	var pid = this.getPID()
 | 
			
		||||
	if pid == 0 {
 | 
			
		||||
		fmt.Println(this.product + " not started yet")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 停止进程
 | 
			
		||||
	_ = proc.Signal(syscall.SIGQUIT)
 | 
			
		||||
	_, _ = this.sock.Send(&gosock.Command{Code: "stop"})
 | 
			
		||||
 | 
			
		||||
	// 在Windows上经常不能及时释放资源
 | 
			
		||||
	_ = DeletePid(Tea.Root + "/bin/pid")
 | 
			
		||||
	fmt.Println(this.product+" stopped ok, pid:", proc.Pid)
 | 
			
		||||
	fmt.Println(this.product+" stopped ok, pid:", types.String(pid))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 重启
 | 
			
		||||
@@ -216,20 +216,24 @@ func (this *AppCmd) runRestart() {
 | 
			
		||||
 | 
			
		||||
// 状态
 | 
			
		||||
func (this *AppCmd) runStatus() {
 | 
			
		||||
	proc := this.checkPid()
 | 
			
		||||
	if proc == nil {
 | 
			
		||||
	var pid = this.getPID()
 | 
			
		||||
	if pid == 0 {
 | 
			
		||||
		fmt.Println(this.product + " not started yet")
 | 
			
		||||
	} else {
 | 
			
		||||
		fmt.Println(this.product + " is running, pid: " + fmt.Sprintf("%d", proc.Pid))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Println(this.product + " is running, pid: " + types.String(pid))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 检查PID
 | 
			
		||||
func (this *AppCmd) checkPid() *os.Process {
 | 
			
		||||
	return CheckPid(Tea.Root + "/bin/pid")
 | 
			
		||||
}
 | 
			
		||||
// 获取当前的PID
 | 
			
		||||
func (this *AppCmd) getPID() int {
 | 
			
		||||
	if !this.sock.IsListening() {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
// 写入PID
 | 
			
		||||
func (this *AppCmd) writePid() error {
 | 
			
		||||
	return WritePid(Tea.Root + "/bin/pid")
 | 
			
		||||
	reply, err := this.sock.Send(&gosock.Command{Code: "pid"})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return maps.NewMap(reply.Params).GetInt("pid")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
// +build !windows
 | 
			
		||||
 | 
			
		||||
package apps
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"syscall"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// lock file
 | 
			
		||||
func LockFile(fp *os.File) error {
 | 
			
		||||
	return syscall.Flock(int(fp.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UnlockFile(fp *os.File) error {
 | 
			
		||||
	return syscall.Flock(int(fp.Fd()), syscall.LOCK_UN)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
// +build windows
 | 
			
		||||
 | 
			
		||||
package apps
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// lock file
 | 
			
		||||
func LockFile(fp *os.File) error {
 | 
			
		||||
	return errors.New("not implemented on windows")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UnlockFile(fp *os.File) error {
 | 
			
		||||
	return errors.New("not implemented on windows")
 | 
			
		||||
}
 | 
			
		||||
@@ -1,113 +0,0 @@
 | 
			
		||||
package apps
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/iwind/TeaGo/types"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"runtime"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var pidFileList = []*os.File{}
 | 
			
		||||
 | 
			
		||||
// 检查Pid
 | 
			
		||||
func CheckPid(path string) *os.Process {
 | 
			
		||||
	// windows上打开的文件是不能删除的
 | 
			
		||||
	if runtime.GOOS == "windows" {
 | 
			
		||||
		if os.Remove(path) == nil {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	file, err := os.Open(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer func() {
 | 
			
		||||
		_ = file.Close()
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// 是否能取得Lock
 | 
			
		||||
	err = LockFile(file)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		_ = UnlockFile(file)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pidBytes, err := ioutil.ReadAll(file)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	pid := types.Int(string(pidBytes))
 | 
			
		||||
 | 
			
		||||
	if pid <= 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	proc, _ := os.FindProcess(pid)
 | 
			
		||||
	return proc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 写入Pid
 | 
			
		||||
func WritePid(path string) error {
 | 
			
		||||
	fp, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY|os.O_RDONLY, 0666)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if runtime.GOOS != "windows" {
 | 
			
		||||
		err = LockFile(fp)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	pidFileList = append(pidFileList, fp) // hold the file pointers
 | 
			
		||||
 | 
			
		||||
	_, err = fp.WriteString(fmt.Sprintf("%d", os.Getpid()))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 写入Ppid
 | 
			
		||||
func WritePpid(path string) error {
 | 
			
		||||
	fp, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY|os.O_RDONLY, 0666)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if runtime.GOOS != "windows" {
 | 
			
		||||
		err = LockFile(fp)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	pidFileList = append(pidFileList, fp) // hold the file pointers
 | 
			
		||||
 | 
			
		||||
	_, err = fp.WriteString(fmt.Sprintf("%d", os.Getppid()))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 删除Pid
 | 
			
		||||
func DeletePid(path string) error {
 | 
			
		||||
	_, err := os.Stat(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if !os.IsNotExist(err) {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, fp := range pidFileList {
 | 
			
		||||
		_ = UnlockFile(fp)
 | 
			
		||||
		_ = fp.Close()
 | 
			
		||||
	}
 | 
			
		||||
	return os.Remove(path)
 | 
			
		||||
}
 | 
			
		||||
@@ -108,7 +108,6 @@ func (this *APIConfig) WriteFile(path string) error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = ioutil.WriteFile(path, data, 0666)
 | 
			
		||||
 | 
			
		||||
	// 写入 ~/ 和 /etc/ 目录,因为是备份需要,所以不需要提示错误信息
 | 
			
		||||
	// 写入 ~/.edge-admin/
 | 
			
		||||
@@ -141,5 +140,10 @@ func (this *APIConfig) WriteFile(path string) error {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
	err = ioutil.WriteFile(path, data, 0666)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								internal/const/vars.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								internal/const/vars.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
 | 
			
		||||
 | 
			
		||||
package teaconst
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	IsRecoverMode = false
 | 
			
		||||
)
 | 
			
		||||
@@ -10,11 +10,12 @@ import (
 | 
			
		||||
	"github.com/iwind/TeaGo/Tea"
 | 
			
		||||
	"github.com/iwind/TeaGo/lists"
 | 
			
		||||
	"github.com/iwind/TeaGo/logs"
 | 
			
		||||
	"github.com/iwind/TeaGo/maps"
 | 
			
		||||
	"github.com/iwind/TeaGo/rands"
 | 
			
		||||
	"github.com/iwind/TeaGo/sessions"
 | 
			
		||||
	"github.com/iwind/gosock/pkg/gosock"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
@@ -25,6 +26,7 @@ import (
 | 
			
		||||
var SharedAdminNode *AdminNode = nil
 | 
			
		||||
 | 
			
		||||
type AdminNode struct {
 | 
			
		||||
	sock    *gosock.Sock
 | 
			
		||||
	subPIDs []int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -42,7 +44,7 @@ func (this *AdminNode) Run() {
 | 
			
		||||
	// 本地Sock
 | 
			
		||||
	err := this.listenSock()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logs.Println("NODE" + err.Error())
 | 
			
		||||
		logs.Println("[NODE]", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -89,11 +91,11 @@ func (this *AdminNode) Run() {
 | 
			
		||||
 | 
			
		||||
// Daemon 实现守护进程
 | 
			
		||||
func (this *AdminNode) Daemon() {
 | 
			
		||||
	path := os.TempDir() + "/edge-admin.sock"
 | 
			
		||||
	var sock = gosock.NewTmpSock(teaconst.ProcessName)
 | 
			
		||||
	isDebug := lists.ContainsString(os.Args, "debug")
 | 
			
		||||
	isDebug = true
 | 
			
		||||
	for {
 | 
			
		||||
		conn, err := net.DialTimeout("unix", path, 1*time.Second)
 | 
			
		||||
		conn, err := sock.Dial()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if isDebug {
 | 
			
		||||
				log.Println("[DAEMON]starting ...")
 | 
			
		||||
@@ -281,37 +283,59 @@ func (this *AdminNode) genSecret() string {
 | 
			
		||||
 | 
			
		||||
// 监听本地sock
 | 
			
		||||
func (this *AdminNode) listenSock() error {
 | 
			
		||||
	path := os.TempDir() + "/edge-admin.sock"
 | 
			
		||||
	this.sock = gosock.NewTmpSock(teaconst.ProcessName)
 | 
			
		||||
 | 
			
		||||
	// 检查是否已经存在
 | 
			
		||||
	_, err := os.Stat(path)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		conn, err := net.Dial("unix", path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			_ = os.Remove(path)
 | 
			
		||||
	// 检查是否在运行
 | 
			
		||||
	if this.sock.IsListening() {
 | 
			
		||||
		reply, err := this.sock.Send(&gosock.Command{Code: "pid"})
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return errors.New("error: the process is already running, pid: " + maps.NewMap(reply.Params).GetString("pid"))
 | 
			
		||||
		} else {
 | 
			
		||||
			_ = conn.Close()
 | 
			
		||||
			return errors.New("error: the process is already running")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 新的监听任务
 | 
			
		||||
	listener, err := net.Listen("unix", path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	events.On(events.EventQuit, func() {
 | 
			
		||||
		logs.Println("NODE", "quit unix sock")
 | 
			
		||||
		_ = listener.Close()
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// 启动监听
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			_, err := listener.Accept()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return
 | 
			
		||||
		this.sock.OnCommand(func(cmd *gosock.Command) {
 | 
			
		||||
			switch cmd.Code {
 | 
			
		||||
			case "pid":
 | 
			
		||||
				_ = cmd.Reply(&gosock.Command{
 | 
			
		||||
					Code: "pid",
 | 
			
		||||
					Params: map[string]interface{}{
 | 
			
		||||
						"pid": os.Getpid(),
 | 
			
		||||
					},
 | 
			
		||||
				})
 | 
			
		||||
			case "stop":
 | 
			
		||||
				_ = cmd.ReplyOk()
 | 
			
		||||
 | 
			
		||||
				// 关闭子进程
 | 
			
		||||
				for _, pid := range this.subPIDs {
 | 
			
		||||
					p, err := os.FindProcess(pid)
 | 
			
		||||
					if err == nil && p != nil {
 | 
			
		||||
						_ = p.Kill()
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 退出主进程
 | 
			
		||||
				events.Notify(events.EventQuit)
 | 
			
		||||
				os.Exit(0)
 | 
			
		||||
			case "recover":
 | 
			
		||||
				teaconst.IsRecoverMode = true
 | 
			
		||||
				_ = cmd.ReplyOk()
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		err := this.sock.Listen()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logs.Println("NODE", err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	events.On(events.EventQuit, func() {
 | 
			
		||||
		logs.Println("NODE", "quit unix sock")
 | 
			
		||||
		_ = this.sock.Close()
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,10 @@ func NewRPCClient(apiConfig *configs.APIConfig) (*RPCClient, error) {
 | 
			
		||||
	return client, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *RPCClient) APITokenRPC() pb.APITokenServiceClient {
 | 
			
		||||
	return pb.NewAPITokenServiceClient(this.pickConn())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *RPCClient) AdminRPC() pb.AdminServiceClient {
 | 
			
		||||
	return pb.NewAdminServiceClient(this.pickConn())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package tasks
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/configs"
 | 
			
		||||
	teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/events"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/setup"
 | 
			
		||||
@@ -44,7 +45,7 @@ func (this *SyncAPINodesTask) Start() {
 | 
			
		||||
 | 
			
		||||
func (this *SyncAPINodesTask) Loop() error {
 | 
			
		||||
	// 如果还没有安装直接返回
 | 
			
		||||
	if !setup.IsConfigured() {
 | 
			
		||||
	if !setup.IsConfigured() || teaconst.IsRecoverMode {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package tasks
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/events"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/setup"
 | 
			
		||||
@@ -40,7 +41,7 @@ func (this *SyncClusterTask) Start() {
 | 
			
		||||
 | 
			
		||||
func (this *SyncClusterTask) loop() error {
 | 
			
		||||
	// 如果还没有安装直接返回
 | 
			
		||||
	if !setup.IsConfigured() {
 | 
			
		||||
	if !setup.IsConfigured() || teaconst.IsRecoverMode {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								internal/web/actions/default/recover/helper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								internal/web/actions/default/recover/helper.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
package recover
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
 | 
			
		||||
	"github.com/iwind/TeaGo/actions"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Helper struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool) {
 | 
			
		||||
	if !teaconst.IsRecoverMode {
 | 
			
		||||
		actionPtr.Object().RedirectURL("/")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								internal/web/actions/default/recover/index.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								internal/web/actions/default/recover/index.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
 | 
			
		||||
 | 
			
		||||
package recover
 | 
			
		||||
 | 
			
		||||
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
 | 
			
		||||
 | 
			
		||||
type IndexAction struct {
 | 
			
		||||
	actionutils.ParentAction
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *IndexAction) Init() {
 | 
			
		||||
	this.Nav("", "", "")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *IndexAction) RunGet(params struct{}) {
 | 
			
		||||
	this.Show()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								internal/web/actions/default/recover/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								internal/web/actions/default/recover/init.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
package recover
 | 
			
		||||
 | 
			
		||||
import "github.com/iwind/TeaGo"
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	TeaGo.BeforeStart(func(server *TeaGo.Server) {
 | 
			
		||||
		server.
 | 
			
		||||
			Helper(new(Helper)).
 | 
			
		||||
			Prefix("/recover").
 | 
			
		||||
			Get("", new(IndexAction)).
 | 
			
		||||
			Post("/validateApi", new(ValidateApiAction)).
 | 
			
		||||
			Post("/updateHosts", new(UpdateHostsAction)).
 | 
			
		||||
			EndAll()
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										192
									
								
								internal/web/actions/default/recover/updateHosts.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								internal/web/actions/default/recover/updateHosts.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,192 @@
 | 
			
		||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
 | 
			
		||||
 | 
			
		||||
package recover
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/configs"
 | 
			
		||||
	teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
 | 
			
		||||
	"github.com/iwind/TeaGo/Tea"
 | 
			
		||||
	"github.com/iwind/TeaGo/lists"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UpdateHostsAction struct {
 | 
			
		||||
	actionutils.ParentAction
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *UpdateHostsAction) RunPost(params struct {
 | 
			
		||||
	Protocol   string
 | 
			
		||||
	Host       string
 | 
			
		||||
	Port       string
 | 
			
		||||
	NodeId     string
 | 
			
		||||
	NodeSecret string
 | 
			
		||||
 | 
			
		||||
	OldHosts []string
 | 
			
		||||
	NewHosts []string
 | 
			
		||||
}) {
 | 
			
		||||
	if len(params.OldHosts) != len(params.NewHosts) {
 | 
			
		||||
		this.Fail("参数配置错误,请刷新页面后重试")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client, err := rpc.NewRPCClient(&configs.APIConfig{
 | 
			
		||||
		RPC: struct {
 | 
			
		||||
			Endpoints []string `yaml:"endpoints"`
 | 
			
		||||
		}{
 | 
			
		||||
			Endpoints: []string{params.Protocol + "://" + configutils.QuoteIP(params.Host) + ":" + params.Port},
 | 
			
		||||
		},
 | 
			
		||||
		NodeId: params.NodeId,
 | 
			
		||||
		Secret: params.NodeSecret,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.FailField("host", "测试API节点时出错,请检查配置,错误信息:"+err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	_, err = client.APINodeRPC().FindCurrentAPINodeVersion(client.APIContext(0), &pb.FindCurrentAPINodeVersionRequest{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.FailField("host", "无法连接此API节点,错误信息:"+err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 获取管理员节点信息
 | 
			
		||||
	apiTokensResp, err := client.APITokenRPC().FindAllEnabledAPITokens(client.APIContext(0), &pb.FindAllEnabledAPITokensRequest{Role: "admin"})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.Fail("读取管理员令牌失败:" + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var apiTokens = apiTokensResp.ApiTokens
 | 
			
		||||
	if len(apiTokens) == 0 {
 | 
			
		||||
		this.Fail("数据库中没有管理员令牌信息,请确认数据是否完整")
 | 
			
		||||
	}
 | 
			
		||||
	var adminAPIToken = apiTokens[0]
 | 
			
		||||
 | 
			
		||||
	// API节点列表
 | 
			
		||||
	nodesResp, err := client.APINodeRPC().FindAllEnabledAPINodes(client.Context(0), &pb.FindAllEnabledAPINodesRequest{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.Fail("获取API节点列表失败,错误信息:" + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	var endpoints = []string{}
 | 
			
		||||
	for _, node := range nodesResp.Nodes {
 | 
			
		||||
		if !node.IsOn {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// http
 | 
			
		||||
		if len(node.HttpJSON) > 0 {
 | 
			
		||||
			for index, oldHost := range params.OldHosts {
 | 
			
		||||
				if len(params.NewHosts[index]) == 0 {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				node.HttpJSON = bytes.ReplaceAll(node.HttpJSON, []byte("\""+oldHost+"\""), []byte("\""+params.NewHosts[index]+"\""))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https
 | 
			
		||||
		if len(node.HttpsJSON) > 0 {
 | 
			
		||||
			for index, oldHost := range params.OldHosts {
 | 
			
		||||
				if len(params.NewHosts[index]) == 0 {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				node.HttpsJSON = bytes.ReplaceAll(node.HttpsJSON, []byte("\""+oldHost+"\""), []byte("\""+params.NewHosts[index]+"\""))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// restHTTP
 | 
			
		||||
		if len(node.RestHTTPJSON) > 0 {
 | 
			
		||||
			for index, oldHost := range params.OldHosts {
 | 
			
		||||
				if len(params.NewHosts[index]) == 0 {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				node.RestHTTPJSON = bytes.ReplaceAll(node.RestHTTPJSON, []byte("\""+oldHost+"\""), []byte("\""+params.NewHosts[index]+"\""))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// restHTTPS
 | 
			
		||||
		if len(node.RestHTTPSJSON) > 0 {
 | 
			
		||||
			for index, oldHost := range params.OldHosts {
 | 
			
		||||
				if len(params.NewHosts[index]) == 0 {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				node.RestHTTPSJSON = bytes.ReplaceAll(node.RestHTTPSJSON, []byte("\""+oldHost+"\""), []byte("\""+params.NewHosts[index]+"\""))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// access addrs
 | 
			
		||||
		if len(node.AccessAddrsJSON) > 0 {
 | 
			
		||||
			for index, oldHost := range params.OldHosts {
 | 
			
		||||
				if len(params.NewHosts[index]) == 0 {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				node.AccessAddrsJSON = bytes.ReplaceAll(node.AccessAddrsJSON, []byte("\""+oldHost+"\""), []byte("\""+params.NewHosts[index]+"\""))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var addrs []*serverconfigs.NetworkAddressConfig
 | 
			
		||||
			err = json.Unmarshal(node.AccessAddrsJSON, &addrs)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				this.Fail("读取节点访问地址失败:" + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			for _, addr := range addrs {
 | 
			
		||||
				err = addr.Init()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					// 暂时不提示错误
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				for _, a := range addr.FullAddresses() {
 | 
			
		||||
					if !lists.ContainsString(endpoints, a) {
 | 
			
		||||
						endpoints = append(endpoints, a)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 保存
 | 
			
		||||
		_, err = client.APINodeRPC().UpdateAPINode(client.Context(0), &pb.UpdateAPINodeRequest{
 | 
			
		||||
			NodeId:          node.Id,
 | 
			
		||||
			Name:            node.Name,
 | 
			
		||||
			Description:     node.Description,
 | 
			
		||||
			HttpJSON:        node.HttpJSON,
 | 
			
		||||
			HttpsJSON:       node.HttpsJSON,
 | 
			
		||||
			AccessAddrsJSON: node.AccessAddrsJSON,
 | 
			
		||||
			IsOn:            node.IsOn,
 | 
			
		||||
			RestIsOn:        node.RestIsOn,
 | 
			
		||||
			RestHTTPJSON:    node.RestHTTPJSON,
 | 
			
		||||
			RestHTTPSJSON:   node.RestHTTPSJSON,
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			this.Fail("保存API节点信息失败:" + err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 修改api.yaml
 | 
			
		||||
	var apiConfig = &configs.APIConfig{
 | 
			
		||||
		RPC: struct {
 | 
			
		||||
			Endpoints []string `yaml:"endpoints"`
 | 
			
		||||
		}{
 | 
			
		||||
			Endpoints: endpoints,
 | 
			
		||||
		},
 | 
			
		||||
		NodeId: adminAPIToken.NodeId,
 | 
			
		||||
		Secret: adminAPIToken.Secret,
 | 
			
		||||
	}
 | 
			
		||||
	err = apiConfig.WriteFile(Tea.Root + "/configs/api.yaml")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.Fail("保存configs/api.yaml失败:" + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 加载api.yaml
 | 
			
		||||
	rpcClient, err := rpc.SharedRPC()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.Fail("初始化RPC失败:" + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	err = rpcClient.UpdateConfig(apiConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.Fail("修改API配置失败:" + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 退出恢复模式
 | 
			
		||||
	teaconst.IsRecoverMode = false
 | 
			
		||||
 | 
			
		||||
	this.Success()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										153
									
								
								internal/web/actions/default/recover/validateApi.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								internal/web/actions/default/recover/validateApi.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,153 @@
 | 
			
		||||
package recover
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/configs"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
 | 
			
		||||
	"github.com/iwind/TeaGo/actions"
 | 
			
		||||
	"github.com/iwind/TeaGo/lists"
 | 
			
		||||
	"github.com/iwind/TeaGo/maps"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ValidateApiAction struct {
 | 
			
		||||
	actionutils.ParentAction
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *ValidateApiAction) RunPost(params struct {
 | 
			
		||||
	Protocol   string
 | 
			
		||||
	Host       string
 | 
			
		||||
	Port       string
 | 
			
		||||
	NodeId     string
 | 
			
		||||
	NodeSecret string
 | 
			
		||||
 | 
			
		||||
	Must *actions.Must
 | 
			
		||||
}) {
 | 
			
		||||
	params.NodeId = strings.Trim(params.NodeId, "\"' ")
 | 
			
		||||
	params.NodeSecret = strings.Trim(params.NodeSecret, "\"' ")
 | 
			
		||||
 | 
			
		||||
	// 使用已有的API节点
 | 
			
		||||
	params.Must.
 | 
			
		||||
		Field("host", params.Host).
 | 
			
		||||
		Require("请输入主机地址").
 | 
			
		||||
		Field("port", params.Port).
 | 
			
		||||
		Require("请输入服务端口").
 | 
			
		||||
		Match(`^\d+$`, "服务端口只能是数字").
 | 
			
		||||
		Field("nodeId", params.NodeId).
 | 
			
		||||
		Require("请输入节点nodeId").
 | 
			
		||||
		Field("nodeSecret", params.NodeSecret).
 | 
			
		||||
		Require("请输入节点secret")
 | 
			
		||||
	client, err := rpc.NewRPCClient(&configs.APIConfig{
 | 
			
		||||
		RPC: struct {
 | 
			
		||||
			Endpoints []string `yaml:"endpoints"`
 | 
			
		||||
		}{
 | 
			
		||||
			Endpoints: []string{params.Protocol + "://" + configutils.QuoteIP(params.Host) + ":" + params.Port},
 | 
			
		||||
		},
 | 
			
		||||
		NodeId: params.NodeId,
 | 
			
		||||
		Secret: params.NodeSecret,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.FailField("host", "测试API节点时出错,请检查配置,错误信息:"+err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	_, err = client.APINodeRPC().FindCurrentAPINodeVersion(client.APIContext(0), &pb.FindCurrentAPINodeVersionRequest{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.FailField("host", "无法连接此API节点,错误信息:"+err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// API节点列表
 | 
			
		||||
	nodesResp, err := client.APINodeRPC().FindAllEnabledAPINodes(client.Context(0), &pb.FindAllEnabledAPINodesRequest{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.Fail("获取API节点列表失败,错误信息:" + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	var hosts = []string{}
 | 
			
		||||
	for _, node := range nodesResp.Nodes {
 | 
			
		||||
		if !node.IsOn {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// http
 | 
			
		||||
		if len(node.HttpJSON) > 0 {
 | 
			
		||||
			var config = &serverconfigs.HTTPProtocolConfig{}
 | 
			
		||||
			err = json.Unmarshal(node.HttpJSON, config)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				this.Fail("读取节点HTTP信息失败:" + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			for _, listen := range config.Listen {
 | 
			
		||||
				if len(listen.Host) > 0 && !lists.ContainsString(hosts, listen.Host) {
 | 
			
		||||
					hosts = append(hosts, listen.Host)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https
 | 
			
		||||
		if len(node.HttpsJSON) > 0 {
 | 
			
		||||
			var config = &serverconfigs.HTTPSProtocolConfig{}
 | 
			
		||||
			err = json.Unmarshal(node.HttpsJSON, config)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				this.Fail("读取节点HTTPS信息失败:" + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			for _, listen := range config.Listen {
 | 
			
		||||
				if len(listen.Host) > 0 && !lists.ContainsString(hosts, listen.Host) {
 | 
			
		||||
					hosts = append(hosts, listen.Host)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// restHTTP
 | 
			
		||||
		if len(node.RestHTTPJSON) > 0 {
 | 
			
		||||
			var config = &serverconfigs.HTTPProtocolConfig{}
 | 
			
		||||
			err = json.Unmarshal(node.RestHTTPJSON, config)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				this.Fail("读取节点REST HTTP信息失败:" + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			for _, listen := range config.Listen {
 | 
			
		||||
				if len(listen.Host) > 0 && !lists.ContainsString(hosts, listen.Host) {
 | 
			
		||||
					hosts = append(hosts, listen.Host)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// restHTTPS
 | 
			
		||||
		if len(node.RestHTTPSJSON) > 0 {
 | 
			
		||||
			var config = &serverconfigs.HTTPSProtocolConfig{}
 | 
			
		||||
			err = json.Unmarshal(node.RestHTTPSJSON, config)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				this.Fail("读取节点REST HTTPS信息失败:" + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			for _, listen := range config.Listen {
 | 
			
		||||
				if len(listen.Host) > 0 && !lists.ContainsString(hosts, listen.Host) {
 | 
			
		||||
					hosts = append(hosts, listen.Host)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// access addrs
 | 
			
		||||
		if len(node.AccessAddrsJSON) > 0 {
 | 
			
		||||
			var addrs []*serverconfigs.NetworkAddressConfig
 | 
			
		||||
			err = json.Unmarshal(node.AccessAddrsJSON, &addrs)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				this.Fail("读取节点访问地址失败:" + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			for _, addr := range addrs {
 | 
			
		||||
				if len(addr.Host) > 0 && !lists.ContainsString(hosts, addr.Host) {
 | 
			
		||||
					hosts = append(hosts, addr.Host)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.Data["apiNode"] = maps.Map{
 | 
			
		||||
		"protocol":   params.Protocol,
 | 
			
		||||
		"host":       params.Host,
 | 
			
		||||
		"port":       params.Port,
 | 
			
		||||
		"nodeId":     params.NodeId,
 | 
			
		||||
		"nodeSecret": params.NodeSecret,
 | 
			
		||||
		"hosts":      hosts,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.Success()
 | 
			
		||||
}
 | 
			
		||||
@@ -3,8 +3,7 @@ package setup
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
 | 
			
		||||
	"net"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type IndexAction struct {
 | 
			
		||||
@@ -16,31 +15,18 @@ func (this *IndexAction) Init() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *IndexAction) RunGet(params struct{}) {
 | 
			
		||||
	// 当前服务器的IP
 | 
			
		||||
	serverIPs := []string{}
 | 
			
		||||
	addrs, _ := net.InterfaceAddrs()
 | 
			
		||||
	for _, addr := range addrs {
 | 
			
		||||
		netAddr, ok := addr.(*net.IPNet)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			continue
 | 
			
		||||
	var currentHost = this.Request.Host
 | 
			
		||||
	if strings.Contains(this.Request.Host, ":") {
 | 
			
		||||
		host, _, err := net.SplitHostPort(this.Request.Host)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			currentHost = host
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		serverIPs = append(serverIPs, netAddr.IP.String())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 对IP进行排序,我们希望IPv4排在前面,而且希望127.0.0.1排在IPv4里的最后
 | 
			
		||||
	sort.Slice(serverIPs, func(i, j int) bool {
 | 
			
		||||
		ip1 := serverIPs[i]
 | 
			
		||||
 | 
			
		||||
		if ip1 == "127.0.0.1" {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
		if regexp.MustCompile(`^\d+\.\d+\.\d+.\d+$`).MatchString(ip1) {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	})
 | 
			
		||||
	this.Data["serverIPs"] = serverIPs
 | 
			
		||||
	if net.ParseIP(currentHost) != nil {
 | 
			
		||||
		this.Data["currentHost"] = currentHost
 | 
			
		||||
	} else {
 | 
			
		||||
		this.Data["currentHost"] = ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.Show()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import (
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/nodes"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
 | 
			
		||||
	"github.com/go-yaml/yaml"
 | 
			
		||||
@@ -68,14 +69,14 @@ func (this *InstallAction) RunPost(params struct {
 | 
			
		||||
			_, err = os.Stat(apiNodeDir)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				if os.IsNotExist(err) {
 | 
			
		||||
					this.Fail("在当前目录下找不到" + dir + "目录,请将" + dir + "目录上传或者重新下载解压")
 | 
			
		||||
					this.Fail("在当前目录(" + Tea.Root + ")下找不到" + dir + "目录,请将" + dir + "目录上传或者重新下载解压")
 | 
			
		||||
				}
 | 
			
		||||
				this.Fail("无法检查" + dir + "目录,发生错误:" + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 保存数据库配置
 | 
			
		||||
		dsn := dbMap.GetString("username") + ":" + dbMap.GetString("password") + "@tcp(" + dbMap.GetString("host") + ":" + dbMap.GetString("port") + ")/" + dbMap.GetString("database") + "?charset=utf8mb4&timeout=30s"
 | 
			
		||||
		dsn := dbMap.GetString("username") + ":" + dbMap.GetString("password") + "@tcp(" + configutils.QuoteIP(dbMap.GetString("host")) + ":" + dbMap.GetString("port") + ")/" + dbMap.GetString("database") + "?charset=utf8mb4&timeout=30s"
 | 
			
		||||
		dbConfig := &dbs.Config{
 | 
			
		||||
			DBs: map[string]*dbs.DBConfig{
 | 
			
		||||
				"prod": {
 | 
			
		||||
@@ -175,7 +176,7 @@ func (this *InstallAction) RunPost(params struct {
 | 
			
		||||
			RPC: struct {
 | 
			
		||||
				Endpoints []string `yaml:"endpoints"`
 | 
			
		||||
			}{
 | 
			
		||||
				Endpoints: []string{"http://" + apiNodeMap.GetString("newHost") + ":" + apiNodeMap.GetString("newPort")},
 | 
			
		||||
				Endpoints: []string{"http://" + configutils.QuoteIP(apiNodeMap.GetString("newHost")) + ":" + apiNodeMap.GetString("newPort")},
 | 
			
		||||
			},
 | 
			
		||||
			NodeId: resultMap.GetString("adminNodeId"),
 | 
			
		||||
			Secret: resultMap.GetString("adminNodeSecret"),
 | 
			
		||||
@@ -233,7 +234,7 @@ func (this *InstallAction) RunPost(params struct {
 | 
			
		||||
			RPC: struct {
 | 
			
		||||
				Endpoints []string `yaml:"endpoints"`
 | 
			
		||||
			}{
 | 
			
		||||
				Endpoints: []string{apiNodeMap.GetString("oldProtocol") + "://" + apiNodeMap.GetString("oldHost") + ":" + apiNodeMap.GetString("oldPort")},
 | 
			
		||||
				Endpoints: []string{apiNodeMap.GetString("oldProtocol") + "://" + configutils.QuoteIP(apiNodeMap.GetString("oldHost")) + ":" + apiNodeMap.GetString("oldPort")},
 | 
			
		||||
			},
 | 
			
		||||
			NodeId: apiNodeMap.GetString("oldNodeId"),
 | 
			
		||||
			Secret: apiNodeMap.GetString("oldNodeSecret"),
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import (
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/configs"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
 | 
			
		||||
	"github.com/iwind/TeaGo/actions"
 | 
			
		||||
	"github.com/iwind/TeaGo/maps"
 | 
			
		||||
@@ -62,7 +63,7 @@ func (this *ValidateApiAction) RunPost(params struct {
 | 
			
		||||
		if net.ParseIP(params.NewHost) == nil {
 | 
			
		||||
			this.FailField("newHost", "请输入正确的节点主机地址")
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
 | 
			
		||||
		params.Must.
 | 
			
		||||
			Field("newHost", params.NewHost).
 | 
			
		||||
			Require("请输入节点主机地址")
 | 
			
		||||
@@ -86,7 +87,7 @@ func (this *ValidateApiAction) RunPost(params struct {
 | 
			
		||||
		RPC: struct {
 | 
			
		||||
			Endpoints []string `yaml:"endpoints"`
 | 
			
		||||
		}{
 | 
			
		||||
			Endpoints: []string{params.OldProtocol + "://" + params.OldHost + ":" + params.OldPort},
 | 
			
		||||
			Endpoints: []string{params.OldProtocol + "://" + configutils.QuoteIP(params.OldHost) + ":" + params.OldPort},
 | 
			
		||||
		},
 | 
			
		||||
		NodeId: params.OldNodeId,
 | 
			
		||||
		Secret: params.OldNodeSecret,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,14 @@ package setup
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
 | 
			
		||||
	"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
 | 
			
		||||
	_ "github.com/go-sql-driver/mysql"
 | 
			
		||||
	"github.com/iwind/TeaGo/actions"
 | 
			
		||||
	"github.com/iwind/TeaGo/dbs"
 | 
			
		||||
	"github.com/iwind/TeaGo/maps"
 | 
			
		||||
	stringutil "github.com/iwind/TeaGo/utils/string"
 | 
			
		||||
	"net"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -27,7 +30,20 @@ func (this *ValidateDbAction) RunPost(params struct {
 | 
			
		||||
	params.Must.
 | 
			
		||||
		Field("host", params.Host).
 | 
			
		||||
		Require("请输入主机地址").
 | 
			
		||||
		Match(`^[\w\.-]+$`, "主机地址中不能包含特殊字符").
 | 
			
		||||
		Expect(func() (message string, success bool) {
 | 
			
		||||
			// 是否为IP
 | 
			
		||||
			if net.ParseIP(params.Host) != nil {
 | 
			
		||||
				success = true
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if !regexp.MustCompile(`^[\w.-]+$`).MatchString(params.Host) {
 | 
			
		||||
				message = "主机地址中不能包含特殊字符"
 | 
			
		||||
				success = false
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			success = true
 | 
			
		||||
			return
 | 
			
		||||
		}).
 | 
			
		||||
		Field("port", params.Port).
 | 
			
		||||
		Require("请输入端口").
 | 
			
		||||
		Match(`^\d+$`, "端口中只能包含数字").
 | 
			
		||||
@@ -41,7 +57,7 @@ func (this *ValidateDbAction) RunPost(params struct {
 | 
			
		||||
	// 测试连接
 | 
			
		||||
	db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
 | 
			
		||||
		Driver: "mysql",
 | 
			
		||||
		Dsn:    params.Username + ":" + params.Password + "@tcp(" + params.Host + ":" + params.Port + ")/" + params.Database,
 | 
			
		||||
		Dsn:    params.Username + ":" + params.Password + "@tcp(" + configutils.QuoteIP(params.Host) + ":" + params.Port + ")/" + params.Database,
 | 
			
		||||
		Prefix: "",
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -58,7 +74,7 @@ func (this *ValidateDbAction) RunPost(params struct {
 | 
			
		||||
		if strings.Contains(err.Error(), "Error 1049") {
 | 
			
		||||
			db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
 | 
			
		||||
				Driver: "mysql",
 | 
			
		||||
				Dsn:    params.Username + ":" + params.Password + "@tcp(" + params.Host + ":" + params.Port + ")/",
 | 
			
		||||
				Dsn:    params.Username + ":" + params.Password + "@tcp(" + configutils.QuoteIP(params.Host) + ":" + params.Port + ")/",
 | 
			
		||||
				Prefix: "",
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,11 @@ func NewUserMustAuth(module string) *userMustAuth {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *userMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramName string) (goNext bool) {
 | 
			
		||||
	if teaconst.IsRecoverMode {
 | 
			
		||||
		actionPtr.Object().RedirectURL("/recover")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	var action = actionPtr.Object()
 | 
			
		||||
 | 
			
		||||
	// 安全相关
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,11 @@ type UserShouldAuth struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *UserShouldAuth) BeforeAction(actionPtr actions.ActionWrapper, paramName string) (goNext bool) {
 | 
			
		||||
	if teaconst.IsRecoverMode {
 | 
			
		||||
		actionPtr.Object().RedirectURL("/recover")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.action = actionPtr.Object()
 | 
			
		||||
 | 
			
		||||
	// 安全相关
 | 
			
		||||
@@ -34,7 +39,7 @@ func (this *UserShouldAuth) BeforeAction(actionPtr actions.ActionWrapper, paramN
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 存储用户名到SESSION
 | 
			
		||||
// StoreAdmin 存储用户名到SESSION
 | 
			
		||||
func (this *UserShouldAuth) StoreAdmin(adminId int64, remember bool) {
 | 
			
		||||
	// 修改sid的时间
 | 
			
		||||
	if remember {
 | 
			
		||||
 
 | 
			
		||||
@@ -131,6 +131,9 @@ import (
 | 
			
		||||
	_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/user-nodes"
 | 
			
		||||
	_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/user-ui"
 | 
			
		||||
 | 
			
		||||
	// 恢复
 | 
			
		||||
	_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/recover"
 | 
			
		||||
 | 
			
		||||
	// 安装
 | 
			
		||||
	_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										54
									
								
								web/views/@default/recover/@install.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								web/views/@default/recover/@install.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
.install-box {
 | 
			
		||||
  width: 50em;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  margin-left: -25em;
 | 
			
		||||
  top: 1em;
 | 
			
		||||
  bottom: 1em;
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
}
 | 
			
		||||
.install-box .button.margin {
 | 
			
		||||
  margin-top: 1em;
 | 
			
		||||
}
 | 
			
		||||
.install-box .button.primary {
 | 
			
		||||
  float: right;
 | 
			
		||||
}
 | 
			
		||||
.install-box table td.title {
 | 
			
		||||
  width: 10em;
 | 
			
		||||
}
 | 
			
		||||
.install-box .radio {
 | 
			
		||||
  margin-right: 1em;
 | 
			
		||||
}
 | 
			
		||||
.install-box .radio label {
 | 
			
		||||
  cursor: pointer !important;
 | 
			
		||||
  font-size: 0.9em !important;
 | 
			
		||||
}
 | 
			
		||||
.install-box h3 {
 | 
			
		||||
  font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
.install-box .content-box {
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 5em;
 | 
			
		||||
  bottom: 5em;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  width: 50em;
 | 
			
		||||
  padding-right: 1em;
 | 
			
		||||
  margin-left: -25em;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
}
 | 
			
		||||
.install-box .content-box::-webkit-scrollbar {
 | 
			
		||||
  width: 4px;
 | 
			
		||||
}
 | 
			
		||||
.install-box .button-group {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  margin-left: -25em;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  width: 50em;
 | 
			
		||||
  bottom: 1em;
 | 
			
		||||
}
 | 
			
		||||
.install-box::-webkit-scrollbar {
 | 
			
		||||
  width: 4px;
 | 
			
		||||
}
 | 
			
		||||
/*# sourceMappingURL=@install.css.map */
 | 
			
		||||
							
								
								
									
										1
									
								
								web/views/@default/recover/@install.css.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/views/@default/recover/@install.css.map
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
{"version":3,"sources":["@install.less"],"names":[],"mappings":"AAAA;EAGC,WAAA;EACA,eAAA;EACA,SAAA;EACA,kBAAA;EACA,QAAA;EACA,WAAA;EACA,gBAAA;;AATD,YAWC,QAAO;EACN,eAAA;;AAZF,YAeC,QAAO;EACN,YAAA;;AAhBF,YAmBC,MACC,GAAE;EACD,WAAA;;AArBH,YAyBC;EACC,iBAAA;;AA1BF,YAyBC,OAGC;EACC,0BAAA;EACA,2BAAA;;AA9BH,YAkCC;EACC,mBAAA;;AAnCF,YAsCC;EACC,gBAAA;EACA,eAAA;EACA,QAAA;EACA,WAAA;EACA,SAAA;EACA,WAAA;EACA,kBAAA;EACA,kBAAA;EACA,UAAA;;AA/CF,YAkDC,aAAY;EACX,UAAA;;AAnDF,YAsDC;EACC,eAAA;EACA,SAAA;EACA,kBAAA;EACA,UAAA;EACA,WAAA;EACA,WAAA;;AAIF,YAAY;EACX,UAAA","file":"@install.css"}
 | 
			
		||||
							
								
								
									
										67
									
								
								web/views/@default/recover/@install.less
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								web/views/@default/recover/@install.less
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
.install-box {
 | 
			
		||||
	@width: 50em;
 | 
			
		||||
 | 
			
		||||
	width: @width;
 | 
			
		||||
	position: fixed;
 | 
			
		||||
	left: 50%;
 | 
			
		||||
	margin-left: -@width/2;
 | 
			
		||||
	top: 1em;
 | 
			
		||||
	bottom: 1em;
 | 
			
		||||
	overflow-y: auto;
 | 
			
		||||
 | 
			
		||||
	.button.margin {
 | 
			
		||||
		margin-top: 1em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.button.primary {
 | 
			
		||||
		float: right;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	table {
 | 
			
		||||
		td.title {
 | 
			
		||||
			width: 10em;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.radio {
 | 
			
		||||
		margin-right: 1em;
 | 
			
		||||
 | 
			
		||||
		label {
 | 
			
		||||
			cursor: pointer !important;
 | 
			
		||||
			font-size: 0.9em !important;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h3 {
 | 
			
		||||
		font-weight: normal;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.content-box {
 | 
			
		||||
		overflow-y: auto;
 | 
			
		||||
		position: fixed;
 | 
			
		||||
		top: 5em;
 | 
			
		||||
		bottom: 5em;
 | 
			
		||||
		left: 50%;
 | 
			
		||||
		width: @width;
 | 
			
		||||
		padding-right: 1em;
 | 
			
		||||
		margin-left: -@width/2;
 | 
			
		||||
		z-index: 1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.content-box::-webkit-scrollbar {
 | 
			
		||||
		width: 4px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.button-group {
 | 
			
		||||
		position: fixed;
 | 
			
		||||
		left: 50%;
 | 
			
		||||
		margin-left: -@width/2;
 | 
			
		||||
		z-index: 1;
 | 
			
		||||
		width: @width;
 | 
			
		||||
		bottom: 1em;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.install-box::-webkit-scrollbar {
 | 
			
		||||
	width: 4px;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										54
									
								
								web/views/@default/recover/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								web/views/@default/recover/index.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
.install-box {
 | 
			
		||||
  width: 50em;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  margin-left: -25em;
 | 
			
		||||
  top: 1em;
 | 
			
		||||
  bottom: 1em;
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
}
 | 
			
		||||
.install-box .button.margin {
 | 
			
		||||
  margin-top: 1em;
 | 
			
		||||
}
 | 
			
		||||
.install-box .button.primary {
 | 
			
		||||
  float: right;
 | 
			
		||||
}
 | 
			
		||||
.install-box table td.title {
 | 
			
		||||
  width: 10em;
 | 
			
		||||
}
 | 
			
		||||
.install-box .radio {
 | 
			
		||||
  margin-right: 1em;
 | 
			
		||||
}
 | 
			
		||||
.install-box .radio label {
 | 
			
		||||
  cursor: pointer !important;
 | 
			
		||||
  font-size: 0.9em !important;
 | 
			
		||||
}
 | 
			
		||||
.install-box h3 {
 | 
			
		||||
  font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
.install-box .content-box {
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 5em;
 | 
			
		||||
  bottom: 5em;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  width: 50em;
 | 
			
		||||
  padding-right: 1em;
 | 
			
		||||
  margin-left: -25em;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
}
 | 
			
		||||
.install-box .content-box::-webkit-scrollbar {
 | 
			
		||||
  width: 4px;
 | 
			
		||||
}
 | 
			
		||||
.install-box .button-group {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  margin-left: -25em;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  width: 50em;
 | 
			
		||||
  bottom: 1em;
 | 
			
		||||
}
 | 
			
		||||
.install-box::-webkit-scrollbar {
 | 
			
		||||
  width: 4px;
 | 
			
		||||
}
 | 
			
		||||
/*# sourceMappingURL=index.css.map */
 | 
			
		||||
							
								
								
									
										1
									
								
								web/views/@default/recover/index.css.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/views/@default/recover/index.css.map
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
{"version":3,"sources":["@install.less"],"names":[],"mappings":"AAAA;EAGC,WAAA;EACA,eAAA;EACA,SAAA;EACA,kBAAA;EACA,QAAA;EACA,WAAA;EACA,gBAAA;;AATD,YAWC,QAAO;EACN,eAAA;;AAZF,YAeC,QAAO;EACN,YAAA;;AAhBF,YAmBC,MACC,GAAE;EACD,WAAA;;AArBH,YAyBC;EACC,iBAAA;;AA1BF,YAyBC,OAGC;EACC,0BAAA;EACA,2BAAA;;AA9BH,YAkCC;EACC,mBAAA;;AAnCF,YAsCC;EACC,gBAAA;EACA,eAAA;EACA,QAAA;EACA,WAAA;EACA,SAAA;EACA,WAAA;EACA,kBAAA;EACA,kBAAA;EACA,UAAA;;AA/CF,YAkDC,aAAY;EACX,UAAA;;AAnDF,YAsDC;EACC,eAAA;EACA,SAAA;EACA,kBAAA;EACA,UAAA;EACA,WAAA;EACA,WAAA;;AAIF,YAAY;EACX,UAAA","file":"index.css"}
 | 
			
		||||
							
								
								
									
										138
									
								
								web/views/@default/recover/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								web/views/@default/recover/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="zh">
 | 
			
		||||
<head>
 | 
			
		||||
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
 | 
			
		||||
	<link rel="shortcut icon" href="/images/favicon.png"/>
 | 
			
		||||
	<title>GoEdge管理系统恢复模式</title>
 | 
			
		||||
	<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
 | 
			
		||||
	{$TEA.VUE}
 | 
			
		||||
	{$TEA.SEMANTIC}
 | 
			
		||||
	<script type="text/javascript" src="/js/md5.min.js"></script>
 | 
			
		||||
	<script type="text/javascript" src="/js/utils.js"></script>
 | 
			
		||||
	<script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js"></script>
 | 
			
		||||
	<script type="text/javascript" src="/ui/components.js"></script>
 | 
			
		||||
	<link rel="stylesheet" href="/_/@default/@layout.css"/>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
<div class="install-box">
 | 
			
		||||
	<!-- 步骤列表 -->
 | 
			
		||||
	<div class="ui steps fluid small">
 | 
			
		||||
        <div class="ui step" :class="{active: step == STEP_INTRO}">
 | 
			
		||||
            <div class="content">
 | 
			
		||||
                <div :class="{title: step == STEP_INTRO}">1. 介绍</div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
		<div class="ui step" :class="{active: step == STEP_NEW_API}">
 | 
			
		||||
			<div class="content">
 | 
			
		||||
				<div :class="{title: step == STEP_NEW_API}">2. 新API地址</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="ui step" :class="{active: step == STEP_API_LIST}">
 | 
			
		||||
			<div class="content">
 | 
			
		||||
				<div :class="{title: step == STEP_API_LIST}">3. 修改API节点地址</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
        <div class="ui step" :class="{active: step == STEP_FINISH}">
 | 
			
		||||
            <div class="content">
 | 
			
		||||
                <div :class="{title: step == STEP_FINISH}">4. 完成</div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
    <!-- 介绍 -->
 | 
			
		||||
    <div v-if="step == STEP_INTRO">
 | 
			
		||||
        可以通过恢复模式重新设置系统的API节点。
 | 
			
		||||
 | 
			
		||||
        <button class="ui button primary" style="margin-top: 10em" type="button" @click.prevent="goIntroNext()">开始<i class="icon long arrow right"></i></button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 新API地址 -->
 | 
			
		||||
    <div v-show="step == STEP_NEW_API">
 | 
			
		||||
        <p class="comment">输入一个可用的API节点信息:</p>
 | 
			
		||||
        <form method="post" class="ui form" data-tea-action=".validateApi" data-tea-success="apiSuccess" data-tea-before="apiSubmit" data-tea-done="apiDone">
 | 
			
		||||
            <table class="ui table definition selectable">
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td class="title">节点协议 *</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <select class="ui dropdown auto-width" name="protocol">
 | 
			
		||||
                            <option value="http">HTTP</option>
 | 
			
		||||
                            <option value="https">HTTPS</option>
 | 
			
		||||
                        </select>
 | 
			
		||||
                        <p class="comment">API节点使用的协议。</p>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>主机地址 *</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <input type="text" name="host" maxlength="100" style="width:20em"/>
 | 
			
		||||
                        <p class="comment">API节点所在主机地址。</p>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>服务端口 *</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <input type="text" name="port" style="width:6em" maxlength="5"/>
 | 
			
		||||
                        <p class="comment">API节点启动的端口。</p>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>节点nodeId *</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <input type="text" name="nodeId" maxlength="100"/>
 | 
			
		||||
                        <p class="comment">在节点的配置文件中<code-label>configs/api.yaml</code-label>中获取,不需要带双引号。</p>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>节点secret *</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <input type="password" name="nodeSecret" maxlength="100"/>
 | 
			
		||||
                        <p class="comment">在节点的配置文件中<code-label>configs/api.yaml</code-label>中获取,不需要带双引号。</p>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </table>
 | 
			
		||||
 | 
			
		||||
            <button class="ui button" type="button" @click.prevent="goBackIntro"><i class="icon long arrow left"></i>上一步</button>  
 | 
			
		||||
            <button class="ui button primary" type="submit" v-if="!apiRequesting">下一步<i class="icon long arrow right"></i></button>
 | 
			
		||||
            <button class="ui button primary" type="button" v-if="apiRequesting">提交中</button>
 | 
			
		||||
        </form>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- API节点列表 -->
 | 
			
		||||
    <div v-show="step == STEP_API_LIST">
 | 
			
		||||
        <form method="post" class="ui form" data-tea-action=".updateHosts" data-tea-success="updateHostsSuccess">
 | 
			
		||||
            <input type="hidden" name="protocol" :value="apiNodeInfo.protocol"/>
 | 
			
		||||
            <input type="hidden" name="host" :value="apiNodeInfo.host"/>
 | 
			
		||||
            <input type="hidden" name="port" :value="apiNodeInfo.port"/>
 | 
			
		||||
            <input type="hidden" name="nodeId" :value="apiNodeInfo.nodeId"/>
 | 
			
		||||
            <input type="hidden" name="nodeSecret" :value="apiNodeInfo.nodeSecret"/>
 | 
			
		||||
 | 
			
		||||
            <table class="ui table selectable celled">
 | 
			
		||||
                <thead>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th style="width: 50%">原地址</th>
 | 
			
		||||
                        <th>新地址(留空表示不修改)</th>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tr v-for="host in apiNodeInfo.hosts">
 | 
			
		||||
                    <td>{{host}}</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <input type="text" maxlength="100" name="newHosts"/>
 | 
			
		||||
                        <input type="hidden" name="oldHosts" :value="host"/>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </table>
 | 
			
		||||
 | 
			
		||||
            <button class="ui button" type="button" @click.prevent="goBackAPI"><i class="icon long arrow left"></i>上一步</button>  
 | 
			
		||||
            <button class="ui button primary" type="submit">保存<i class="icon long arrow right"></i></button>
 | 
			
		||||
        </form>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 修改成功 -->
 | 
			
		||||
    <div v-show="step == STEP_FINISH">
 | 
			
		||||
        修改成功,请<a href="/">刷新</a>此页面后,进入系统。
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										42
									
								
								web/views/@default/recover/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								web/views/@default/recover/index.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
Tea.context(function () {
 | 
			
		||||
	this.STEP_INTRO = "intro"
 | 
			
		||||
	this.STEP_NEW_API = "newAPI"
 | 
			
		||||
	this.STEP_API_LIST = "apiList"
 | 
			
		||||
	this.STEP_FINISH = "finish"
 | 
			
		||||
 | 
			
		||||
	this.step = this.STEP_INTRO
 | 
			
		||||
 | 
			
		||||
	// 介绍
 | 
			
		||||
	this.goIntroNext = function () {
 | 
			
		||||
		this.step = this.STEP_NEW_API
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 新API地址
 | 
			
		||||
	this.apiNodeInfo = {}
 | 
			
		||||
	this.apiRequesting = false
 | 
			
		||||
	this.goBackIntro = function () {
 | 
			
		||||
		this.step = this.STEP_INTRO
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.apiSubmit = function () {
 | 
			
		||||
		this.apiRequesting = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.apiDone = function () {
 | 
			
		||||
		this.apiRequesting = false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.apiSuccess = function (resp) {
 | 
			
		||||
		this.step = this.STEP_API_LIST
 | 
			
		||||
		this.apiNodeInfo = resp.data.apiNode
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 修改API主机地址
 | 
			
		||||
	this.goBackAPI = function () {
 | 
			
		||||
		this.step = this.STEP_NEW_API
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.updateHostsSuccess = function () {
 | 
			
		||||
		this.step = this.STEP_FINISH
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										2
									
								
								web/views/@default/recover/index.less
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								web/views/@default/recover/index.less
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
@import "@install";
 | 
			
		||||
 | 
			
		||||
@@ -50,6 +50,7 @@
 | 
			
		||||
		<div>感谢你选择使用<strong>GoEdge</strong>集群服务系统,下面让我们一起开始配置系统。</div>
 | 
			
		||||
		<div class="margin">在这之前如果你还没有可用的MySQL数据库,请先安装数据库再进行。</div>
 | 
			
		||||
        <div class="margin" style="color: grey">免责声明:GoEdge软件开发者并不对您的软件使用方法、服务对象、服务内容负道德或法律上的约束责任,在使用本软件时产生的一切法律风险自负。</div>
 | 
			
		||||
        <div class="margin" style="color: grey">用户协议:请在遵守中华人民共和国政策、法律、法规的前提下使用本软件;自愿承担因不当使用本软件产生的一切法律后果;承认GoEdge开发者拥有本软件的著作权;点击"开始"安装表示你同意此用户协议。</div>
 | 
			
		||||
		<button class="ui button primary" style="margin-top: 10em" type="button" @click.prevent="goIntroNext()">开始<i class="icon long arrow right"></i></button>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
@@ -85,20 +86,10 @@
 | 
			
		||||
						<td>
 | 
			
		||||
							<div class="ui fields inline">
 | 
			
		||||
								<div class="ui field">
 | 
			
		||||
									<div v-if="serverIPs.length > 0 && !apiHostInput">
 | 
			
		||||
										<select name="newHost" class="ui dropdown auto-width">
 | 
			
		||||
											<option v-for="ip in serverIPs" :value="ip">{{ip}}</option>
 | 
			
		||||
										</select>
 | 
			
		||||
									</div>
 | 
			
		||||
									<div v-else="">
 | 
			
		||||
										<input type="text" name="newHost" value="" ref="newHostRef" placeholder="x.x.x.x"/>
 | 
			
		||||
									</div>
 | 
			
		||||
								</div>
 | 
			
		||||
								<div class="ui field">
 | 
			
		||||
									<a href="" v-if="!apiHostInput" @click.prevent="inputAPIHost()">[手工输入]</a>
 | 
			
		||||
                                    <input type="text" name="newHost" value="" ref="newHostRef" placeholder="x.x.x.x" v-model="currentHost"/>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
							<p class="comment">其他节点访问此API节点的主机地址,可以是IP或者域名,第一次安装时通常是<strong>当前服务器</strong>的IP地址。后期可以修改这个地址。</p>
 | 
			
		||||
							<p class="comment">其他节点访问此API节点的主机地址,可以是IP或者域名,第一次安装时通常是<strong>当前服务器</strong>的公网IP地址。后期可以修改这个地址。</p>
 | 
			
		||||
						</td>
 | 
			
		||||
					</tr>
 | 
			
		||||
				</tbody>
 | 
			
		||||
@@ -191,7 +182,10 @@
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <td>访问日志保留天数</td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            <input type="number" name="accessLogKeepDays" style="width: 5em" maxlength="4" value="30"/>
 | 
			
		||||
                            <div class="ui input right labeled">
 | 
			
		||||
                                <input type="number" name="accessLogKeepDays" style="width: 5em" maxlength="4" value="30"/>
 | 
			
		||||
                                <span class="ui label">天</span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <p class="comment">网站等服务记录的访问日志保留天数,防止无限制地占用数据库空间。</p>
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user