Files
benadis-rac/benadis_rac.go
gitops 46261f066b refactor: restructure library to expose public API directly
feat: add new helper methods for RAC operations
docs: add comprehensive library usage documentation
chore: move example to examples/library_usage directory
2025-08-04 12:03:45 +03:00

513 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}