// Package benadis_rac provides functionality for managing 1C service mode package benadis_rac import ( "context" "fmt" "log/slog" "os" "os/exec" "path" "strings" "time" "unicode/utf8" ) // Logger интерфейс для логирования type Logger interface { Debug(msg string, args ...any) Info(msg string, args ...any) Warn(msg string, args ...any) Error(msg string, args ...any) DebugCommand(msg string, command []string) } // Config конфигурация для управления сервисным режимом type Config struct { // RAC configuration RACPath string // Путь к исполняемому файлу RAC ConnectionTimeout time.Duration // Таймаут подключения CommandTimeout time.Duration // Таймаут выполнения команды RetryCount int // Количество повторных попыток RetryDelay time.Duration // Задержка между попытками // Server configuration ServerHost string // Хост сервера 1C ServerPort int // Порт сервера 1C RACPort int // Порт RAC ServerName string // Имя сервера BaseName string // Имя информационной базы // Authentication ClusterAdmin string // Администратор кластера ClusterAdminPassword string // Пароль администратора кластера DBAdmin string // Администратор ИБ DBAdminPassword string // Пароль администратора ИБ // Service mode settings DeniedMessage string // Сообщение при блокировке PermissionCode string // Код разрешения } // ServiceModeManager интерфейс для управления сервисным режимом type ServiceModeManager interface { // EnableServiceMode включает сервисный режим EnableServiceMode(ctx context.Context) error // DisableServiceMode выключает сервисный режим DisableServiceMode(ctx context.Context) error // GetServiceModeStatus получает текущий статус сервисного режима GetServiceModeStatus(ctx context.Context) (bool, error) } // Client клиент для работы с 1C сервисным режимом type Client struct { config *Config logger Logger } // SlogLogger реализация Logger с использованием slog type SlogLogger struct { logger *slog.Logger } // NewLogger создает новый логгер func NewLogger(level string) Logger { var logLevel slog.Level switch strings.ToLower(level) { case "debug": logLevel = slog.LevelDebug case "info": logLevel = slog.LevelInfo case "warn": logLevel = slog.LevelWarn case "error": logLevel = slog.LevelError default: logLevel = slog.LevelInfo } handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: logLevel, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { if a.Key == slog.SourceKey { source := a.Value.Any().(*slog.Source) source.File = path.Base(source.File) } return a }, }) return &SlogLogger{ logger: slog.New(handler), } } func (l *SlogLogger) Debug(msg string, args ...any) { l.logger.Debug(msg, args...) } func (l *SlogLogger) Info(msg string, args ...any) { l.logger.Info(msg, args...) } func (l *SlogLogger) Warn(msg string, args ...any) { l.logger.Warn(msg, args...) } func (l *SlogLogger) Error(msg string, args ...any) { l.logger.Error(msg, args...) } func (l *SlogLogger) DebugCommand(msg string, command []string) { maskedCommand := maskPasswords(command) l.logger.Debug(msg, "command", strings.Join(maskedCommand, " ")) } func maskPasswords(command []string) []string { masked := make([]string, len(command)) copy(masked, command) passwordFlags := []string{"--cluster-pwd", "--infobase-pwd"} for i, arg := range masked { for _, flag := range passwordFlags { if arg == flag && i+1 < len(masked) { masked[i+1] = "***" break } } } return masked } // NewClient создает новый клиент для управления сервисным режимом func NewClient(config *Config) (*Client, error) { if config == nil { return nil, fmt.Errorf("config cannot be nil") } // Создаем логгер logger := NewLogger("info") return &Client{ config: config, logger: logger, }, nil } // ManageServiceMode основная функция для управления сервисным режимом func ManageServiceMode(ctx context.Context, config *Config, logger Logger, enable bool) error { if config == nil { return fmt.Errorf("config cannot be nil") } if logger == nil { return fmt.Errorf("logger cannot be nil") } client := &Client{ config: config, logger: logger, } if enable { return client.EnableServiceMode(ctx) } return client.DisableServiceMode(ctx) } // EnableServiceMode включает сервисный режим func (c *Client) EnableServiceMode(ctx context.Context) error { c.logger.Info("Starting service mode enablement") // Получаем UUID кластера clusterUUID, err := c.getClusterUUID(ctx) if err != nil { return fmt.Errorf("failed to get cluster UUID: %w", err) } // Получаем UUID информационной базы infobaseUUID, err := c.getInfobaseUUID(ctx, clusterUUID) if err != nil { return fmt.Errorf("failed to get infobase UUID: %w", err) } // Включаем сервисный режим if err := c.setServiceMode(ctx, clusterUUID, infobaseUUID, true); err != nil { return fmt.Errorf("failed to enable service mode: %w", err) } // Завершаем все активные сессии if err := c.terminateAllSessions(ctx, clusterUUID, infobaseUUID); err != nil { c.logger.Warn("Failed to terminate all sessions", "error", err) } // Проверяем статус if err := c.verifyServiceMode(ctx, clusterUUID, infobaseUUID, true); err != nil { return fmt.Errorf("service mode verification failed: %w", err) } c.logger.Info("Service mode enabled successfully") return nil } // DisableServiceMode выключает сервисный режим func (c *Client) DisableServiceMode(ctx context.Context) error { c.logger.Info("Starting service mode disablement") // Получаем UUID кластера clusterUUID, err := c.getClusterUUID(ctx) if err != nil { return fmt.Errorf("failed to get cluster UUID: %w", err) } // Получаем UUID информационной базы infobaseUUID, err := c.getInfobaseUUID(ctx, clusterUUID) if err != nil { return fmt.Errorf("failed to get infobase UUID: %w", err) } // Выключаем сервисный режим if err := c.setServiceMode(ctx, clusterUUID, infobaseUUID, false); err != nil { return fmt.Errorf("failed to disable service mode: %w", err) } // Проверяем статус if err := c.verifyServiceMode(ctx, clusterUUID, infobaseUUID, false); err != nil { return fmt.Errorf("service mode verification failed: %w", err) } c.logger.Info("Service mode disabled successfully") return nil } // GetServiceModeStatus получает текущий статус сервисного режима func (c *Client) GetServiceModeStatus(ctx context.Context) (bool, error) { // Получаем UUID кластера clusterUUID, err := c.getClusterUUID(ctx) if err != nil { return false, fmt.Errorf("failed to get cluster UUID: %w", err) } // Получаем UUID информационной базы infobaseUUID, err := c.getInfobaseUUID(ctx, clusterUUID) if err != nil { return false, fmt.Errorf("failed to get infobase UUID: %w", err) } // Получаем информацию об информационной базе args := []string{ c.config.RACPath, "infobase", "info", "--cluster=" + clusterUUID, "--infobase=" + infobaseUUID, "--cluster-user=" + c.config.ClusterAdmin, "--cluster-pwd=" + c.config.ClusterAdminPassword, "--infobase-user=" + c.config.DBAdmin, "--infobase-pwd=" + c.config.DBAdminPassword, c.getRACAddress(), } output, err := c.executeCommand(ctx, args) if err != nil { return false, fmt.Errorf("failed to get infobase info: %w", err) } // Проверяем статус сервисного режима return strings.Contains(output, "sessions-deny") && strings.Contains(output, "on"), nil } // Константы const ( DefaultDeniedMessage = "Техническое обслуживание. Попробуйте позже." DefaultPermissionCode = "service-mode" TechnicalMaintenanceMessage = "Техническое обслуживание" UUIDLength = 36 UUIDDashCount = 4 DefaultRACPort = 1545 DefaultConnectionTimeout = 30 * time.Second DefaultCommandTimeout = 60 * time.Second DefaultRetryCount = 3 DefaultRetryDelay = 5 * time.Second ) // getRACAddress возвращает адрес RAC сервера func (c *Client) getRACAddress() string { port := c.config.RACPort if port == 0 { port = DefaultRACPort } return fmt.Sprintf("%s:%d", c.config.ServerHost, port) } // executeCommand выполняет RAC команду с retry логикой func (c *Client) executeCommand(ctx context.Context, args []string) (string, error) { var lastErr error retryCount := c.config.RetryCount if retryCount == 0 { retryCount = DefaultRetryCount } retryDelay := c.config.RetryDelay if retryDelay == 0 { retryDelay = DefaultRetryDelay } for attempt := 1; attempt <= retryCount; attempt++ { output, err := c.executeCommandOnce(ctx, args) if err == nil { return output, nil } lastErr = err c.logger.Warn("Command execution failed", "attempt", attempt, "error", err) if attempt < retryCount { c.logger.Info("Retrying after delay", "delay", retryDelay) select { case <-ctx.Done(): return "", ctx.Err() case <-time.After(retryDelay): } } } return "", fmt.Errorf("command failed after %d attempts: %w", retryCount, lastErr) } // executeCommandOnce выполняет RAC команду один раз func (c *Client) executeCommandOnce(ctx context.Context, args []string) (string, error) { c.logger.DebugCommand("Executing RAC command", args) cmdTimeout := c.config.CommandTimeout if cmdTimeout == 0 { cmdTimeout = DefaultCommandTimeout } cmdCtx, cancel := context.WithTimeout(ctx, cmdTimeout) defer cancel() cmd := exec.CommandContext(cmdCtx, args[0], args[1:]...) output, err := cmd.Output() if err != nil { c.logger.Error("RAC command failed", "error", err, "output", string(output)) return "", fmt.Errorf("RAC command failed: %w, output: %s", err, string(output)) } utf8Output := convertToUTF8(output) c.logger.Debug("RAC command executed successfully", "output_length", len(utf8Output)) return utf8Output, nil } // convertToUTF8 конвертирует байты в UTF-8 строку func convertToUTF8(data []byte) string { if utf8.Valid(data) { return string(data) } // Пытаемся декодировать как Windows-1251 runes := make([]rune, 0, len(data)) for _, b := range data { if b < 128 { runes = append(runes, rune(b)) } else { // Простая таблица конвертации для основных символов Windows-1251 switch b { case 0xC0: // А runes = append(runes, 'А') case 0xC1: // Б runes = append(runes, 'Б') case 0xC2: // В runes = append(runes, 'В') case 0xC3: // Г runes = append(runes, 'Г') case 0xC4: // Д runes = append(runes, 'Д') case 0xC5: // Е runes = append(runes, 'Е') case 0xC6: // Ж runes = append(runes, 'Ж') case 0xC7: // З runes = append(runes, 'З') case 0xC8: // И runes = append(runes, 'И') case 0xC9: // Й runes = append(runes, 'Й') case 0xCA: // К runes = append(runes, 'К') case 0xCB: // Л runes = append(runes, 'Л') case 0xCC: // М runes = append(runes, 'М') case 0xCD: // Н runes = append(runes, 'Н') case 0xCE: // О runes = append(runes, 'О') case 0xCF: // П runes = append(runes, 'П') case 0xD0: // Р runes = append(runes, 'Р') case 0xD1: // С runes = append(runes, 'С') case 0xD2: // Т runes = append(runes, 'Т') case 0xD3: // У runes = append(runes, 'У') case 0xD4: // Ф runes = append(runes, 'Ф') case 0xD5: // Х runes = append(runes, 'Х') case 0xD6: // Ц runes = append(runes, 'Ц') case 0xD7: // Ч runes = append(runes, 'Ч') case 0xD8: // Ш runes = append(runes, 'Ш') case 0xD9: // Щ runes = append(runes, 'Щ') case 0xDA: // Ъ runes = append(runes, 'Ъ') case 0xDB: // Ы runes = append(runes, 'Ы') case 0xDC: // Ь runes = append(runes, 'Ь') case 0xDD: // Э runes = append(runes, 'Э') case 0xDE: // Ю runes = append(runes, 'Ю') case 0xDF: // Я runes = append(runes, 'Я') case 0xE0: // а runes = append(runes, 'а') case 0xE1: // б runes = append(runes, 'б') case 0xE2: // в runes = append(runes, 'в') case 0xE3: // г runes = append(runes, 'г') case 0xE4: // д runes = append(runes, 'д') case 0xE5: // е runes = append(runes, 'е') case 0xE6: // ж runes = append(runes, 'ж') case 0xE7: // з runes = append(runes, 'з') case 0xE8: // и runes = append(runes, 'и') case 0xE9: // й runes = append(runes, 'й') case 0xEA: // к runes = append(runes, 'к') case 0xEB: // л runes = append(runes, 'л') case 0xEC: // м runes = append(runes, 'м') case 0xED: // н runes = append(runes, 'н') case 0xEE: // о runes = append(runes, 'о') case 0xEF: // п runes = append(runes, 'п') case 0xF0: // р runes = append(runes, 'р') case 0xF1: // с runes = append(runes, 'с') case 0xF2: // т runes = append(runes, 'т') case 0xF3: // у runes = append(runes, 'у') case 0xF4: // ф runes = append(runes, 'ф') case 0xF5: // х runes = append(runes, 'х') case 0xF6: // ц runes = append(runes, 'ц') case 0xF7: // ч runes = append(runes, 'ч') case 0xF8: // ш runes = append(runes, 'ш') case 0xF9: // щ runes = append(runes, 'щ') case 0xFA: // ъ runes = append(runes, 'ъ') case 0xFB: // ы runes = append(runes, 'ы') case 0xFC: // ь runes = append(runes, 'ь') case 0xFD: // э runes = append(runes, 'э') case 0xFE: // ю runes = append(runes, 'ю') case 0xFF: // я runes = append(runes, 'я') default: runes = append(runes, rune(b)) } } } return string(runes) }