feat: add new helper methods for RAC operations docs: add comprehensive library usage documentation chore: move example to examples/library_usage directory
513 lines
14 KiB
Go
513 lines
14 KiB
Go
// 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)
|
||
}
|