mirror of
https://github.com/VaalaCat/frp-panel.git
synced 2025-09-26 19:31:18 +08:00
feat: mutate proxy form default value
feat: support select log pkg for stream
This commit is contained in:
@@ -39,6 +39,11 @@ func PullWorkers(appInstance app.Application, clientID, clientSecret string) err
|
||||
|
||||
ctrl := ctx.GetApp().GetWorkersManager()
|
||||
for _, worker := range resp.GetWorkers() {
|
||||
_, err := ctrl.GetWorkerStatus(ctx, worker.GetWorkerId())
|
||||
if err == nil {
|
||||
logger.Logger(ctx).Infof("worker [%s] already running", worker.GetWorkerId())
|
||||
continue
|
||||
}
|
||||
ctrl.RunWorker(ctx, worker.GetWorkerId(), workerd.NewWorkerdController(worker, ctx.GetApp().GetConfig().Client.Worker.WorkerdWorkDir))
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/VaalaCat/frp-panel/utils/logger"
|
||||
)
|
||||
|
||||
func StartSteamLogHandler(ctx *app.Context, req *pb.CommonRequest) (*pb.CommonResponse, error) {
|
||||
func StartSteamLogHandler(ctx *app.Context, req *pb.StartSteamLogRequest) (*pb.CommonResponse, error) {
|
||||
return common.StartSteamLogHandler(ctx, req, initStreamLog)
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,7 @@ import (
|
||||
type HookMgr struct {
|
||||
*sync.Mutex
|
||||
hook *logger.StreamLogHook
|
||||
pkgs []string
|
||||
}
|
||||
|
||||
func (h *HookMgr) Close() {
|
||||
@@ -33,23 +34,37 @@ func (h *HookMgr) AddStream(send func(msg string), closeSend func()) {
|
||||
}
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
h.hook = logger.NewStreamLogHook(send, closeSend)
|
||||
if h.pkgs == nil {
|
||||
h.pkgs = make([]string, 0)
|
||||
}
|
||||
h.hook = logger.NewStreamLogHook(send, closeSend, h.pkgs...)
|
||||
logger.Instance().AddHook(h.hook)
|
||||
go h.hook.Send()
|
||||
}
|
||||
|
||||
func StartSteamLogHandler(ctx *app.Context, req *pb.CommonRequest, initStreamLogFunc func(*app.Context, app.StreamLogHookMgr)) (*pb.CommonResponse, error) {
|
||||
logger.Logger(ctx).Infof("get a start stream log request, origin is: [%+v]", req)
|
||||
func (h *HookMgr) SetPkgs(pkgs []string) {
|
||||
if h.Mutex == nil {
|
||||
h.Mutex = &sync.Mutex{}
|
||||
}
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
h.pkgs = pkgs
|
||||
}
|
||||
|
||||
StopSteamLogHandler(ctx, req)
|
||||
func StartSteamLogHandler(ctx *app.Context, req *pb.StartSteamLogRequest, initStreamLogFunc func(*app.Context, app.StreamLogHookMgr)) (*pb.CommonResponse, error) {
|
||||
logger.Logger(ctx).Infof("get a start stream log request, origin is: [%s]", req.String())
|
||||
|
||||
StopSteamLogHandler(ctx, &pb.CommonRequest{})
|
||||
hookMgr := ctx.GetApp().GetStreamLogHookMgr()
|
||||
hookMgr.SetPkgs(req.GetPkgs())
|
||||
|
||||
initStreamLogFunc(ctx, hookMgr)
|
||||
|
||||
return &pb.CommonResponse{}, nil
|
||||
}
|
||||
|
||||
func StopSteamLogHandler(ctx *app.Context, req *pb.CommonRequest) (*pb.CommonResponse, error) {
|
||||
logger.Logger(ctx).Infof("get a stop stream log request, origin is: [%+v]", req)
|
||||
logger.Logger(ctx).Infof("get a stop stream log request, origin is: [%s]", req.String())
|
||||
h := ctx.GetApp().GetStreamLogHookMgr()
|
||||
h.Close()
|
||||
return &pb.CommonResponse{}, nil
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/VaalaCat/frp-panel/common"
|
||||
"github.com/VaalaCat/frp-panel/pb"
|
||||
@@ -22,13 +23,21 @@ func GetLogHandler(appInstance app.Application) func(*gin.Context) {
|
||||
|
||||
func getLogHander(c *gin.Context, appInstance app.Application) {
|
||||
id := c.Query("id")
|
||||
logger.Logger(c).Infof("user try to get stream log, id: [%s]", id)
|
||||
pkgsQuery := c.Query("pkgs")
|
||||
pkgs := strings.Split(pkgsQuery, ",")
|
||||
logger.Logger(c).Infof("user try to get stream log, id: [%s], pkgs: [%s]", id, pkgsQuery)
|
||||
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, common.Err("id is empty"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(pkgs) != 0 {
|
||||
if pkgs[0] == "all" {
|
||||
pkgs = make([]string, 0)
|
||||
}
|
||||
}
|
||||
|
||||
appInstance.GetClientLogManager().GetClientLock(id).Lock()
|
||||
defer appInstance.GetClientLogManager().GetClientLock(id).Unlock()
|
||||
|
||||
@@ -38,7 +47,7 @@ func getLogHander(c *gin.Context, appInstance app.Application) {
|
||||
}
|
||||
appInstance.GetClientLogManager().Store(id, ch)
|
||||
|
||||
_, err := rpc.CallClient(app.NewContext(c, appInstance), id, pb.Event_EVENT_START_STREAM_LOG, &pb.CommonRequest{})
|
||||
_, err := rpc.CallClient(app.NewContext(c, appInstance), id, pb.Event_EVENT_START_STREAM_LOG, &pb.StartSteamLogRequest{Pkgs: pkgs})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, common.Err(err.Error()))
|
||||
return
|
||||
|
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/VaalaCat/frp-panel/utils/logger"
|
||||
)
|
||||
|
||||
func StartSteamLogHandler(ctx *app.Context, req *pb.CommonRequest) (*pb.CommonResponse, error) {
|
||||
func StartSteamLogHandler(ctx *app.Context, req *pb.StartSteamLogRequest) (*pb.CommonResponse, error) {
|
||||
return common.StartSteamLogHandler(ctx, req, initStreamLog)
|
||||
}
|
||||
|
||||
|
@@ -28,7 +28,8 @@ type ReqType interface {
|
||||
pb.StartProxyRequest | pb.StopProxyRequest |
|
||||
pb.CreateWorkerRequest | pb.RemoveWorkerRequest | pb.RunWorkerRequest | pb.StopWorkerRequest | pb.UpdateWorkerRequest | pb.GetWorkerRequest |
|
||||
pb.ListWorkersRequest | pb.CreateWorkerIngressRequest | pb.GetWorkerIngressRequest |
|
||||
pb.GetWorkerStatusRequest | pb.InstallWorkerdRequest
|
||||
pb.GetWorkerStatusRequest | pb.InstallWorkerdRequest |
|
||||
pb.StartSteamLogRequest
|
||||
}
|
||||
|
||||
func GetProtoRequest[T ReqType](c *gin.Context) (r *T, err error) {
|
||||
|
@@ -29,7 +29,8 @@ type RespType interface {
|
||||
pb.StartProxyResponse | pb.StopProxyResponse |
|
||||
pb.CreateWorkerResponse | pb.RemoveWorkerResponse | pb.RunWorkerResponse | pb.StopWorkerResponse | pb.UpdateWorkerResponse | pb.GetWorkerResponse |
|
||||
pb.ListWorkersResponse | pb.CreateWorkerIngressResponse | pb.GetWorkerIngressResponse |
|
||||
pb.GetWorkerStatusResponse | pb.InstallWorkerdResponse
|
||||
pb.GetWorkerStatusResponse | pb.InstallWorkerdResponse |
|
||||
pb.StartSteamLogResponse
|
||||
}
|
||||
|
||||
func OKResp[T RespType](c *gin.Context, origin *T) {
|
||||
|
@@ -49,4 +49,12 @@ message GetClientCertRequest {
|
||||
message GetClientCertResponse {
|
||||
optional common.Status status = 1;
|
||||
bytes cert = 2;
|
||||
}
|
||||
|
||||
message StartSteamLogRequest {
|
||||
repeated string pkgs = 1; // 需要获取哪些包的日志
|
||||
}
|
||||
|
||||
message StartSteamLogResponse {
|
||||
optional common.Status status = 1;
|
||||
}
|
@@ -473,6 +473,94 @@ func (x *GetClientCertResponse) GetCert() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
type StartSteamLogRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Pkgs []string `protobuf:"bytes,1,rep,name=pkgs,proto3" json:"pkgs,omitempty"` // 需要获取哪些包的日志
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StartSteamLogRequest) Reset() {
|
||||
*x = StartSteamLogRequest{}
|
||||
mi := &file_api_master_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *StartSteamLogRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StartSteamLogRequest) ProtoMessage() {}
|
||||
|
||||
func (x *StartSteamLogRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_master_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StartSteamLogRequest.ProtoReflect.Descriptor instead.
|
||||
func (*StartSteamLogRequest) Descriptor() ([]byte, []int) {
|
||||
return file_api_master_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *StartSteamLogRequest) GetPkgs() []string {
|
||||
if x != nil {
|
||||
return x.Pkgs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type StartSteamLogResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Status *Status `protobuf:"bytes,1,opt,name=status,proto3,oneof" json:"status,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StartSteamLogResponse) Reset() {
|
||||
*x = StartSteamLogResponse{}
|
||||
mi := &file_api_master_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *StartSteamLogResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StartSteamLogResponse) ProtoMessage() {}
|
||||
|
||||
func (x *StartSteamLogResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_master_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StartSteamLogResponse.ProtoReflect.Descriptor instead.
|
||||
func (*StartSteamLogResponse) Descriptor() ([]byte, []int) {
|
||||
return file_api_master_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *StartSteamLogResponse) GetStatus() *Status {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_api_master_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_api_master_proto_rawDesc = "" +
|
||||
@@ -527,6 +615,11 @@ const file_api_master_proto_rawDesc = "" +
|
||||
"\x15GetClientCertResponse\x12+\n" +
|
||||
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01\x12\x12\n" +
|
||||
"\x04cert\x18\x02 \x01(\fR\x04certB\t\n" +
|
||||
"\a_status\"*\n" +
|
||||
"\x14StartSteamLogRequest\x12\x12\n" +
|
||||
"\x04pkgs\x18\x01 \x03(\tR\x04pkgs\"O\n" +
|
||||
"\x15StartSteamLogResponse\x12+\n" +
|
||||
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01B\t\n" +
|
||||
"\a_statusB\aZ\x05../pbb\x06proto3"
|
||||
|
||||
var (
|
||||
@@ -542,7 +635,7 @@ func file_api_master_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_api_master_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_api_master_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
|
||||
var file_api_master_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_api_master_proto_goTypes = []any{
|
||||
(ClientStatus_Status)(0), // 0: api_master.ClientStatus.Status
|
||||
(*ClientStatus)(nil), // 1: api_master.ClientStatus
|
||||
@@ -551,25 +644,28 @@ var file_api_master_proto_goTypes = []any{
|
||||
(*GetClientsStatusResponse)(nil), // 4: api_master.GetClientsStatusResponse
|
||||
(*GetClientCertRequest)(nil), // 5: api_master.GetClientCertRequest
|
||||
(*GetClientCertResponse)(nil), // 6: api_master.GetClientCertResponse
|
||||
nil, // 7: api_master.GetClientsStatusResponse.ClientsEntry
|
||||
(ClientType)(0), // 8: common.ClientType
|
||||
(*Status)(nil), // 9: common.Status
|
||||
(*StartSteamLogRequest)(nil), // 7: api_master.StartSteamLogRequest
|
||||
(*StartSteamLogResponse)(nil), // 8: api_master.StartSteamLogResponse
|
||||
nil, // 9: api_master.GetClientsStatusResponse.ClientsEntry
|
||||
(ClientType)(0), // 10: common.ClientType
|
||||
(*Status)(nil), // 11: common.Status
|
||||
}
|
||||
var file_api_master_proto_depIdxs = []int32{
|
||||
8, // 0: api_master.ClientStatus.client_type:type_name -> common.ClientType
|
||||
0, // 1: api_master.ClientStatus.status:type_name -> api_master.ClientStatus.Status
|
||||
2, // 2: api_master.ClientStatus.version:type_name -> api_master.ClientVersion
|
||||
8, // 3: api_master.GetClientsStatusRequest.client_type:type_name -> common.ClientType
|
||||
9, // 4: api_master.GetClientsStatusResponse.status:type_name -> common.Status
|
||||
7, // 5: api_master.GetClientsStatusResponse.clients:type_name -> api_master.GetClientsStatusResponse.ClientsEntry
|
||||
8, // 6: api_master.GetClientCertRequest.client_type:type_name -> common.ClientType
|
||||
9, // 7: api_master.GetClientCertResponse.status:type_name -> common.Status
|
||||
1, // 8: api_master.GetClientsStatusResponse.ClientsEntry.value:type_name -> api_master.ClientStatus
|
||||
9, // [9:9] is the sub-list for method output_type
|
||||
9, // [9:9] is the sub-list for method input_type
|
||||
9, // [9:9] is the sub-list for extension type_name
|
||||
9, // [9:9] is the sub-list for extension extendee
|
||||
0, // [0:9] is the sub-list for field type_name
|
||||
10, // 0: api_master.ClientStatus.client_type:type_name -> common.ClientType
|
||||
0, // 1: api_master.ClientStatus.status:type_name -> api_master.ClientStatus.Status
|
||||
2, // 2: api_master.ClientStatus.version:type_name -> api_master.ClientVersion
|
||||
10, // 3: api_master.GetClientsStatusRequest.client_type:type_name -> common.ClientType
|
||||
11, // 4: api_master.GetClientsStatusResponse.status:type_name -> common.Status
|
||||
9, // 5: api_master.GetClientsStatusResponse.clients:type_name -> api_master.GetClientsStatusResponse.ClientsEntry
|
||||
10, // 6: api_master.GetClientCertRequest.client_type:type_name -> common.ClientType
|
||||
11, // 7: api_master.GetClientCertResponse.status:type_name -> common.Status
|
||||
11, // 8: api_master.StartSteamLogResponse.status:type_name -> common.Status
|
||||
1, // 9: api_master.GetClientsStatusResponse.ClientsEntry.value:type_name -> api_master.ClientStatus
|
||||
10, // [10:10] is the sub-list for method output_type
|
||||
10, // [10:10] is the sub-list for method input_type
|
||||
10, // [10:10] is the sub-list for extension type_name
|
||||
10, // [10:10] is the sub-list for extension extendee
|
||||
0, // [0:10] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_api_master_proto_init() }
|
||||
@@ -581,13 +677,14 @@ func file_api_master_proto_init() {
|
||||
file_api_master_proto_msgTypes[0].OneofWrappers = []any{}
|
||||
file_api_master_proto_msgTypes[3].OneofWrappers = []any{}
|
||||
file_api_master_proto_msgTypes[5].OneofWrappers = []any{}
|
||||
file_api_master_proto_msgTypes[7].OneofWrappers = []any{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_master_proto_rawDesc), len(file_api_master_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 7,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
@@ -18,6 +18,7 @@ import (
|
||||
// biz/common/stream_log.go
|
||||
type StreamLogHookMgr interface {
|
||||
AddStream(send func(msg string), closeSend func())
|
||||
SetPkgs(pkgs []string)
|
||||
Close()
|
||||
Lock()
|
||||
TryLock() bool
|
||||
|
@@ -4,7 +4,6 @@ package workerd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"github.com/VaalaCat/frp-panel/services/app"
|
||||
"github.com/VaalaCat/frp-panel/utils"
|
||||
"github.com/VaalaCat/frp-panel/utils/logger"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type workerExecManager struct {
|
||||
@@ -70,8 +70,8 @@ func (m *workerExecManager) RunCmd(uid string, cwd string, argv []string) {
|
||||
cmd := exec.CommandContext(ctx, m.binaryPath, args...)
|
||||
cmd.Dir = cwd
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: false}
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = logger.LoggerWriter("workerd", logrus.InfoLevel)
|
||||
cmd.Stderr = logger.LoggerWriter("workerd", logrus.ErrorLevel)
|
||||
if err := cmd.Run(); err != nil {
|
||||
logger.Logger(ctx).WithError(err).Errorf("command id: [%s] run failed, binary path: [%s], args: %s", uid, m.binaryPath, utils.MarshalForJson(args))
|
||||
}
|
||||
|
@@ -61,6 +61,8 @@ func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
}
|
||||
|
||||
isFrpPkg := entry.Data["pkg"] == "frp"
|
||||
isWorkerdPkg := entry.Data["pkg"] == "workerd"
|
||||
|
||||
resetCode := f.getReset()
|
||||
|
||||
var levelColorCode string
|
||||
@@ -100,12 +102,15 @@ func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
levelColor := f.getColor(levelColorCode)
|
||||
fmt.Fprintf(b, " [%s%s%s]", levelColor, entry.Level.String(), resetCode)
|
||||
|
||||
if entry.HasCaller() {
|
||||
fileName := filepath.Base(entry.Caller.File)
|
||||
fatherDir := filepath.Base(filepath.Dir(entry.Caller.File))
|
||||
callerColor := f.getColor(colorGray)
|
||||
fmt.Fprintf(b, " [%s%s:%d%s]", callerColor, filepath.Join(fatherDir, fileName), entry.Caller.Line, resetCode)
|
||||
if !isWorkerdPkg {
|
||||
if entry.HasCaller() {
|
||||
fileName := filepath.Base(entry.Caller.File)
|
||||
fatherDir := filepath.Base(filepath.Dir(entry.Caller.File))
|
||||
callerColor := f.getColor(colorGray)
|
||||
fmt.Fprintf(b, " [%s%s:%d%s]", callerColor, filepath.Join(fatherDir, fileName), entry.Caller.Line, resetCode)
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString(" ")
|
||||
}
|
||||
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -18,9 +19,11 @@ type StreamLogHook struct {
|
||||
streamEnabled bool
|
||||
stdio io.Writer
|
||||
lock *sync.Mutex
|
||||
pkgs map[string]bool // 只传输指定包的日志
|
||||
}
|
||||
|
||||
func NewStreamLogHook(handler func(msg string), stopFunc func()) *StreamLogHook {
|
||||
func NewStreamLogHook(handler func(msg string), stopFunc func(), pkgs ...string) *StreamLogHook {
|
||||
pkgs = lo.FilterMap(pkgs, func(v string, _ int) (string, bool) { return v, len(v) > 0 })
|
||||
return &StreamLogHook{
|
||||
ch: make(chan string, 4096),
|
||||
handler: handler,
|
||||
@@ -28,6 +31,7 @@ func NewStreamLogHook(handler func(msg string), stopFunc func()) *StreamLogHook
|
||||
stdio: bufio.NewWriter(os.Stdout),
|
||||
stopFunc: stopFunc,
|
||||
lock: &sync.Mutex{},
|
||||
pkgs: lo.SliceToMap(pkgs, func(v string) (string, bool) { return v, true }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +39,18 @@ func (s *StreamLogHook) Fire(entry *logrus.Entry) error {
|
||||
if !s.streamEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 有过滤时需要过滤
|
||||
if len(s.pkgs) > 0 {
|
||||
pkgName, ok := entry.Data["pkg"].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if _, ok := s.pkgs[pkgName]; !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
str, _ := entry.String()
|
||||
s.ch <- str
|
||||
return nil
|
||||
|
@@ -20,7 +20,7 @@ func initFrpLogger(frpLogLevel log.Level) {
|
||||
log.WithCaller(true),
|
||||
log.AddCallerSkip(1),
|
||||
log.WithLevel(frpLogLevel),
|
||||
log.WithOutput(logger))
|
||||
log.WithOutput(LoggerWriter("frp", logrus.InfoLevel)))
|
||||
}
|
||||
|
||||
func InitLogger() {
|
||||
|
@@ -9,36 +9,38 @@ import (
|
||||
type LogrusWriter struct {
|
||||
Logger *logrus.Logger
|
||||
Level logrus.Level
|
||||
Pkg string
|
||||
}
|
||||
|
||||
func (w *LogrusWriter) Write(p []byte) (n int, err error) {
|
||||
msg := string(p)
|
||||
switch w.Level {
|
||||
case logrus.DebugLevel:
|
||||
w.Logger.WithField("pkg", "frp").Debug(msg)
|
||||
w.Logger.WithField("pkg", w.Pkg).Debug(msg)
|
||||
case logrus.InfoLevel:
|
||||
w.Logger.WithField("pkg", "frp").Info(msg)
|
||||
w.Logger.WithField("pkg", w.Pkg).Info(msg)
|
||||
case logrus.WarnLevel:
|
||||
w.Logger.WithField("pkg", "frp").Warn(msg)
|
||||
w.Logger.WithField("pkg", w.Pkg).Warn(msg)
|
||||
case logrus.ErrorLevel:
|
||||
w.Logger.WithField("pkg", "frp").Error(msg)
|
||||
w.Logger.WithField("pkg", w.Pkg).Error(msg)
|
||||
default:
|
||||
w.Logger.WithField("pkg", "frp").Info(msg)
|
||||
w.Logger.WithField("pkg", w.Pkg).Info(msg)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
var (
|
||||
logger = &LogrusWriter{
|
||||
Logger: logrus.New(),
|
||||
Level: logrus.InfoLevel,
|
||||
}
|
||||
LoggerInstance = logrus.New()
|
||||
)
|
||||
|
||||
func Instance() *logrus.Logger {
|
||||
return logger.Logger
|
||||
return LoggerInstance
|
||||
}
|
||||
|
||||
func LoggerWriter(pkg string, level logrus.Level) *LogrusWriter {
|
||||
return &LogrusWriter{Logger: LoggerInstance, Level: level, Pkg: pkg}
|
||||
}
|
||||
|
||||
func Logger(c context.Context) *logrus.Entry {
|
||||
return logger.Logger.WithContext(c)
|
||||
return LoggerInstance.WithContext(c)
|
||||
}
|
||||
|
@@ -1,74 +1,56 @@
|
||||
import { Server } from "@/lib/pb/common";
|
||||
import { TypedClientPluginOptions } from "@/types/plugin";
|
||||
import { HTTPProxyConfig, TypedProxyConfig } from "@/types/proxy";
|
||||
import { ServerConfig } from "@/types/server";
|
||||
import { ArrowRight, Globe, Monitor } from 'lucide-react';
|
||||
|
||||
export function VisitPreview({ server, typedProxyConfig }: { server: Server; typedProxyConfig: TypedProxyConfig }) {
|
||||
export function VisitPreview({ server, typedProxyConfig, direction, withIcon = true }:
|
||||
{
|
||||
server: Server;
|
||||
typedProxyConfig: TypedProxyConfig;
|
||||
direction?: "row" | "column";
|
||||
withIcon?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className={"flex items-start sm:items-center justify-start p-2 text-xs font-mono text-nowrap " + (
|
||||
!direction ? "flex-wrap" : direction == "row" ? "flex-row" : "flex-col"
|
||||
)}>
|
||||
<ServerSideVisitPreview server={server} typedProxyConfig={typedProxyConfig} withIcon={withIcon} />
|
||||
<ArrowRight className="hidden sm:block w-4 h-4 text-gray-400 mx-2 flex-shrink-0" />
|
||||
<ClientSideVisitPreview typedProxyConfig={typedProxyConfig} withIcon={withIcon} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ServerSideVisitPreview({ server, typedProxyConfig, withIcon = true }: { server: Server; typedProxyConfig: TypedProxyConfig; withIcon?: boolean }) {
|
||||
const serverCfg = JSON.parse(server?.config || '{}') as ServerConfig;
|
||||
const serverAddress = server.ip || serverCfg.bindAddr || 'Unknown';
|
||||
const serverPort = getServerPort(typedProxyConfig, serverCfg);
|
||||
|
||||
return <div className="flex items-center mb-2 sm:mb-0">
|
||||
{withIcon && <Globe className="w-4 h-4 text-blue-500 mr-2 flex-shrink-0" />}
|
||||
<span className="text-nowrap">{typedProxyConfig.type == "http" ? "http://" : ""}{
|
||||
typedProxyConfig.type == "http" ? (
|
||||
getServerAuth(typedProxyConfig as HTTPProxyConfig) + getServerHost(typedProxyConfig as HTTPProxyConfig, serverCfg, serverAddress)
|
||||
) : serverAddress
|
||||
}:{serverPort || "?"}{
|
||||
typedProxyConfig.type == "http" ?
|
||||
getServerPath(typedProxyConfig as HTTPProxyConfig) : ""
|
||||
}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function ClientSideVisitPreview({ typedProxyConfig, withIcon = true }: { typedProxyConfig: TypedProxyConfig, withIcon?: boolean }) {
|
||||
const localAddress = typedProxyConfig.localIP || '127.0.0.1';
|
||||
const localPort = typedProxyConfig.localPort;
|
||||
const clientPlugin = typedProxyConfig.plugin;
|
||||
|
||||
function getServerPath(httpProxyConfig: HTTPProxyConfig) {
|
||||
if (!httpProxyConfig.locations) {
|
||||
return "";
|
||||
}
|
||||
if (httpProxyConfig.locations.length == 0) {
|
||||
return "";
|
||||
}
|
||||
if (httpProxyConfig.locations.length == 1) {
|
||||
return httpProxyConfig.locations[0];
|
||||
}
|
||||
return `[${httpProxyConfig.locations.join(", ")}]`;
|
||||
}
|
||||
|
||||
function getServerHost(httpProxyConfig: HTTPProxyConfig) {
|
||||
let allHosts = []
|
||||
if (httpProxyConfig.subdomain) {
|
||||
allHosts.push(`${httpProxyConfig.subdomain}.${serverCfg.subDomainHost}`);
|
||||
}
|
||||
|
||||
allHosts.push(...(httpProxyConfig.customDomains || []));
|
||||
|
||||
if (allHosts.length == 0) {
|
||||
return serverAddress;
|
||||
}
|
||||
|
||||
if (allHosts.length == 1) {
|
||||
return allHosts[0];
|
||||
}
|
||||
|
||||
return `[${allHosts.join(", ")}]`;
|
||||
}
|
||||
|
||||
function getServerAuth(httpProxyConfig: HTTPProxyConfig) {
|
||||
if (!httpProxyConfig.httpUser || !httpProxyConfig.httpPassword) {
|
||||
return "";
|
||||
}
|
||||
return `${httpProxyConfig.httpUser}:${httpProxyConfig.httpPassword}@`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-start p-2 text-sm font-mono text-nowrap">
|
||||
<div className="flex items-center mb-2 sm:mb-0">
|
||||
<Globe className="w-4 h-4 text-blue-500 mr-2 flex-shrink-0" />
|
||||
<span className="text-nowrap">{typedProxyConfig.type == "http" ? "http://" : ""}{
|
||||
typedProxyConfig.type == "http" ? (
|
||||
getServerAuth(typedProxyConfig as HTTPProxyConfig) + getServerHost(typedProxyConfig as HTTPProxyConfig)
|
||||
) : serverAddress
|
||||
}:{serverPort || "?"}{
|
||||
typedProxyConfig.type == "http" ?
|
||||
getServerPath(typedProxyConfig as HTTPProxyConfig) : ""
|
||||
}</span>
|
||||
</div>
|
||||
<ArrowRight className="hidden sm:block w-4 h-4 text-gray-400 mx-2 flex-shrink-0" />
|
||||
<div className="flex items-center mb-2 sm:mb-0">
|
||||
<Monitor className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
|
||||
<span className="text-nowrap">{localAddress}:{localPort}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <div className="flex items-center mb-2 sm:mb-0">
|
||||
{withIcon && <Monitor className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />}
|
||||
{clientPlugin && clientPlugin.type.length > 0 ?
|
||||
<PluginLocalDist plugins={clientPlugin} /> :
|
||||
<span className="text-nowrap">{localAddress}:{localPort}</span>}
|
||||
</div>
|
||||
}
|
||||
|
||||
function getServerPort(proxyConfig: TypedProxyConfig, serverConfig: ServerConfig): number | undefined {
|
||||
@@ -86,3 +68,56 @@ function getServerPort(proxyConfig: TypedProxyConfig, serverConfig: ServerConfig
|
||||
}
|
||||
}
|
||||
|
||||
function getServerAuth(httpProxyConfig: HTTPProxyConfig) {
|
||||
if (!httpProxyConfig.httpUser || !httpProxyConfig.httpPassword) {
|
||||
return "";
|
||||
}
|
||||
return `${httpProxyConfig.httpUser}:${httpProxyConfig.httpPassword}@`
|
||||
}
|
||||
|
||||
function getServerPath(httpProxyConfig: HTTPProxyConfig) {
|
||||
if (!httpProxyConfig.locations) {
|
||||
return "";
|
||||
}
|
||||
if (httpProxyConfig.locations.length == 0) {
|
||||
return "";
|
||||
}
|
||||
if (httpProxyConfig.locations.length == 1) {
|
||||
return httpProxyConfig.locations[0];
|
||||
}
|
||||
return `[${httpProxyConfig.locations.join(", ")}]`;
|
||||
}
|
||||
|
||||
function getServerHost(httpProxyConfig: HTTPProxyConfig, serverCfg: ServerConfig, serverAddress: string) {
|
||||
let allHosts = []
|
||||
if (httpProxyConfig.subdomain) {
|
||||
allHosts.push(`${httpProxyConfig.subdomain}.${serverCfg.subDomainHost}`);
|
||||
}
|
||||
|
||||
allHosts.push(...(httpProxyConfig.customDomains || []));
|
||||
|
||||
if (allHosts.length == 0) {
|
||||
return serverAddress;
|
||||
}
|
||||
|
||||
if (allHosts.length == 1) {
|
||||
return allHosts[0];
|
||||
}
|
||||
|
||||
return `[${allHosts.join(", ")}]`;
|
||||
}
|
||||
|
||||
function PluginLocalDist({ plugins }: { plugins: TypedClientPluginOptions }) {
|
||||
return (<>
|
||||
{
|
||||
plugins.type === "unix_domain_socket" ? (
|
||||
<span className="text-nowrap">{plugins.unixPath}</span>
|
||||
) : plugins.type === "static_file" ? (
|
||||
<span className="text-nowrap">{plugins.localPath}</span>
|
||||
) : plugins.type === "http2https" || plugins.type === "https2http" || plugins.type === "https2https" ? (
|
||||
<span className="text-nowrap">{plugins.localAddr}</span>
|
||||
) : (
|
||||
<span className="text-nowrap">{JSON.stringify(plugins)}</span>
|
||||
)
|
||||
}</>)
|
||||
}
|
@@ -99,7 +99,7 @@ export const ProxyConfigMutateForm = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (proxyName && proxyType) {
|
||||
setProxyConfigs([{ name: proxyName, type: proxyType }])
|
||||
setProxyConfigs([{...defaultProxyConfig, name: proxyName, type: proxyType }])
|
||||
}
|
||||
}, [proxyName, proxyType])
|
||||
|
||||
|
@@ -28,6 +28,8 @@ import { useStore } from '@nanostores/react'
|
||||
import { $proxyTableRefetchTrigger } from '@/store/refetch-trigger'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ServerSideVisitPreview, VisitPreview } from '../base/visit-preview'
|
||||
import { getServer } from '@/api/server'
|
||||
|
||||
interface WorkerIngressProps {
|
||||
workerId: string
|
||||
@@ -240,14 +242,20 @@ export function WorkerIngress({ workerId, refetchWorker, clients }: WorkerIngres
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs font-medium text-muted-foreground mr-1">Server:</span>
|
||||
<span className="font-mono text-xs truncate max-w-[150px] md:max-w-[200px]">
|
||||
{ingress.serverId}
|
||||
<div className="flex items-center text-center">
|
||||
<span className="text-xs font-medium text-muted-foreground mr-1 font-mono">Server:</span>
|
||||
<span className="font-mono text-xs items-center hide-scroll-bar">
|
||||
<WorkerIngressPreview
|
||||
serverId={ingress.serverId}
|
||||
proxyCfg={JSON.parse(ingress.config || '{}') as TypedProxyConfig} />
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{ingress.serverId}</TooltipContent>
|
||||
<TooltipContent>
|
||||
<div>
|
||||
ServerID: {ingress.serverId}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -255,7 +263,7 @@ export function WorkerIngress({ workerId, refetchWorker, clients }: WorkerIngres
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs font-medium text-muted-foreground mr-1">Client:</span>
|
||||
<span className="text-xs font-medium text-muted-foreground mr-1 font-mono">Client:</span>
|
||||
<span className="font-mono text-xs truncate max-w-[150px] md:max-w-[200px]">
|
||||
{ingress.originClientId}
|
||||
</span>
|
||||
@@ -361,3 +369,16 @@ export const IngressDeleteForm = ({
|
||||
</DialogFooter>
|
||||
)
|
||||
}
|
||||
|
||||
const WorkerIngressPreview = ({ serverId, proxyCfg }: { serverId?: string; proxyCfg: TypedProxyConfig }) => {
|
||||
const { data: getServerResp } = useQuery({
|
||||
queryKey: ['getServer', serverId],
|
||||
queryFn: () => {
|
||||
return getServer({ serverId: serverId })
|
||||
},
|
||||
})
|
||||
|
||||
return (<div className='font-mono text-xs'>
|
||||
<ServerSideVisitPreview server={getServerResp?.server || { frpsUrls: [] }} typedProxyConfig={proxyCfg} withIcon={false} />
|
||||
</div>)
|
||||
}
|
@@ -186,7 +186,8 @@
|
||||
"cancel": "Cancel",
|
||||
"clientType": "Client Type",
|
||||
"disconnect": "Disconnect",
|
||||
"connect": "Connect"
|
||||
"connect": "Connect",
|
||||
"stream_log_pkgs_select": "Stream Log From"
|
||||
},
|
||||
"frpc": {
|
||||
"client_plugins": {
|
||||
|
@@ -186,7 +186,8 @@
|
||||
"cancel": "取消",
|
||||
"clientType": "客户端类型",
|
||||
"disconnect": "断开连接",
|
||||
"connect": "连接"
|
||||
"connect": "连接",
|
||||
"stream_log_pkgs_select": "捕获日志的模块"
|
||||
},
|
||||
"frpc": {
|
||||
"client_plugins": {
|
||||
|
@@ -157,6 +157,24 @@ export interface GetClientCertResponse {
|
||||
*/
|
||||
cert: Uint8Array;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message api_master.StartSteamLogRequest
|
||||
*/
|
||||
export interface StartSteamLogRequest {
|
||||
/**
|
||||
* @generated from protobuf field: repeated string pkgs = 1;
|
||||
*/
|
||||
pkgs: string[]; // 需要获取哪些包的日志
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message api_master.StartSteamLogResponse
|
||||
*/
|
||||
export interface StartSteamLogResponse {
|
||||
/**
|
||||
* @generated from protobuf field: optional common.Status status = 1;
|
||||
*/
|
||||
status?: Status;
|
||||
}
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class ClientStatus$Type extends MessageType<ClientStatus> {
|
||||
constructor() {
|
||||
@@ -590,3 +608,96 @@ class GetClientCertResponse$Type extends MessageType<GetClientCertResponse> {
|
||||
* @generated MessageType for protobuf message api_master.GetClientCertResponse
|
||||
*/
|
||||
export const GetClientCertResponse = new GetClientCertResponse$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class StartSteamLogRequest$Type extends MessageType<StartSteamLogRequest> {
|
||||
constructor() {
|
||||
super("api_master.StartSteamLogRequest", [
|
||||
{ no: 1, name: "pkgs", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<StartSteamLogRequest>): StartSteamLogRequest {
|
||||
const message = globalThis.Object.create((this.messagePrototype!));
|
||||
message.pkgs = [];
|
||||
if (value !== undefined)
|
||||
reflectionMergePartial<StartSteamLogRequest>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: StartSteamLogRequest): StartSteamLogRequest {
|
||||
let message = target ?? this.create(), end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* repeated string pkgs */ 1:
|
||||
message.pkgs.push(reader.string());
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(message: StartSteamLogRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||
/* repeated string pkgs = 1; */
|
||||
for (let i = 0; i < message.pkgs.length; i++)
|
||||
writer.tag(1, WireType.LengthDelimited).string(message.pkgs[i]);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message api_master.StartSteamLogRequest
|
||||
*/
|
||||
export const StartSteamLogRequest = new StartSteamLogRequest$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class StartSteamLogResponse$Type extends MessageType<StartSteamLogResponse> {
|
||||
constructor() {
|
||||
super("api_master.StartSteamLogResponse", [
|
||||
{ no: 1, name: "status", kind: "message", T: () => Status }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<StartSteamLogResponse>): StartSteamLogResponse {
|
||||
const message = globalThis.Object.create((this.messagePrototype!));
|
||||
if (value !== undefined)
|
||||
reflectionMergePartial<StartSteamLogResponse>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: StartSteamLogResponse): StartSteamLogResponse {
|
||||
let message = target ?? this.create(), end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* optional common.Status status */ 1:
|
||||
message.status = Status.internalBinaryRead(reader, reader.uint32(), options, message.status);
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(message: StartSteamLogResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||
/* optional common.Status status = 1; */
|
||||
if (message.status)
|
||||
Status.internalBinaryWrite(message.status, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message api_master.StartSteamLogResponse
|
||||
*/
|
||||
export const StartSteamLogResponse = new StartSteamLogResponse$Type();
|
||||
|
@@ -1,87 +1,96 @@
|
||||
import { API_PATH } from "./consts";
|
||||
import { API_PATH } from './consts'
|
||||
|
||||
export const parseStreaming = async (
|
||||
controller: AbortController,
|
||||
id: string,
|
||||
onLog: (value: string) => void,
|
||||
onError?: (status: number) => void,
|
||||
onDone?: () => void
|
||||
controller: AbortController,
|
||||
id: string,
|
||||
pkgs: string[],
|
||||
onLog: (value: string) => void,
|
||||
onError?: (status: number) => void,
|
||||
onDone?: () => void,
|
||||
) => {
|
||||
const decoder = new TextDecoder();
|
||||
let uint8Array = new Uint8Array();
|
||||
let chunks = "";
|
||||
const response = await fetch(`${API_PATH}/log?id=${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "*./*",
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
onError?.(response.status);
|
||||
return;
|
||||
} else {
|
||||
onError?.(200);
|
||||
}
|
||||
function decodeLog(chunk: string): string {
|
||||
const lines = chunk.split("\n");
|
||||
const newLines = lines.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line)
|
||||
} catch (error) {
|
||||
return line;
|
||||
}
|
||||
})
|
||||
const decodedLines = newLines.map((line) => {
|
||||
return Buffer.from(line, 'base64').toString('utf-8');
|
||||
})
|
||||
const splittedLines = decodedLines.map((line) => {
|
||||
return line.split("\n");
|
||||
}).flat();
|
||||
const trimmedLines = splittedLines.map((line) => {
|
||||
return line.trim().replaceAll("\r", "").replaceAll("\n", "").replaceAll("\t", "");
|
||||
})
|
||||
return trimmedLines.join("\n");
|
||||
const decoder = new TextDecoder()
|
||||
let uint8Array = new Uint8Array()
|
||||
let chunks = ''
|
||||
let param: Record<string, string> = {
|
||||
id: id,
|
||||
}
|
||||
if (pkgs.length > 0) {
|
||||
param['pkgs'] = pkgs.join(',')
|
||||
}
|
||||
|
||||
}
|
||||
fetchStream(
|
||||
response,
|
||||
(chunk) => {
|
||||
//@ts-ignore
|
||||
uint8Array = new Uint8Array([...uint8Array, ...chunk]);
|
||||
chunks = decoder.decode(uint8Array, { stream: true });
|
||||
onLog(decodeLog(chunks));
|
||||
},
|
||||
() => {
|
||||
onDone && onDone();
|
||||
},
|
||||
);
|
||||
};
|
||||
const response = await fetch(`${API_PATH}/log?${new URLSearchParams(param).toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: '*./*',
|
||||
},
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (response.status !== 200) {
|
||||
onError?.(response.status)
|
||||
return
|
||||
} else {
|
||||
onError?.(200)
|
||||
}
|
||||
function decodeLog(chunk: string): string {
|
||||
const lines = chunk.split('\n')
|
||||
const newLines = lines.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line)
|
||||
} catch (error) {
|
||||
return line
|
||||
}
|
||||
})
|
||||
const decodedLines = newLines.map((line) => {
|
||||
return Buffer.from(line, 'base64').toString('utf-8')
|
||||
})
|
||||
const splittedLines = decodedLines
|
||||
.map((line) => {
|
||||
return line.split('\n')
|
||||
})
|
||||
.flat()
|
||||
const trimmedLines = splittedLines.map((line) => {
|
||||
return line.trim().replaceAll('\r', '').replaceAll('\n', '').replaceAll('\t', '')
|
||||
})
|
||||
return trimmedLines.join('\n')
|
||||
}
|
||||
fetchStream(
|
||||
response,
|
||||
(chunk) => {
|
||||
//@ts-ignore
|
||||
uint8Array = new Uint8Array([...uint8Array, ...chunk])
|
||||
chunks = decoder.decode(uint8Array, { stream: true })
|
||||
onLog(decodeLog(chunks))
|
||||
},
|
||||
() => {
|
||||
onDone && onDone()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async function pump(
|
||||
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
controller: ReadableStreamDefaultController,
|
||||
onChunk?: (chunk: Uint8Array) => void,
|
||||
onDone?: () => void,
|
||||
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
controller: ReadableStreamDefaultController,
|
||||
onChunk?: (chunk: Uint8Array) => void,
|
||||
onDone?: () => void,
|
||||
): Promise<ReadableStreamReadResult<Uint8Array> | undefined> {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
onDone && onDone();
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
onChunk && onChunk(value);
|
||||
controller.enqueue(value);
|
||||
return pump(reader, controller, onChunk, onDone);
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
onDone && onDone()
|
||||
controller.close()
|
||||
return
|
||||
}
|
||||
onChunk && onChunk(value)
|
||||
controller.enqueue(value)
|
||||
return pump(reader, controller, onChunk, onDone)
|
||||
}
|
||||
export const fetchStream = (
|
||||
response: Response,
|
||||
onChunk?: (chunk: Uint8Array) => void,
|
||||
onDone?: () => void,
|
||||
response: Response,
|
||||
onChunk?: (chunk: Uint8Array) => void,
|
||||
onDone?: () => void,
|
||||
): ReadableStream<string> => {
|
||||
const reader = response.body!.getReader();
|
||||
return new ReadableStream({
|
||||
start: (controller) => pump(reader, controller, onChunk, onDone),
|
||||
});
|
||||
};
|
||||
const reader = response.body!.getReader()
|
||||
return new ReadableStream({
|
||||
start: (controller) => pump(reader, controller, onChunk, onDone),
|
||||
})
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { Providers } from '@/components/providers'
|
||||
import { RootLayout } from '@/components/layout'
|
||||
@@ -21,23 +21,24 @@ import { PlayCircle, StopCircle, RefreshCcw, Eraser } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const LogTerminalComponent = dynamic(() => import('@/components/base/readonly-xterm'), {
|
||||
ssr: false
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
export default function StreamLogPage() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
const [clientID, setClientID] = useState<string | undefined>(undefined)
|
||||
const [log, setLog] = useState<string | undefined>(undefined)
|
||||
const [clear, setClear] = useState<number>(0)
|
||||
const [enabled, setEnabled] = useState<boolean>(false)
|
||||
const [timeoutID, setTimeoutID] = useState<NodeJS.Timeout | null>(null);
|
||||
const [timeoutID, setTimeoutID] = useState<NodeJS.Timeout | null>(null)
|
||||
const [clientType, setClientType] = useState<ClientType>(ClientType.FRPS)
|
||||
const [status, setStatus] = useState<"loading" | "success" | "error" | undefined>()
|
||||
const [status, setStatus] = useState<'loading' | 'success' | 'error' | undefined>()
|
||||
const [pkgs, setPkgs] = useState<string[]>([])
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const paramClientID = searchParams.get('clientID')
|
||||
const paramClientType = searchParams.get('clientType')
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (paramClientID) {
|
||||
setClientID(paramClientID)
|
||||
@@ -58,47 +59,52 @@ export default function StreamLogPage() {
|
||||
setClear(Math.random())
|
||||
setStatus(undefined)
|
||||
if (!clientID || !enabled) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
setStatus("loading");
|
||||
const abortController = new AbortController()
|
||||
setStatus('loading')
|
||||
|
||||
void parseStreaming(
|
||||
parseStreaming(
|
||||
abortController,
|
||||
clientID,
|
||||
pkgs,
|
||||
setLog,
|
||||
(status: number) => {
|
||||
if (status === 200) {
|
||||
setStatus("success")
|
||||
setStatus('success')
|
||||
} else {
|
||||
setStatus("error")
|
||||
setStatus('error')
|
||||
}
|
||||
},
|
||||
() => {
|
||||
console.log("parseStreaming success")
|
||||
setStatus("success")
|
||||
}
|
||||
);
|
||||
console.log('parseStreaming success')
|
||||
setStatus('success')
|
||||
},
|
||||
)
|
||||
|
||||
return () => {
|
||||
abortController.abort("unmount");
|
||||
setEnabled(false);
|
||||
};
|
||||
}, [clientID, enabled]);
|
||||
abortController.abort('unmount')
|
||||
setEnabled(false)
|
||||
}
|
||||
}, [clientID, enabled, pkgs])
|
||||
|
||||
const handleConnect = () => {
|
||||
if (enabled) {
|
||||
setEnabled(false)
|
||||
if (enabled) {
|
||||
setEnabled(false)
|
||||
}
|
||||
if (timeoutID) {
|
||||
clearTimeout(timeoutID)
|
||||
if (timeoutID) {
|
||||
clearTimeout(timeoutID)
|
||||
}
|
||||
setTimeoutID(setTimeout(() => { setEnabled(true) }, 10))
|
||||
setTimeoutID(
|
||||
setTimeout(() => {
|
||||
setEnabled(true)
|
||||
}, 10),
|
||||
)
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
setClear(Math.random());
|
||||
setClear(Math.random())
|
||||
if (clientID) {
|
||||
getClientsStatus({ clientIds: [clientID], clientType: clientType })
|
||||
}
|
||||
@@ -106,7 +112,7 @@ export default function StreamLogPage() {
|
||||
|
||||
const handleDisconnect = () => {
|
||||
setEnabled(false)
|
||||
setClear(Math.random());
|
||||
setClear(Math.random())
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -114,12 +120,12 @@ export default function StreamLogPage() {
|
||||
<RootLayout mainHeader={<Header />}>
|
||||
<Card className="w-full h-[calc(100dvh_-_80px)] flex flex-col">
|
||||
<CardContent className="p-3 flex-1 flex flex-col gap-2 first-letter:">
|
||||
<div className="flex flex-wrap items-center gap-1.5 shrink-0">
|
||||
<div className="flex flex-wrap items-center gap-1.5 shrink-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<LoadingCircle status={status} />
|
||||
<LoadingCircle status={status} />
|
||||
<Button
|
||||
disabled={!clientID}
|
||||
variant={enabled ? "destructive" : "default"}
|
||||
variant={enabled ? 'destructive' : 'default'}
|
||||
className="h-8 px-2 text-sm gap-1.5"
|
||||
onClick={enabled ? handleDisconnect : handleConnect}
|
||||
>
|
||||
@@ -135,21 +141,12 @@ export default function StreamLogPage() {
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={!clientID}
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
|
||||
<Button disabled={!clientID} variant="outline" className="h-8 w-8 p-0" onClick={handleRefresh}>
|
||||
<RefreshCcw className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => setClear(Math.random())}
|
||||
>
|
||||
<Button variant="outline" className="h-8 w-8 p-0" onClick={() => setClear(Math.random())}>
|
||||
<Eraser className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -157,8 +154,8 @@ export default function StreamLogPage() {
|
||||
<div className="flex items-center gap-1.5">
|
||||
<BaseSelector
|
||||
dataList={[
|
||||
{ value: ClientType.FRPC.toString(), label: "frpc" },
|
||||
{ value: ClientType.FRPS.toString(), label: "frps" }
|
||||
{ value: ClientType.FRPC.toString(), label: 'frpc' },
|
||||
{ value: ClientType.FRPS.toString(), label: 'frps' },
|
||||
]}
|
||||
setValue={(value) => {
|
||||
setClientType(value === ClientType.FRPC.toString() ? ClientType.FRPC : ClientType.FRPS)
|
||||
@@ -168,20 +165,27 @@ export default function StreamLogPage() {
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<BaseSelector
|
||||
dataList={[
|
||||
{ value: 'all', label: 'all' },
|
||||
{ value: 'frp', label: 'frp' },
|
||||
{ value: 'workerd', label: 'workerd' },
|
||||
]}
|
||||
setValue={(value) => {
|
||||
setPkgs([value])
|
||||
}}
|
||||
label={t('common.stream_log_pkgs_select')}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5 min-h-0 flex-1">
|
||||
{clientType === ClientType.FRPC && (
|
||||
<ClientSelector clientID={clientID} setClientID={setClientID} />
|
||||
)}
|
||||
{clientType === ClientType.FRPS && (
|
||||
<ServerSelector serverID={clientID} setServerID={setClientID} />
|
||||
)}
|
||||
|
||||
<div className={cn(
|
||||
'flex-1 min-h-0 overflow-hidden',
|
||||
'border rounded-lg overflow-hidden'
|
||||
)}>
|
||||
<div className="flex flex-col gap-1.5 min-h-0 flex-1">
|
||||
{clientType === ClientType.FRPC && <ClientSelector clientID={clientID} setClientID={setClientID} />}
|
||||
{clientType === ClientType.FRPS && <ServerSelector serverID={clientID} setServerID={setClientID} />}
|
||||
|
||||
<div className={cn('flex-1 min-h-0 overflow-hidden', 'border rounded-lg overflow-hidden')}>
|
||||
<LogTerminalComponent logs={log || ''} reset={clear} />
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -110,4 +110,12 @@
|
||||
width: var(--radix-popover-trigger-width);
|
||||
max-height: var(--radix-popover-content-available-height);
|
||||
}
|
||||
}
|
||||
|
||||
.hide-scroll-bar {
|
||||
overflow: auto;
|
||||
}
|
||||
.hide-scroll-bar::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
@@ -1,7 +1,5 @@
|
||||
import { HeaderOperations } from './common'
|
||||
|
||||
export interface ClientPluginOptions {}
|
||||
|
||||
export type ClientPluginType =
|
||||
| 'http_proxy'
|
||||
| 'http2https'
|
||||
@@ -11,10 +9,14 @@ export type ClientPluginType =
|
||||
| 'static_file'
|
||||
| 'unix_domain_socket'
|
||||
|
||||
export interface TypedClientPluginOptions {
|
||||
type: ClientPluginType
|
||||
clientPluginOptions?: ClientPluginOptions
|
||||
}
|
||||
export type TypedClientPluginOptions =
|
||||
| HTTP2HTTPSPluginOptions
|
||||
| HTTPProxyPluginOptions
|
||||
| HTTPS2HTTPPluginOptions
|
||||
| HTTPS2HTTPSPluginOptions
|
||||
| Socks5PluginOptions
|
||||
| StaticFilePluginOptions
|
||||
| UnixDomainSocketPluginOptions
|
||||
|
||||
export interface HTTP2HTTPSPluginOptions {
|
||||
type: 'http2https'
|
||||
|
Reference in New Issue
Block a user