package welcome import ( "context" "os" "os/signal" "path/filepath" "strings" "syscall" "time" "bash_go_service/shared/pkg/constants" "bash_go_service/shared/pkg/logger" "github.com/fsnotify/fsnotify" ) type Service struct { watcher *fsnotify.Watcher sigChan chan os.Signal stopChan chan struct{} } func NewService() (*Service, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err } return &Service{ watcher: watcher, sigChan: make(chan os.Signal, 1), stopChan: make(chan struct{}), }, nil } func (s *Service) checkExistingTerminals() { entries, err := os.ReadDir(constants.UtmpDir) if err != nil { logger.Error("Error reading %s: %v", constants.UtmpDir, err) return } for _, entry := range entries { if !entry.IsDir() { path := filepath.Join(constants.UtmpDir, entry.Name()) if isValidTerminal(path) { go s.handleNewTerminal(path) } } } } func (s *Service) Start() error { err := s.watcher.Add(constants.UtmpDir) if err != nil { return err } signal.Notify(s.sigChan, syscall.SIGINT, syscall.SIGTERM) s.checkExistingTerminals() logger.Info("Service started") go s.watch() return nil } func (s *Service) Stop() { close(s.stopChan) s.watcher.Close() signal.Stop(s.sigChan) } func (s *Service) watch() { for { select { case event, ok := <-s.watcher.Events: if !ok { return } if event.Op&fsnotify.Create == fsnotify.Create { go s.handleNewTerminal(event.Name) } case err, ok := <-s.watcher.Errors: if !ok { return } logger.Error("Watcher error: %v", err) case sig := <-s.sigChan: logger.Debug("Received signal: %v", sig) return case <-s.stopChan: return } } } // 判断是否为有效的终端设备 func isValidTerminal(path string) bool { // 只处理 tty 和 pts 设备 basename := filepath.Base(path) // 如果是tty设备,直接返回true if strings.HasPrefix(basename, "tty") { return true } // 检查设备文件是否真实存在且可写 info, err := os.Stat(path) if err != nil { return false } // 检查是否为字符设备 if stat, ok := info.Sys().(*syscall.Stat_t); ok { // 检查是否为字符设备(参考 Linux 设备文件类型) return (stat.Mode & syscall.S_IFMT) == syscall.S_IFCHR } return false } func (s *Service) handleNewTerminal(path string) { // 首先检查是否为虚拟终端 if !isValidTerminal(path) { logger.Debug("Skipping no valied terminal: %s", path) return } // 创建一个带超时的context ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // 使用channel来控制任务完成 done := make(chan bool) go func() { defer close(done) // 尝试读取欢迎文件内容 content, err := readWelcomeFile() if err != nil { logger.Error("Failed to read welcome file: %v", err) return } // 尝试打开终端 f, err := os.OpenFile(path, os.O_WRONLY, 0) if err != nil { logger.Error("Error opening terminal %s: %v", path, err) return } defer f.Close() // 写入文件内容 if _, err = f.WriteString(content); err != nil { logger.Error("Error writing to terminal %s: %v", path, err) return } // 写入换行符 if _, err = f.Write([]byte{'\n'}); err != nil { logger.Error("Error sending newline to terminal %s: %v", path, err) return } logger.Info("Welcome message sent to %s", path) done <- true }() // 等待任务完成或超时 select { case <-ctx.Done(): logger.Error("Timeout while sending welcome message to %s", path) return case <-done: return } } // 读取欢迎文件内容的辅助函数 func readWelcomeFile() (string, error) { // 检查文件是否存在 if _, err := os.Stat(constants.WelcomeFilePath); os.IsNotExist(err) { return "已连接到北溟服务,您执行的命令将由北溟提供帮助", nil } // 读取文件内容 content, err := os.ReadFile(constants.WelcomeFilePath) if err != nil { return "已连接到北溟服务,您执行的命令将由北溟提供帮助", nil } // 去除多余的空白字符 return strings.TrimSpace(string(content)), nil }