//go:build client package client import ( "archive/zip" "bufio" "context" "encoding/json" "errors" "fmt" "io" "net/http" "os" "os/exec" "path" "path/filepath" "strings" "syscall" "time" "repositories.action2quare.com/ayo/gocommon/logger" "repositories.action2quare.com/ayo/houston/shared" "repositories.action2quare.com/ayo/houston/shared/protos" ) func lastExecutionArgs(verpath string) []string { argf, err := os.Open(path.Join(verpath, "@args")) if os.IsNotExist(err) { argf, err = os.Open(path.Clean(path.Join(verpath, "..", "@args"))) if os.IsNotExist(err) { return nil } } defer argf.Close() var out []string dec := json.NewDecoder(argf) dec.Decode(&out) return out } var errUploadZipLogFailed = errors.New("not ok") func (hc *houstonClient) uploadZipLogFile(zipFile string, name string, version string) error { zf, err := os.Open(zipFile) if err != nil { return err } if zf == nil { return errUploadZipLogFailed } defer zf.Close() req, err := http.NewRequest("POST", hc.config.HttpAddress+"/upload", zf) if err != nil { logger.Println(err) } req.Header.Set("Houston-Service-Name", name) req.Header.Set("Houston-Service-Version", version) req.Header.Set("Houston-Service-Filename", path.Base(zipFile)) req.Header.Set("Content-Type", "application/zip") resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return errUploadZipLogFailed } return nil } func zipLogFiles(storageRoot string, req *shared.UploadRequest, start, except string) (string, []string, error) { root := path.Join(storageRoot, req.Name, req.Version) matches, err := filepath.Glob(path.Join(root, req.Filter)) if err != nil { return "", nil, err } if len(matches) == 0 { return "", nil, nil } for i, file := range matches { file = filepath.ToSlash(file) matches[i] = file } root = path.Join(root, path.Dir(req.Filter)) zipFileName := path.Join(os.TempDir(), path.Base(matches[0])) + ".zip" f, err := os.OpenFile(zipFileName, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) if err != nil { return "", nil, err } defer f.Close() w := zip.NewWriter(f) defer w.Close() oldestFile := "" for i, file := range matches { if file == root { continue } if len(except) > 0 && file >= except { matches = matches[:i] break } if len(start) > 0 && file < start { continue } if len(oldestFile) == 0 { oldestFile = path.Base(file) } relative := file[len(root)+1:] fw, err := w.Create(relative) if err != nil { logger.Println(err) return "", nil, err } src, err := os.Open(file) if err != nil { logger.Println(err) return "", nil, err } defer src.Close() if _, err = io.Copy(fw, src); err != nil { logger.Println(err) return "", nil, err } } return f.Name(), matches, nil // defer func() { // tempname := f.Name() // f.Close() // resp, _ := http.Post(req.Url, "application/zip", f) // if resp != nil && resp.Body != nil { // resp.Body.Close() // } // os.Remove(tempname) // if del, err := strconv.ParseBool(req.DeleteAfterUploaded); del && err == nil { // for _, file := range matches { // if strings.HasSuffix(file, except) { // continue // } // os.Remove(file) // } // } // }() // Create a new zip archive. //}(f) //return nil } func prepareProcessLaunch(storageRoot string, req *shared.StartProcessRequest) *procmeta { if len(req.Args) == 0 { return nil } verpath := path.Join(storageRoot, req.Name, req.Version) fi, err := os.Stat(verpath) if err == nil && fi.IsDir() { exefile := "./" + path.Clean(strings.TrimPrefix(req.Args[0], "/")) os.Chmod(path.Join(verpath, exefile), 0777) exef, _ := os.Executable() cmd := exec.Command(path.Join(path.Dir(exef), verpath, exefile), req.Args[1:]...) cmd.Dir = verpath stdin, _ := cmd.StdinPipe() return &procmeta{ cmd: cmd, name: req.Name, args: req.Args, version: req.Version, state: protos.ProcessState_Stopped, stdin: stdin, } } return nil } func makeLogFile(meta *procmeta, idx int) string { now := time.Now().UTC() ext := path.Ext(meta.args[0]) nameonly := path.Base(meta.args[0]) if len(ext) > 0 { nameonly = nameonly[:len(nameonly)-len(ext)] } ts := now.Format("2006-01-02T15-04-05") stdPrefix := path.Join(meta.cmd.Dir, "logs", fmt.Sprintf("%s_%s", nameonly, ts)) return fmt.Sprintf("%s_%d.log", stdPrefix, idx) } func (hc *houstonClient) launch(meta *procmeta) error { stdout, err := meta.cmd.StdoutPipe() if err != nil { return err } stderr, err := meta.cmd.StderrPipe() if err != nil { return err } err = os.MkdirAll(path.Join(meta.cmd.Dir, "logs"), 0775) if err != nil { return err } stdReader := func(r io.ReadCloser) { defer func() { reco := recover() if reco != nil { logger.Println(reco) } }() defer r.Close() reader := bufio.NewReader(r) thisFileSize := 0 logFileIndex := 0 logFileName := makeLogFile(meta, logFileIndex) targetFile, err := os.Create(logFileName) if err != nil { logger.Println("failed to create log file :", logFileName) return } defer func() { if targetFile != nil { targetFile.Close() } }() for { buff, err := reader.ReadBytes('\n') if err != nil { break } for written := 0; written < len(buff); { n, err := targetFile.Write(buff) if err != nil { logger.Println("write log file failed :", logFileName, err) break } else { written += n thisFileSize += n } } if thisFileSize > 10*1024*1024 { logFileIndex++ logFileName := makeLogFile(meta, logFileIndex) nextTargetFile, err := os.Create(logFileName) if err != nil { logger.Println("failed to create log file :", logFileName) } else { targetFile.Close() targetFile = nextTargetFile thisFileSize = 0 } } } } go stdReader(stderr) go stdReader(stdout) logger.Println("startChildProcess :", meta.cmd.Args) err = meta.cmd.Start() if err == nil { meta.state = protos.ProcessState_Running } return err } var errPrepareprocessLaunchFailed = errors.New("prepareProcessLaunch failed") func (hc *houstonClient) startChildProcess(req *shared.StartProcessRequest, op protos.OperationClient) error { if req.Version == "latest" { // 최신 버전을 찾음 latest, err := shared.FindLastestVersion(hc.config.StorageRoot, req.Name) if err != nil { return err } req.Version = latest } meta := prepareProcessLaunch(hc.config.StorageRoot, req) if meta == nil { return errPrepareprocessLaunchFailed } if err := hc.launch(meta); err != nil { return err } // launch가 성공하면 args 저장. this and parent folder vers := hc.deploys[req.Name] for _, ver := range vers { if ver.Version == req.Version { ver.Args = meta.args } } if argfile, err := os.Create(path.Join(hc.config.StorageRoot, req.Name, "@args")); err == nil { enc := json.NewEncoder(argfile) enc.Encode(req.Args) argfile.Close() } if argfile, err := os.Create(path.Join(hc.config.StorageRoot, req.Name, req.Version, "@args")); err == nil { enc := json.NewEncoder(argfile) enc.Encode(req.Args) argfile.Close() } hc.childProcs = append(hc.childProcs, meta) op.Refresh(context.Background(), hc.makeOperationQueryRequest()) return nil } var errNoRunningProcess = errors.New("no running processed") func (hc *houstonClient) stopChildProcess(req *shared.StopProcessRequest, op protos.OperationClient) error { if req.Version == "latest" { // 최신 버전을 찾음 latest, err := shared.FindLastestVersion(hc.config.StorageRoot, req.Name) if err != nil { return err } req.Version = latest } var remains []*procmeta var killing []*procmeta for _, proc := range hc.childProcs { if proc.state != protos.ProcessState_Running { continue } if req.Pid != 0 { if req.Pid == int32(proc.cmd.Process.Pid) { // 해당 pid만 제거 killing = append(killing, proc) } else { remains = append(remains, proc) } } else if proc.name == req.Name { if len(req.Version) == 0 { // program 다 정지 killing = append(killing, proc) } else if req.Version == proc.version { // program의 특정 버전만 정지 killing = append(killing, proc) } else { // 해당 사항 없음 remains = append(remains, proc) } } else { // 해당 사항 없음 remains = append(remains, proc) } } if len(killing) > 0 { for _, proc := range killing { proc.state = protos.ProcessState_Stopping if err := proc.cmd.Process.Signal(syscall.SIGTERM); err != nil { proc.cmd.Process.Signal(os.Kill) } } op.Refresh(context.Background(), hc.makeOperationQueryRequest()) for _, proc := range killing { proc.cmd.Wait() proc.cmd.Process.Release() } hc.childProcs = remains op.Refresh(context.Background(), hc.makeOperationQueryRequest()) return nil } return errNoRunningProcess } func (hc *houstonClient) restartChildProcess(req *shared.RestartProcessRequest, op protos.OperationClient) error { for _, proc := range hc.childProcs { if proc.cmd.Process.Pid == int(req.Pid) { if len(req.Config) > 0 { // config.json를 먼저 다운로드 시도 root := proc.cmd.Dir if _, err := download(root, hc.makeDownloadUrl(req.Config), "", nil); err != nil { return err } } proc.state = protos.ProcessState_Restart op.Refresh(context.Background(), hc.makeOperationQueryRequest()) hc.exitChan <- proc.cmd break } } return nil } func (hc *houstonClient) uploadFiles(req *shared.UploadRequest) error { if req.Version == "latest" { // 최신 버전을 찾음 latest, err := shared.FindLastestVersion(hc.config.StorageRoot, req.Name) if err != nil { return err } req.Version = latest } logger.Println("uploadFiles req :", *req) for _, child := range hc.childProcs { if child.version == req.Version && child.name == req.Name { logger.Println("uploadFiles found :", child.version, child.name) go func() { zipFile, srcFiles, err := zipLogFiles(hc.config.StorageRoot, req, "", "") if err == nil && len(zipFile) > 0 && len(srcFiles) > 0 { if err = hc.uploadZipLogFile(zipFile, child.name, child.version); err == nil { // 마지막거 빼고 삭제 for i := 0; i < len(srcFiles)-1; i++ { os.Remove(srcFiles[i]) } } else { logger.Println("uploadZipLogFile failed :", err) } } else if err != nil { logger.Println("zipLogFiles failed :", err) } }() return nil } } // 실행 중이 아닌 폴더에서도 대상을 찾는다 // 전체 파일을 대상으로 zipFile, srcFiles, err := zipLogFiles(hc.config.StorageRoot, req, "", "") if err == nil && len(zipFile) > 0 && len(srcFiles) > 0 { if err = hc.uploadZipLogFile(zipFile, req.Name, req.Version); err != nil { logger.Println("uploadZipLogFile failed :", err) } } else if err != nil { logger.Println("zipLogFiles failed :", err) } return nil }