From c7966cef9aafc4c79a19c17b71d80d811e913356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Fri, 16 Aug 2024 09:34:42 +0800 Subject: [PATCH 1/5] feat: bump node from 16.20 to 20.15 --- .github/workflows/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index be8e87b..7a51b0a 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -33,7 +33,7 @@ jobs: tag: ${{ steps.get_version.outputs.TAG }} - uses: actions/setup-node@v2 with: - node-version: '16.20' + node-version: '20.15' - uses: actions/setup-go@v2 with: go-version: '1.22.x' # The Go version to download (if necessary) and use. From d223aa169a5177d920480b55630c6d91e1994ca0 Mon Sep 17 00:00:00 2001 From: fit2bot Date: Wed, 28 Aug 2024 11:32:11 +0800 Subject: [PATCH 2/5] chore: update checkout action --- .github/workflows/build-base-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-base-image.yml b/.github/workflows/build-base-image.yml index 51e7b15..f35a31f 100644 --- a/.github/workflows/build-base-image.yml +++ b/.github/workflows/build-base-image.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 From 9a39b8849b5f552cbc9bc99346bdcd8b3c6da100 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 12 Aug 2024 19:19:19 +0800 Subject: [PATCH 3/5] perf: support to upload and write part replay --- main.go | 27 +++ pkg/config/config.go | 11 +- pkg/guacd/configuration.go | 10 + pkg/guacd/information.go | 11 ++ pkg/guacd/instruction_test.go | 2 - pkg/guacd/parameters.go | 9 + pkg/guacd/tunnel.go | 1 + pkg/jms-sdk-go/model/session.go | 17 +- pkg/session/configuration.go | 44 ++--- pkg/session/server.go | 87 +-------- pkg/session/session.go | 4 +- pkg/tunnel/monitor.go | 2 +- pkg/tunnel/replay_part_upload.go | 305 +++++++++++++++++++++++++++++++ pkg/tunnel/replay_recorder.go | 258 ++++++++++++++++++++++++++ pkg/tunnel/server.go | 23 ++- 15 files changed, 688 insertions(+), 123 deletions(-) create mode 100644 pkg/tunnel/replay_part_upload.go create mode 100644 pkg/tunnel/replay_recorder.go diff --git a/main.go b/main.go index 0acfa1c..7ae07a0 100644 --- a/main.go +++ b/main.go @@ -282,6 +282,7 @@ func registerRouter(jmsService *service.JMService, tunnelService *tunnel.Guacamo wsGroup.Group("/token").Use( middleware.SessionAuth(jmsService)).GET("/", tunnelService.Connect) + } { @@ -316,9 +317,11 @@ func registerRouter(jmsService *service.JMService, tunnelService *tunnel.Guacamo func bootstrap(jmsService *service.JMService) { replayDir := config.GlobalConfig.RecordPath ftpFilePath := config.GlobalConfig.FTPFilePath + sessionDir := config.GlobalConfig.SessionFolderPath allRemainFiles := scanRemainReplay(jmsService, replayDir) go uploadRemainReplay(jmsService, allRemainFiles) go uploadRemainFTPFile(jmsService, ftpFilePath) + go uploadRemainSessionPartReplay(jmsService, sessionDir) } func uploadRemainFTPFile(jmsService *service.JMService, fileStoreDir string) { @@ -434,6 +437,30 @@ func scanRemainReplay(jmsService *service.JMService, replayDir string) map[strin return allRemainFiles } +func uploadRemainSessionPartReplay(jmsService *service.JMService, sessionDir string) { + sessions, err := os.ReadDir(sessionDir) + if err != nil { + logger.Errorf("Read session dir failed: %s", err) + return + } + terminalConf, _ := jmsService.GetTerminalConfig() + for _, sessionEntry := range sessions { + sessionId := sessionEntry.Name() + if !common.ValidUUIDString(sessionId) { + continue + } + sessionRootPath := filepath.Join(sessionDir, sessionId) + uploader := tunnel.PartUploader{ + RootPath: sessionRootPath, + SessionId: sessionId, + ApiClient: jmsService, + TermCfg: &terminalConf, + } + uploader.Start() + logger.Infof("Upload remain session part replay %s finish", sessionId) + } +} + func GetStatusData(tunnelCache *tunnel.GuaTunnelCacheManager) interface{} { sids := tunnelCache.RangeActiveSessionIds() payload := model.HeartbeatData{ diff --git a/pkg/config/config.go b/pkg/config/config.go index af2c484..d8963e9 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -23,6 +23,7 @@ type Config struct { LogDirPath string AccessKeyFilePath string CertsFolderPath string + SessionFolderPath string Name string `mapstructure:"NAME"` CoreHost string `mapstructure:"CORE_HOST"` @@ -56,6 +57,8 @@ type Config struct { IgnoreVerifyCerts bool `mapstructure:"IGNORE_VERIFY_CERTS"` PandaHost string `mapstructure:"PANDA_HOST"` EnablePanda bool `mapstructure:"ENABLE_PANDA"` + + ReplayMaxSize int `mapstructure:"REPLAY_MAX_SIZE"` } func (c *Config) SelectGuacdAddr() string { @@ -81,6 +84,7 @@ func getDefaultConfig() Config { dataFolderPath := filepath.Join(rootPath, "data") driveFolderPath := filepath.Join(dataFolderPath, "drive") recordFolderPath := filepath.Join(dataFolderPath, "replays") + sessionsPath := filepath.Join(dataFolderPath, "sessions") ftpFileFolderPath := filepath.Join(dataFolderPath, "ftp_files") LogDirPath := filepath.Join(dataFolderPath, "logs") keyFolderPath := filepath.Join(dataFolderPath, "keys") @@ -88,7 +92,7 @@ func getDefaultConfig() Config { accessKeyFilePath := filepath.Join(keyFolderPath, ".access_key") folders := []string{dataFolderPath, driveFolderPath, recordFolderPath, - keyFolderPath, LogDirPath, CertsFolderPath} + keyFolderPath, LogDirPath, CertsFolderPath, sessionsPath} for i := range folders { if err := EnsureDirExist(folders[i]); err != nil { log.Fatalf("Create folder failed: %s", err.Error()) @@ -103,6 +107,7 @@ func getDefaultConfig() Config { DrivePath: driveFolderPath, CertsFolderPath: CertsFolderPath, AccessKeyFilePath: accessKeyFilePath, + SessionFolderPath: sessionsPath, CoreHost: "http://localhost:8080", BootstrapToken: "", BindHost: "0.0.0.0", @@ -116,10 +121,14 @@ func getDefaultConfig() Config { EnableRemoteAPPCopyPaste: false, CleanDriveScheduleTime: 1, PandaHost: "http://localhost:9001", + ReplayMaxSize: defaultMaxSize, } } +// 300MB +const defaultMaxSize = 1024 * 1024 * 300 + func EnsureDirExist(path string) error { if !haveDir(path) { if err := os.MkdirAll(path, os.ModePerm); err != nil { diff --git a/pkg/guacd/configuration.go b/pkg/guacd/configuration.go index d56711d..ec3d273 100644 --- a/pkg/guacd/configuration.go +++ b/pkg/guacd/configuration.go @@ -22,3 +22,13 @@ func (conf *Configuration) UnSetParameter(name string) { func (conf *Configuration) GetParameter(name string) string { return conf.Parameters[name] } + +func (conf *Configuration) Clone() Configuration { + newConf := NewConfiguration() + newConf.ConnectionID = conf.ConnectionID + newConf.Protocol = conf.Protocol + for k, v := range conf.Parameters { + newConf.Parameters[k] = v + } + return newConf +} diff --git a/pkg/guacd/information.go b/pkg/guacd/information.go index 69174a4..7789ef2 100644 --- a/pkg/guacd/information.go +++ b/pkg/guacd/information.go @@ -69,3 +69,14 @@ func (info *ClientInformation) ExtraConfig() map[string]string { } return ret } + +func (info *ClientInformation) Clone() ClientInformation { + return ClientInformation{ + OptimalScreenWidth: info.OptimalScreenWidth, + OptimalScreenHeight: info.OptimalScreenHeight, + OptimalResolution: info.OptimalResolution, + ImageMimetypes: []string{"image/jpeg", "image/png", "image/webp"}, + Timezone: info.Timezone, + KeyboardLayout: info.KeyboardLayout, + } +} diff --git a/pkg/guacd/instruction_test.go b/pkg/guacd/instruction_test.go index 904828f..7864d8c 100644 --- a/pkg/guacd/instruction_test.go +++ b/pkg/guacd/instruction_test.go @@ -1,7 +1,6 @@ package guacd import ( - "errors" "testing" ) @@ -28,7 +27,6 @@ func TestValidateInstructionString(t *testing.T) { for i := range tests { ins, err := ParseInstructionString(tests[i]) if err != nil { - t.Log(errors.As(err, &ErrInstructionBadDigit)) t.Log(err) continue } diff --git a/pkg/guacd/parameters.go b/pkg/guacd/parameters.go index 556eb33..d69d853 100644 --- a/pkg/guacd/parameters.go +++ b/pkg/guacd/parameters.go @@ -53,3 +53,12 @@ const ( WolBroadcastAddr = "wol-broadcast-addr" WolWaitTime = "wol-wait-time" ) + +const ( + READONLY = "read-only" +) + +const ( + BoolFalse = "false" + BoolTrue = "true" +) diff --git a/pkg/guacd/tunnel.go b/pkg/guacd/tunnel.go index 14b95fa..ea02c10 100644 --- a/pkg/guacd/tunnel.go +++ b/pkg/guacd/tunnel.go @@ -124,6 +124,7 @@ func NewTunnel(address string, config Configuration, info ClientInformation) (tu tunnel.uuid = ready.Args[0] tunnel.IsOpen = true + tunnel.Config = config return tunnel, nil } diff --git a/pkg/jms-sdk-go/model/session.go b/pkg/jms-sdk-go/model/session.go index 5b72ef8..96fc7af 100644 --- a/pkg/jms-sdk-go/model/session.go +++ b/pkg/jms-sdk-go/model/session.go @@ -29,20 +29,25 @@ const ( UnKnown ReplayVersion = "" Version2 ReplayVersion = "2" Version3 ReplayVersion = "3" + Version5 ReplayVersion = "5" ) const ( - SuffixReplayGz = ".replay.gz" - SuffixCastGz = ".cast.gz" + SuffixReplayGz = ".replay.gz" + SuffixCastGz = ".cast.gz" + SuffixPartReplay = ".part.gz" + SuffixReplayJson = ".replay.json" ) -var SuffixMap = map[ReplayVersion]string{ - Version2: SuffixReplayGz, - Version3: SuffixCastGz, +var SuffixVersionMap = map[string]ReplayVersion{ + SuffixPartReplay: Version5, + SuffixReplayJson: Version5, + SuffixReplayGz: Version2, + SuffixCastGz: Version3, } func ParseReplayVersion(gzFile string, defaultValue ReplayVersion) ReplayVersion { - for version, suffix := range SuffixMap { + for suffix, version := range SuffixVersionMap { if strings.HasSuffix(gzFile, suffix) { return version } diff --git a/pkg/session/configuration.go b/pkg/session/configuration.go index ca74d2f..9de075e 100644 --- a/pkg/session/configuration.go +++ b/pkg/session/configuration.go @@ -77,14 +77,14 @@ func (r RDPConfiguration) GetGuacdConfiguration() guacd.Configuration { conf.SetParameter(guacd.RDPDomain, adDomain) } - // 设置 录像路径 - if r.TerminalConfig.ReplayStorage.TypeName != "null" { - recordDirPath := filepath.Join(config.GlobalConfig.RecordPath, - r.Created.Format(recordDirTimeFormat)) - conf.SetParameter(guacd.RecordingPath, recordDirPath) - conf.SetParameter(guacd.CreateRecordingPath, BoolTrue) - conf.SetParameter(guacd.RecordingName, r.SessionId) - } + //// 设置 录像路径 + //if r.TerminalConfig.ReplayStorage.TypeName != "null" { + // recordDirPath := filepath.Join(config.GlobalConfig.RecordPath, + // r.Created.Format(recordDirTimeFormat)) + // conf.SetParameter(guacd.RecordingPath, recordDirPath) + // conf.SetParameter(guacd.CreateRecordingPath, BoolTrue) + // conf.SetParameter(guacd.RecordingName, r.SessionId) + //} // display 相关 { @@ -181,13 +181,13 @@ func (r VNCConfiguration) GetGuacdConfiguration() guacd.Configuration { conf.SetParameter(guacd.VNCAutoretry, "3") } // 设置存储 - replayCfg := r.TerminalConfig.ReplayStorage - if replayCfg.TypeName != "null" { - recordDirPath := filepath.Join(config.GlobalConfig.RecordPath, r.Created.Format(recordDirTimeFormat)) - conf.SetParameter(guacd.RecordingPath, recordDirPath) - conf.SetParameter(guacd.CreateRecordingPath, BoolTrue) - conf.SetParameter(guacd.RecordingName, r.SessionId) - } + //replayCfg := r.TerminalConfig.ReplayStorage + //if replayCfg.TypeName != "null" { + // recordDirPath := filepath.Join(config.GlobalConfig.RecordPath, r.Created.Format(recordDirTimeFormat)) + // conf.SetParameter(guacd.RecordingPath, recordDirPath) + // conf.SetParameter(guacd.CreateRecordingPath, BoolTrue) + // conf.SetParameter(guacd.RecordingName, r.SessionId) + //} { for key, value := range VNCDisplay.GetDisplayParams() { conf.SetParameter(key, value) @@ -247,13 +247,13 @@ func (r VirtualAppConfiguration) GetGuacdConfiguration() guacd.Configuration { conf.SetParameter(guacd.VNCAutoretry, "10") } // 设置存储 - replayCfg := r.TerminalConfig.ReplayStorage - if replayCfg.TypeName != "null" { - recordDirPath := filepath.Join(config.GlobalConfig.RecordPath, r.Created.Format(recordDirTimeFormat)) - conf.SetParameter(guacd.RecordingPath, recordDirPath) - conf.SetParameter(guacd.CreateRecordingPath, BoolTrue) - conf.SetParameter(guacd.RecordingName, r.SessionId) - } + //replayCfg := r.TerminalConfig.ReplayStorage + //if replayCfg.TypeName != "null" { + // recordDirPath := filepath.Join(config.GlobalConfig.RecordPath, r.Created.Format(recordDirTimeFormat)) + // conf.SetParameter(guacd.RecordingPath, recordDirPath) + // conf.SetParameter(guacd.CreateRecordingPath, BoolTrue) + // conf.SetParameter(guacd.RecordingName, r.SessionId) + //} { for key, value := range VNCDisplay.GetDisplayParams() { conf.SetParameter(key, value) diff --git a/pkg/session/server.go b/pkg/session/server.go index 60b824b..452c4e8 100644 --- a/pkg/session/server.go +++ b/pkg/session/server.go @@ -3,15 +3,11 @@ package session import ( "errors" "fmt" - "os" - "path/filepath" - "strings" "time" "github.com/gin-gonic/gin" "lion/pkg/common" - "lion/pkg/config" "lion/pkg/guacd" "lion/pkg/jms-sdk-go/model" "lion/pkg/jms-sdk-go/service" @@ -281,11 +277,11 @@ func (s *Server) Create(ctx *gin.Context, opts ...TunnelOption) (sess TunnelSess AccountID: opt.Account.ID, Comment: comment, } + sess.ModelSession = &jmsSession sess.ConnectedCallback = s.RegisterConnectedCallback(jmsSession) sess.ConnectedSuccessCallback = s.RegisterConnectedSuccessCallback(jmsSession) sess.ConnectedFailedCallback = s.RegisterConnectedFailedCallback(jmsSession) sess.DisConnectedCallback = s.RegisterDisConnectedCallback(jmsSession) - sess.FinishReplayCallback = s.RegisterFinishReplayCallback(sess) sess.ReleaseAppletAccount = func() error { if opt.appletOpt != nil { return s.JmsService.ReleaseAppletAccount(opt.appletOpt.ID) @@ -367,87 +363,6 @@ func (s *Server) UploadReplayToVideoWorker(tunnel TunnelSession, info guacd.Clie return true } -func (s *Server) RegisterFinishReplayCallback(tunnel TunnelSession) func(guacd.ClientInformation) error { - return func(info guacd.ClientInformation) error { - replayConfig := tunnel.TerminalConfig.ReplayStorage - storageType := replayConfig.TypeName - if storageType == "null" { - logger.Error("录像存储设置为 null,无存储") - reason := model.SessionLifecycleLog{Reason: string(model.ReasonErrNullStorage)} - s.RecordLifecycleLog(tunnel.ID, model.ReplayUploadFailure, reason) - return nil - } - var replayErrReason model.ReplayError - - defer func() { - if replayErrReason != "" { - if err1 := s.JmsService.SessionReplayFailed(tunnel.ID, replayErrReason); err1 != nil { - logger.Errorf("Update %s replay status %s failed err: %s", tunnel.ID, replayErrReason, err1) - } - } - }() - - recordDirPath := filepath.Join(config.GlobalConfig.RecordPath, - tunnel.Created.Format(recordDirTimeFormat)) - originReplayFilePath := filepath.Join(recordDirPath, tunnel.ID) - dstReplayFilePath := originReplayFilePath + ReplayFileNameSuffix - fi, err := os.Stat(originReplayFilePath) - if err != nil { - replayErrReason = model.SessionReplayErrConnectFailed - return err - } - if fi.Size() < 1024 { - logger.Error("录像文件小于1024字节,可判断连接失败,未能产生有效的录像文件") - _ = os.Remove(originReplayFilePath) - replayErrReason = model.SessionReplayErrConnectFailed - return s.JmsService.SessionFailed(tunnel.ID, replayErrReason) - } - // 压缩文件 - err = common.CompressToGzipFile(originReplayFilePath, dstReplayFilePath) - if err != nil { - logger.Error("压缩文件失败: ", err) - replayErrReason = model.SessionReplayErrCreatedFailed - return err - } - // 压缩完成则删除源文件 - defer os.Remove(originReplayFilePath) - - if s.VideoWorkerClient != nil && s.UploadReplayToVideoWorker(tunnel, info, dstReplayFilePath) { - logger.Infof("Upload replay file to video worker: %s", dstReplayFilePath) - _ = os.Remove(dstReplayFilePath) - return nil - } - s.RecordLifecycleLog(tunnel.ID, model.ReplayUploadStart, model.EmptyLifecycleLog) - defaultStorage := storage.ServerStorage{StorageType: "server", JmsService: s.JmsService} - logger.Infof("Upload record file: %s, type: %s", dstReplayFilePath, storageType) - targetName := strings.Join([]string{tunnel.Created.Format(recordDirTimeFormat), - tunnel.ID + ReplayFileNameSuffix}, "/") - if replayStorage := storage.NewReplayStorage(s.JmsService, replayConfig); replayStorage != nil { - if err = replayStorage.Upload(dstReplayFilePath, targetName); err != nil { - logger.Errorf("Upload replay failed: %s", err) - logger.Errorf("Upload replay by type %s failed, try use default", storageType) - err = defaultStorage.Upload(dstReplayFilePath, targetName) - } - } else { - err = defaultStorage.Upload(dstReplayFilePath, targetName) - } - // 上传文件 - if err != nil { - logger.Errorf("Upload replay failed: %s", err.Error()) - replayErrReason = model.SessionReplayErrUploadFailed - reason := model.SessionLifecycleLog{Reason: err.Error()} - s.RecordLifecycleLog(tunnel.ID, model.ReplayUploadFailure, reason) - return err - } - // 上传成功,删除压缩文件 - defer os.Remove(dstReplayFilePath) - // 通知core上传完成 - err = s.JmsService.FinishReply(tunnel.ID) - s.RecordLifecycleLog(tunnel.ID, model.ReplayUploadSuccess, model.EmptyLifecycleLog) - return err - } -} - func (s *Server) RecordLifecycleLog(sid string, event model.LifecycleEvent, logObj model.SessionLifecycleLog) { if err := s.JmsService.RecordSessionLifecycleLog(sid, event, logObj); err != nil { logger.Errorf("Record session %s lifecycle %s log err: %s", sid, event, err) diff --git a/pkg/session/session.go b/pkg/session/session.go index ae7a37e..2b69212 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -32,9 +32,9 @@ type TunnelSession struct { ConnectedFailedCallback func(err error) error `json:"-"` DisConnectedCallback func() error `json:"-"` - FinishReplayCallback func(guacd.ClientInformation) error `json:"-"` - ReleaseAppletAccount func() error `json:"-"` + + ModelSession *model.Session `json:"-"` } const ( diff --git a/pkg/tunnel/monitor.go b/pkg/tunnel/monitor.go index f4ac702..d2346ad 100644 --- a/pkg/tunnel/monitor.go +++ b/pkg/tunnel/monitor.go @@ -146,7 +146,7 @@ func (m *MonitorCon) Run(ctx context.Context) (err error) { logger.Infof("Monitor[%s] exit: %+v", m.Id, err) return err case <-ctx.Done(): - logger.Info("Monitor[%s] done", m.Id) + logger.Infof("Monitor[%s] done", m.Id) return nil case event := <-retChan.eventCh: if m.Meta == nil { diff --git a/pkg/tunnel/replay_part_upload.go b/pkg/tunnel/replay_part_upload.go new file mode 100644 index 0000000..7e9900c --- /dev/null +++ b/pkg/tunnel/replay_part_upload.go @@ -0,0 +1,305 @@ +package tunnel + +import ( + "bufio" + "encoding/json" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "lion/pkg/common" + "lion/pkg/guacd" + "lion/pkg/jms-sdk-go/model" + "lion/pkg/jms-sdk-go/service" + "lion/pkg/logger" + "lion/pkg/storage" +) + +/* + 原始录像的 part 数据格式 + +data/sessions/e32248ce-2dc8-43c8-b37e-a61d5ee32176 +├── e32248ce-2dc8-43c8-b37e-a61d5ee32176.0.part +├── e32248ce-2dc8-43c8-b37e-a61d5ee32176.0.part.meta +└── e32248ce-2dc8-43c8-b37e-a61d5ee32176.json + +upload +├── e32248ce-2dc8-43c8-b37e-a61d5ee32176.replay.json +├── e32248ce-2dc8-43c8-b37e-a61d5ee32176.0.part.gz +*/ + +const ReplayType = "guacamole" + +type SessionReplayMeta struct { + model.Session + DateEnd common.UTCTime `json:"date_end,omitempty"` + ReplayType string `json:"type,omitempty"` + + PartMetas []PartFileMeta `json:"files,omitempty"` +} + +type PartFileMeta struct { + Name string `json:"name"` + PartMeta +} + +type PartUploader struct { + SessionId string + RootPath string + ApiClient *service.JMService + TermCfg *model.TerminalConfig + + replayMeta SessionReplayMeta + partFiles []os.DirEntry +} + +func (p *PartUploader) preCheckSessionMeta() error { + metaPath := filepath.Join(p.RootPath, p.SessionId+".json") + if _, err := os.Stat(metaPath); err != nil { + logger.Errorf("PartUploader %s get meta file error: %v", p.SessionId, err) + return err + } + metaBuf, err := os.ReadFile(metaPath) + if err != nil { + logger.Errorf("PartUploader %s read meta file error: %v", p.SessionId, err) + return err + } + if err1 := json.Unmarshal(metaBuf, &p.replayMeta); err1 != nil { + logger.Errorf("PartUploader %s unmarshal meta file error: %v", p.SessionId, err) + return err1 + } + if p.replayMeta.DateStart == p.replayMeta.DateEnd { + // 未结束的录像, 计算结束时间,并上传到 core api 作为会话结束时间 + endTime := GetMaxModTime(p.partFiles) + p.replayMeta.DateEnd = common.NewUTCTime(endTime) + // api finish time + if err1 := p.ApiClient.SessionFinished(p.SessionId, p.replayMeta.DateEnd); err1 != nil { + logger.Errorf("PartUploader %s finish session error: %v", p.SessionId, err1) + return err + } + // write meta file + metaBuf, _ = json.Marshal(p.replayMeta) + if err1 := os.WriteFile(metaPath, metaBuf, os.ModePerm); err1 != nil { + logger.Errorf("PartUploader %s write meta file error: %v", p.SessionId, err1) + } + } + p.replayMeta.ReplayType = ReplayType + return nil +} + +func GetMaxModTime(parts []os.DirEntry) time.Time { + var t time.Time + for i := range parts { + partFile := parts[i] + partFileInfo, err := partFile.Info() + if err != nil { + logger.Errorf("PartUploader get part file %s info error: %v", partFile.Name(), err) + continue + } + modTime := partFileInfo.ModTime() + if t.Before(modTime) { + t = modTime + } + } + return t +} + +func (p *PartUploader) Start() { + /* + 1、创建 upload 目录 + 2、将所有的 part 文件压缩成gz文件,并移动到 upload 目录 + 3、生成新的 meta 文件 + 4、上传 + */ + p.CollectionPartFiles() + if err := p.preCheckSessionMeta(); err != nil { + return + } + if len(p.partFiles) == 0 { + logger.Errorf("PartUploader %s no part file", p.SessionId) + return + } + // 1、创建 upload 目录 + uploadPath := filepath.Join(p.RootPath, "upload") + if err := os.MkdirAll(uploadPath, os.ModePerm); err != nil { + logger.Errorf("PartUploader %s create upload dir error: %v", p.SessionId, err) + return + } + // 2、将所有的 part 文件压缩移动到 upload 目录 + for i := range p.partFiles { + partFile := p.partFiles[i] + partFilePath := filepath.Join(p.RootPath, partFile.Name()) + partGzFilename := partFile.Name() + ".gz" + uploadFilePath := filepath.Join(uploadPath, partGzFilename) + + if err := common.CompressToGzipFile(partFilePath, uploadFilePath); err != nil { + logger.Errorf("PartUploader %s compress part file %s error: %v", p.SessionId, partFile.Name(), err) + return + } + + // 3、生成新的 meta 文件 + + partFileMeta := PartFileMeta{Name: partGzFilename} + // 读取 {part}.meta 文件 + if buf, err := os.ReadFile(filepath.Join(p.RootPath, partFile.Name()+".meta")); err == nil { + _ = json.Unmarshal(buf, &partFileMeta.PartMeta) + } else { + meta, err1 := LoadPartMetaByFile(partFilePath) + if err1 != nil { + logger.Errorf("PartUploader %s load part file %s meta error: %v", p.SessionId, partFile.Name(), err1) + return + } + // 存储一份 meta 文件 + metaBuf, _ := json.Marshal(meta) + _ = os.WriteFile(filepath.Join(uploadPath, partFile.Name()+".meta"), metaBuf, os.ModePerm) + partFileMeta.PartMeta = meta + } + p.replayMeta.PartMetas = append(p.replayMeta.PartMetas, partFileMeta) + } + // upload 写入 replayMeta json + replayMetaBuf, _ := json.Marshal(p.replayMeta) + if err := os.WriteFile(filepath.Join(uploadPath, p.SessionId+".replay.json"), replayMetaBuf, os.ModePerm); err != nil { + logger.Errorf("PartUploader %s write replay meta file error: %v", p.SessionId, err) + return + } + // 4、上传 upload 目录下的所有文件到 存储 + p.uploadToStorage(uploadPath) +} + +func (p *PartUploader) CollectionPartFiles() { + entries, err := os.ReadDir(p.RootPath) + if err != nil { + logger.Errorf("PartUploader %s read dir %s error: %v", p.SessionId, p.RootPath, err) + return + } + p.partFiles = make([]os.DirEntry, 0, 5) + for _, entry := range entries { + if entry.IsDir() { + continue + } + if strings.HasSuffix(entry.Name(), ".part") { + p.partFiles = append(p.partFiles, entry) + } + } +} + +func (p *PartUploader) GetStorage() storage.ReplayStorage { + return storage.NewReplayStorage(p.ApiClient, p.TermCfg.ReplayStorage) +} + +const recordDirTimeFormat = "2006-01-02" + +func (p *PartUploader) uploadToStorage(uploadPath string) { + // 上传到存储 + uploadFiles, err := os.ReadDir(uploadPath) + if err != nil { + logger.Errorf("PartUploader %s read upload dir %s error: %v", p.SessionId, uploadPath, err) + return + } + //defaultStorage := storage.ServerStorage{StorageType: "server", JmsService: p.apiClient} + p.RecordLifecycleLog(model.ReplayUploadStart, model.EmptyLifecycleLog) + replayStorage := p.GetStorage() + storageType := replayStorage.TypeName() + dateRoot := p.replayMeta.DateStart.Format(recordDirTimeFormat) + targetRoot := strings.Join([]string{dateRoot, p.SessionId}, "/") + logger.Infof("PartUploader %s upload replay files: %v, type: %s", p.SessionId, uploadFiles, storageType) + for _, uploadFile := range uploadFiles { + if uploadFile.IsDir() { + continue + } + uploadFilePath := filepath.Join(uploadPath, uploadFile.Name()) + targetFile := strings.Join([]string{targetRoot, uploadFile.Name()}, "/") + if err1 := replayStorage.Upload(uploadFilePath, targetFile); err1 != nil { + logger.Errorf("PartUploader %s upload file %s error: %v", p.SessionId, uploadFilePath, err1) + reason := model.SessionLifecycleLog{Reason: err1.Error()} + p.RecordLifecycleLog(model.ReplayUploadFailure, reason) + return + } + logger.Debugf("PartUploader %s upload file %s success", p.SessionId, uploadFilePath) + } + if err = p.ApiClient.FinishReply(p.SessionId); err != nil { + logger.Errorf("PartUploader %s finish replay error: %v", p.SessionId, err) + return + } + + p.RecordLifecycleLog(model.ReplayUploadSuccess, model.EmptyLifecycleLog) + logger.Infof("PartUploader %s upload replay success", p.SessionId) + if err = os.RemoveAll(p.RootPath); err != nil { + logger.Errorf("PartUploader %s remove root path %s error: %v", p.SessionId, p.RootPath, err) + return + } + logger.Infof("PartUploader %s remove root path %s success", p.SessionId, p.RootPath) + +} + +func (p *PartUploader) RecordLifecycleLog(event model.LifecycleEvent, logObj model.SessionLifecycleLog) { + if err := p.ApiClient.RecordSessionLifecycleLog(p.SessionId, event, logObj); err != nil { + logger.Errorf("Record session %s lifecycle %s log err: %s", p.SessionId, event, err) + } +} + +func ReadInstruction(r *bufio.Reader) (guacd.Instruction, error) { + var ret strings.Builder + for { + msg, err := r.ReadString(guacd.ByteSemicolonDelimiter) + if err != nil && msg == "" { + return guacd.Instruction{}, err + } + ret.WriteString(msg) + if retInstruction, err1 := guacd.ParseInstructionString(ret.String()); err1 == nil { + return retInstruction, nil + } else { + logger.Infof("ReadInstruction err: %v\n", err1.Error()) + } + } +} + +func LoadPartMetaByFile(partFile string) (PartMeta, error) { + var partMeta PartMeta + info, err := os.Stat(partFile) + if err != nil { + logger.Errorf("LoadPartMetaByFile stat %s error: %v", partFile, err) + return partMeta, err + } + partMeta.Size = info.Size() + startTime, endTime, err := LoadPartReplayTime(partFile) + if err != nil { + logger.Errorf("LoadPartMetaByFile %s load replay time error: %v", partFile, err) + return partMeta, err + } + partMeta.StartTime = startTime + partMeta.EndTime = endTime + partMeta.Duration = endTime - startTime + return partMeta, nil +} + +func LoadPartReplayTime(partFile string) (startTime int64, endTime int64, err error) { + fd, err := os.Open(partFile) + if err != nil { + return 0, 0, err + } + defer fd.Close() + reader := bufio.NewReader(fd) + for { + inst, err1 := ReadInstruction(reader) + if err1 != nil { + break + } + if inst.Opcode != "sync" { + continue + } + if len(inst.Args) > 0 { + syncMill, err2 := strconv.ParseInt(inst.Args[0], 10, 64) + if err2 != nil { + continue + } + endTime = syncMill + if startTime == 0 { + startTime = syncMill + } + } + } + return startTime, endTime, nil +} diff --git a/pkg/tunnel/replay_recorder.go b/pkg/tunnel/replay_recorder.go new file mode 100644 index 0000000..03e72a6 --- /dev/null +++ b/pkg/tunnel/replay_recorder.go @@ -0,0 +1,258 @@ +package tunnel + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "sync" + + "lion/pkg/common" + "lion/pkg/config" + "lion/pkg/guacd" + "lion/pkg/jms-sdk-go/model" + "lion/pkg/jms-sdk-go/service" + "lion/pkg/logger" + "lion/pkg/session" +) + +type ReplayRecorder struct { + tunnelSession *session.TunnelSession + SessionId string + guacdAddr string + conf guacd.Configuration + info guacd.ClientInformation + newPartChan chan struct{} + currentIndex int + MaxSize int + apiClient *service.JMService + + RootPath string + wg sync.WaitGroup +} + +func (r *ReplayRecorder) run(ctx context.Context) { + r.startRecordPartReplay(ctx) + for { + select { + case <-ctx.Done(): + logger.Infof("ReplayRecorder %s done", r.SessionId) + return + case <-r.newPartChan: + r.currentIndex++ + r.startRecordPartReplay(ctx) + } + } +} + +func (r *ReplayRecorder) startRecordPartReplay(ctx context.Context) { + r.wg.Add(1) + go r.recordReplay(ctx, &r.wg) +} + +func (r *ReplayRecorder) Start(ctx context.Context) { + if r.tunnelSession.TerminalConfig.ReplayStorage.TypeName == "null" { + logger.Warnf("ReplayRecorder %s storage is null, not record", r.SessionId) + return + } + rootPath := filepath.Join(config.GlobalConfig.SessionFolderPath, r.SessionId) + _ = os.MkdirAll(rootPath, os.ModePerm) + r.RootPath = rootPath + r.WriteSessionMeta(r.tunnelSession.Created) + go r.run(ctx) +} + +func (r *ReplayRecorder) WriteSessionMeta(t common.UTCTime) { + var sessionData struct { + model.Session + DateEnd common.UTCTime `json:"date_end"` + } + sessionData.Session = *r.tunnelSession.ModelSession + sessionData.DateEnd = t + metaFilename := r.SessionId + ".json" + metaFilePath := filepath.Join(r.RootPath, metaFilename) + metaBuf, _ := json.Marshal(sessionData) + if err := os.WriteFile(metaFilePath, metaBuf, os.ModePerm); err != nil { + logger.Errorf("ReplayRecorder(%s) Write session meta file %s failed: %v", r.SessionId, metaFilename, err) + return + } + logger.Infof("ReplayRecorder(%s) Write session meta file %s success", r.SessionId, metaFilename) +} + +func (r *ReplayRecorder) Stop() { + r.wg.Wait() + r.WriteSessionMeta(common.NewNowUTCTime()) + uploader := PartUploader{ + RootPath: r.RootPath, + SessionId: r.SessionId, + ApiClient: r.apiClient, + TermCfg: r.tunnelSession.TerminalConfig, + } + go uploader.Start() + + logger.Infof("Replay recorder %s stop and uploading replay parts", r.SessionId) +} + +func (r *ReplayRecorder) GetPartFilename() string { + return fmt.Sprintf("%s.%d.part", r.SessionId, r.currentIndex) +} + +type PartMeta struct { + StartTime int64 `json:"start,omitempty"` + EndTime int64 `json:"end,omitempty"` + Duration int64 `json:"duration,omitempty"` + Size int64 `json:"size,omitempty"` +} + +const ( + PartSuffix = ".part" + MetaSuffix = ".meta" +) + +func (r *ReplayRecorder) recordReplay(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + joinTunnel, err1 := guacd.NewTunnel(r.guacdAddr, r.conf, r.info) + if err1 != nil { + logger.Errorf("Join replay tunnel %s failed: %v", r.SessionId, err1) + return + } + defer joinTunnel.Close() + partFilename := r.GetPartFilename() + partMetaFilename := partFilename + MetaSuffix + partFilePath := filepath.Join(r.RootPath, partFilename) + partMetaFilePath := filepath.Join(r.RootPath, partMetaFilename) + partRecorder := PartRecorder{ + Id: r.SessionId, + MetaFilename: partMetaFilename, + MetaFilePath: partMetaFilePath, + PartFilename: partFilename, + PartFilePath: partFilePath, + MaxSize: r.MaxSize, + currentIndex: r.currentIndex, + ExitSignal: func() { + r.newPartChan <- struct{}{} + }, + } + partRecorder.Start(ctx, joinTunnel) +} + +func NewReplayConfiguration(conf *guacd.Configuration, connectionId string) guacd.Configuration { + newCfg := conf.Clone() + newCfg.ConnectionID = connectionId + newCfg.SetParameter(guacd.READONLY, guacd.BoolTrue) + return newCfg +} + +type PartRecorder struct { + Id string + MetaFilename string + MetaFilePath string + + PartFilename string + PartFilePath string + + MaxSize int + currentIndex int + ExitSignal func() + + StartTime int64 + EndTime int64 +} + +func (p *PartRecorder) String() string { + return fmt.Sprintf("%s, part %d", p.Id, p.currentIndex) +} + +func (p *PartRecorder) Start(ctx context.Context, joinTunnel *guacd.Tunnel) { + fd, err := os.Create(p.PartFilePath) + if err != nil { + logger.Errorf("PartRecorder create replay file %s failed: %v", p.PartFilePath, err) + return + } + defer fd.Close() + writer := bufio.NewWriter(fd) + defer writer.Flush() + totalWrittenSize := 0 + disconnectInst := guacd.NewInstruction(guacd.InstructionClientDisconnect) + var ( + waitExit bool + ) + for { + inst, err2 := joinTunnel.ReadInstruction() + if err2 != nil { + if waitExit && (err2 == io.EOF) { + logger.Infof("PartRecorder(%s) tunnel EOF", p) + break + } + logger.Warnf("PartRecorder(%s) read failed: %v", p, err2) + break + } + if inst.Opcode == INTERNALDATAOPCODE && len(inst.Args) >= 2 && inst.Args[0] == PINGOPCODE { + if err3 := joinTunnel.WriteInstruction(guacd.NewInstruction(INTERNALDATAOPCODE, PINGOPCODE)); err3 != nil { + logger.Warnf("Join tunnel %s write ping failed: %v", p.Id, err3) + } + continue + } + select { + case <-ctx.Done(): + if !waitExit { + _ = joinTunnel.WriteInstructionAndFlush(disconnectInst) + waitExit = true + logger.Infof("PartRecorder(%s) ctx done and sned disconnect to guacd", p) + } else { + logger.Infof("PartRecorder(%s) ctx done and wait exit", p) + } + default: + + } + switch inst.Opcode { + case guacd.InstructionClientSync: + _ = joinTunnel.WriteInstructionAndFlush(inst) + if syncTime, err3 := strconv.ParseInt(inst.Args[0], 10, 64); err3 == nil { + p.EndTime = syncTime + if p.StartTime == 0 { + p.StartTime = syncTime + } + } + case guacd.InstructionClientNop: + logger.Debugf("PartRecorder(%s) receive nop", p) + continue + default: + } + wr, err3 := writer.WriteString(inst.String()) + if err3 != nil { + logger.Errorf("PartRecorder(%s) write failed: %v", p, err3) + } + totalWrittenSize += wr + if totalWrittenSize > p.MaxSize && !waitExit { + _ = joinTunnel.WriteInstructionAndFlush(disconnectInst) + waitExit = true + logger.Infof("PartRecorder(%s) finish, start new part", p) + if p.ExitSignal != nil { + p.ExitSignal() + } + } + if inst.Opcode == guacd.InstructionClientDisconnect { + logger.Infof("PartRecorder(%s) receive disconnect", p) + break + } + } + p.WritePartMeta(totalWrittenSize) +} + +func (p *PartRecorder) WritePartMeta(size int) { + meta := PartMeta{ + StartTime: p.StartTime, + EndTime: p.EndTime, + Duration: p.EndTime - p.StartTime, + Size: int64(size), + } + metaBuf, _ := json.Marshal(meta) + if err := os.WriteFile(p.MetaFilePath, metaBuf, os.ModePerm); err != nil { + logger.Errorf("Write replay meta file %s failed: %v", p.MetaFilename, err) + } +} diff --git a/pkg/tunnel/server.go b/pkg/tunnel/server.go index c72b62b..781940b 100644 --- a/pkg/tunnel/server.go +++ b/pkg/tunnel/server.go @@ -1,6 +1,7 @@ package tunnel import ( + "context" "encoding/json" "fmt" "io" @@ -249,14 +250,30 @@ func (g *GuacamoleTunnelServer) Connect(ctx *gin.Context) { conn.inputFilter = &inputFilter logger.Infof("Session[%s] connect success", sessionId) g.Cache.Add(&conn) + replayRecorder := &ReplayRecorder{ + tunnelSession: &tunnelSession, + SessionId: tunnelSession.ID, + guacdAddr: guacdAddr, + conf: NewReplayConfiguration(&conf, tunnel.UUID()), + info: info.Clone(), + newPartChan: make(chan struct{}, 1), + MaxSize: config.GlobalConfig.ReplayMaxSize, + apiClient: g.JmsService, + currentIndex: 0, + } + childCtx, cancel := context.WithCancel(ctx) + replayRecorder.Start(childCtx) + defer func() { + cancel() + logger.Infof("replayRecorder[%s] stop", sessionId) + replayRecorder.Stop() + + }() _ = conn.Run(ctx) g.Cache.Delete(&conn) if err = tunnelSession.DisConnectedCallback(); err != nil { logger.Errorf("Session DisConnectedCallback err: %+v", err) } - if err = tunnelSession.FinishReplayCallback(info); err != nil { - logger.Errorf("Session Replay upload err: %+v", err) - } logger.Infof("Session[%s] disconnect", sessionId) } From e78763dc76aff9c00144f32eb58c6c8ce1303446 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 17 Sep 2024 13:29:49 +0800 Subject: [PATCH 4/5] perf: fix i18n warning msg --- ui/src/components/GuacFileSystem.vue | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ui/src/components/GuacFileSystem.vue b/ui/src/components/GuacFileSystem.vue index f80ae71..e06f5d9 100644 --- a/ui/src/components/GuacFileSystem.vue +++ b/ui/src/components/GuacFileSystem.vue @@ -183,6 +183,13 @@ export default { iframe.src = url this.$log.debug(url) }, + handleI18nStatus(status) { + let msg = status.message + if (['zh', 'zh_Hant', 'ja'].includes(getLanguage())) { + msg = this.$t(ErrorStatusCodes[status.code]) || status.message + } + return msg + }, fileDrop: function(e) { e.stopPropagation() @@ -192,10 +199,7 @@ export default { this.handleFiles(files[0]).then(() => { this.$message(files[0].name + ' ' + this.$t('UploadSuccess')) }).catch(status => { - let msg = status.message - if (['zh-CN', 'zh-Hant'].includes(getLanguage())) { - msg = this.$t(ErrorStatusCodes[status.code]) || status.message - } + const msg = this.handleI18nStatus(status) this.$warning(msg) }) }, @@ -323,10 +327,7 @@ export default { }).catch(err => { fileObj.onError(err) this.$log.debug('Upload error: ', err) - let msg = err.message - if (['zh-CN', 'zh-Hant'].includes(getLanguage())) { - msg = this.$t(ErrorStatusCodes[err.code]) || err.message - } + const msg = this.handleI18nStatus(err) this.$warning(msg) }) }, From 57ee18e6fd11c1a968b6504f58688dfeaad4ed91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Thu, 19 Sep 2024 18:04:05 +0800 Subject: [PATCH 5/5] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=20actions=20?= =?UTF-8?q?=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/golangci-lint.yml | 51 +++++++++++++++++++ .github/workflows/release-drafter.yml | 43 ++++++++++------ .golangci.yml | 20 ++++++++ .goreleaser.yaml | 70 +++++++++++++++++++++++++++ Makefile | 17 +++---- 5 files changed, 176 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .golangci.yml create mode 100644 .goreleaser.yaml diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..179f97e --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,51 @@ +name: golangci-lint +on: + pull_request: + push: + branches: + - dev + +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + # pull-requests: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create ui/dist directory + run: | + mkdir -p ui/dist + touch ui/dist/.gitkeep + + - uses: actions/setup-go@v5 + with: + go-version: stable + + - uses: golangci/golangci-lint-action@v6 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: latest + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # args: --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # Optional: if set to true then the all caching functionality will be complete disabled, + # takes precedence over all other caching options. + # skip-cache: true + + # Optional: if set to true then the action don't cache or restore ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # skip-build-cache: true \ No newline at end of file diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 7a51b0a..b579cb2 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -13,38 +13,49 @@ jobs: outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - - name: Checkout code - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + /usr/local/share/.cache/yarn + key: ${{ runner.os }}-lion + restore-keys: ${{ runner.os }}-lion + - name: Get version - id: get_version run: | TAG=$(basename ${GITHUB_REF}) - VERSION=${TAG/v/} - echo "::set-output name=TAG::$TAG" - echo "::set-output name=VERSION::$VERSION" + echo "TAG=$TAG" >> $GITHUB_ENV + - name: Create Release id: create_release - uses: release-drafter/release-drafter@v5 + uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: config-name: release-config.yml - version: ${{ steps.get_version.outputs.TAG }} - tag: ${{ steps.get_version.outputs.TAG }} - - uses: actions/setup-node@v2 + version: ${{ env.TAG }} + tag: ${{ env.TAG }} + + - uses: actions/setup-node@v4 with: node-version: '20.15' - - uses: actions/setup-go@v2 + + - uses: actions/setup-go@v5 with: - go-version: '1.22.x' # The Go version to download (if necessary) and use. + go-version: '1.22' # The Go version to download (if necessary) and use. + - name: Make Build id: make_build - env: - VERSION: ${{ steps.get_version.outputs.TAG }} run: | - make all -s && ls build + make all -s && ls build + env: + VERSION: ${{ env.TAG }} + - name: Release Upload Assets - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: draft: true diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..789e7d7 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,20 @@ +run: + timeout: 5m + modules-download-mode: readonly + +issues: + exclude-dirs: + - docs + - ui + - .git + + exclude-files: + - pkg/utils/terminal.go + +linters: + enable: + - govet + - staticcheck + +output: + format: colored-line-number \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..cc13004 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,70 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +version: 2 + +project_name: lion + +before: + hooks: + - go mod tidy + - go generate ./... + +snapshot: + version_template: "{{ .Tag }}-next" + +builds: + - id: lion + main: main.go + binary: lion + goos: + - linux + - darwin + - freebsd + - netbsd + goarch: + - amd64 + - arm64 + - mips64le + - ppc64le + - s390x + - riscv64 + - loong64 + env: + - CGO_ENABLED=0 + ldflags: + - -w -s + - -X 'main.Buildstamp={{ .Date }}' + - -X 'main.Githash={{ .ShortCommit }}' + - -X 'main.Goversion={{ .Env.GOVERSION }}' + - -X 'main.Version={{ .Tag }}' + +archives: + - format: tar.gz + wrap_in_directory: true + files: + - LICENSE + - README.md + - config_example.yml + - ui/dist/** + + format_overrides: + - goos: windows + format: zip + name_template: "{{ .ProjectName }}-{{ .Tag }}-{{ .Os }}-{{ .Arch }}{{- if .Arm }}v{{ .Arm }}{{ end }}" + +checksum: + name_template: "{{ .ProjectName }}_checksums.txt" + +release: + draft: true + mode: append + extra_files: + - glob: dist/*.tar.gz + - glob: dist/*.txt + name_template: "Release {{.Tag}}" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" \ No newline at end of file diff --git a/Makefile b/Makefile index 31a587d..4954e48 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,13 @@ NAME=lion BUILDDIR=build -VERSION ?=Unknown -BuildTime:=$(shell date -u '+%Y-%m-%d %I:%M:%S%p') -COMMIT:=$(shell git rev-parse HEAD) -GOVERSION:=$(shell go version) -GOOS:=$(shell go env GOOS) -GOARCH:=$(shell go env GOARCH) +VERSION ?= Unknown +BuildTime := $(shell date -u '+%Y-%m-%d %I:%M:%S%p') +COMMIT := $(shell git rev-parse HEAD) +GOVERSION := $(shell go version) + +GOOS := $(shell go env GOOS) +GOARCH := $(shell go env GOARCH) LDFLAGS=-w -s @@ -18,8 +19,6 @@ GOLDFLAGS+=-X 'main.Goversion=$(GOVERSION)' GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags "$(GOLDFLAGS) ${LDFLAGS}" UIDIR=ui -NPMINSTALL=yarn -NPMBUILD=yarn build define make_artifact_full GOOS=$(1) GOARCH=$(2) $(GOBUILD) -o $(BUILDDIR)/$(NAME)-$(1)-$(2) @@ -80,7 +79,7 @@ docker: lion-ui: @echo "build ui" - @cd $(UIDIR) && $(NPMINSTALL) && $(NPMBUILD) + @cd $(UIDIR) && yarn install && yarn build clean: rm -rf $(BUILDDIR)