Pisanie GUI dla 1C RAC, czyli znowu o Tcl/Tk

Gdy zagłębiliśmy się w temat działania produktów 1C w środowisku Linux, odkryto jedną wadę - brak wygodnego graficznego wieloplatformowego narzędzia do zarządzania klastrem serwerów 1C. Postanowiono naprawić tę wadę, pisząc GUI dla narzędzia konsoli rac. Jako język programistyczny wybrano Tcl/tk, który moim zdaniem jest najbardziej odpowiedni do tego zadania. Dlatego też chciałbym w tym materiale przedstawić kilka ciekawych aspektów rozwiązania.

Do pracy będziesz potrzebować dystrybucji tcl/tk i 1C. A ponieważ zdecydowałem się maksymalnie wykorzystać możliwości podstawowego dostarczania tcl/tk bez korzystania z pakietów firm trzecich, będę potrzebować wersji 8.6.7, która zawiera ttk - pakiet z dodatkowymi elementami graficznymi, których głównie potrzebujemy ttk ::TreeView umożliwia wyświetlanie danych zarówno w formie struktury drzewiastej, jak i w formie tabeli (listy). W nowej wersji przerobiono także pracę z wyjątkami (polecenie try, które jest wykorzystywane w projekcie podczas uruchamiania poleceń zewnętrznych).

Projekt składa się z kilku plików (choć nic nie stoi na przeszkodzie, żeby zrobić wszystko w jednym):

rac_gui.cfg - domyślna konfiguracja
rac_gui.tcl - główny skrypt uruchamiający
Katalog lib zawiera pliki, które są automatycznie ładowane podczas uruchamiania:
funkcja.tcl - plik z procedurami
gui.tcl - główny interfejs graficzny
obrazy.tcl - biblioteka obrazów base64

W rzeczywistości plik rac_gui.tcl uruchamia interpreter, inicjuje zmienne, ładuje moduły, konfiguracje i tak dalej. Zawartość pliku z komentarzami:

rac_gui.tcl

#!/bin/sh
exec wish "$0" -- "$@"

# Устанавливаем текущий каталог
set dir(root) [pwd]
# Устанавливаем рабочий каталог, если его нет то создаём
set dir(work) [file join $env(HOME) .rac_gui]
if {[file exists $dir(work)] == 0 } {
    file mkdir $dir(work)    
}
# каталог с модулями
set dir(lib) "[file join $dir(root) lib]"

# загружаем пользовательский конфиг, если он отсутствует, то копируем дефолтный
if {[file exists [file join $dir(work) rac_gui.cfg]] ==0} {
    file copy [file join [pwd] rac_gui.cfg] [file join $dir(work) rac_gui.cfg]
} 
source [file join $dir(work) rac_gui.cfg]
# Код проверки наличия rac и правильности указания пути в конфиге
# если программа не найдена то будет выведен диалог для указания корректного пути
# и этот путь будет записан в пользовательский конфиг
if {[file exists $rac_cmd] == 0} {
    set rac_cmd [tk_getOpenFile -initialdir $env(HOME) -parent . -title "Укажите путь до rac" -initialfile rac]
    file copy [file join $dir(work) rac_gui.cfg] [file join $dir(work) rac_gui.cfg.bak] 
    set orig_file [open [file join $dir(work) rac_gui.cfg.bak] "r"]
    set file [open [file join $dir(work) rac_gui.cfg] "w"]
    while {[gets $orig_file line] >=0 } {
        if {[string match "set rac_cmd*" $line]} {
            puts $file "set rac_cmd $rac_cmd"
        } else {
            puts $file $line
        }
    }
    close $file
    close $orig_file
    #return "$host:$port"
    file delete [file join $dir(work) 1c_srv.cfg.bak] 
} else {
    puts "Found $rac_cmd"
}

set cluster_user ""
set cluster_pwd ""
set agent_user ""
set agent_pwd ""
## LOAD FILE ##
# Загружаем модули кроме gui.tcl так как его надо загрузить последним
foreach modFile [lsort [glob -nocomplain [file join $dir(lib) *.tcl]]] {
    if {[file tail $modFile] ne "gui.tcl"} {
        source $modFile
        puts "Loaded module $modFile"
    }
}
source [file join $dir(lib) gui.tcl]
source [file join $dir(work) rac_gui.cfg]

# Читаем файл со списком серверов 1С
# и добавляем в дерево
if [file exists [file join $dir(work) 1c_srv.cfg]] {
    set f [open [file join $dir(work) 1c_srv.cfg] "RDONLY"]
    while {[gets $f line] >=0} {
        .frm_tree.tree insert {} end -id "server::$line" -text "$line" -values "$line"
    }    
}

Po pobraniu wszystkiego, co jest wymagane i sprawdzeniu obecności narzędzia rac, uruchomi się okno graficzne. Interfejs programu składa się z trzech elementów:

Pasek narzędzi, drzewo i lista

Uczyniłem zawartość „drzewa” jak najbardziej zbliżoną do standardowego wyposażenia Windows z 1C.

Pisanie GUI dla 1C RAC, czyli znowu o Tcl/Tk

Główny kod tworzący to okno jest zawarty w pliku
lib/gui.tcl

# установка размера и положения основного окна
# можно установить в переменную topLevelGeometry в конфиг программы
if {[info exists topLevelGeometry]} {
    wm geometry . $topLevelGeometry
} else {
    wm geometry . 1024x768
}
# Заголовок окна
wm title . "1C Rac GUI"
wm iconname . "1C Rac Gui"
# иконка окна (берется из файла lib/imges.tcl)
wm iconphoto . tcl
wm protocol . WM_DELETE_WINDOW Quit
wm overrideredirect . 0
wm positionfrom . user

ttk::style theme use clam

# Панель инсрументов
set frm_tool [frame .frm_tool]
pack $frm_tool -side left -fill y 
ttk::panedwindow .panel -orient horizontal -style TPanedwindow
pack .panel -expand true -fill both
pack propagate .panel false

ttk::button $frm_tool.btn_add  -command Add  -image add_grey_32
ttk::button $frm_tool.btn_del  -command Del -image del_grey_32
ttk::button $frm_tool.btn_edit  -command Edit -image edit_grey_32
ttk::button $frm_tool.btn_quit -command Quit -image quit_grey_32

pack $frm_tool.btn_add $frm_tool.btn_del $frm_tool.btn_edit -side top -padx 5 -pady 5
pack $frm_tool.btn_quit  -side bottom -padx 5 -pady 5

# Дерево с полосами прокрутки
set frm_tree [frame .frm_tree]

ttk::scrollbar $frm_tree.hsb1 -orient horizontal -command [list $frm_tree.tree xview]
ttk::scrollbar $frm_tree.vsb1 -orient vertical -command [list $frm_tree.tree yview]
set tree [ttk::treeview $frm_tree.tree -show tree 
-xscrollcommand [list $frm_tree.hsb1 set] -yscrollcommand [list $frm_tree.vsb1 set]]

grid $tree -row 0 -column 0 -sticky nsew
grid $frm_tree.vsb1 -row 0 -column 1 -sticky nsew
grid $frm_tree.hsb1 -row 1 -column 0 -sticky nsew
grid columnconfigure $frm_tree 0 -weight 1
grid rowconfigure $frm_tree 0 -weight 1

# назначение обработчика нажатия кнопкой мыши
bind $frm_tree.tree <ButtonRelease> "TreePress $frm_tree.tree"

# Список для данных (таблица)
set frm_work [frame .frm_work]
ttk::scrollbar $frm_work.hsb -orient horizontal -command [list $frm_work.tree_work xview]
ttk::scrollbar $frm_work.vsb -orient vertical -command [list $frm_work.tree_work yview]
set tree_work [
    ttk::treeview $frm_work.tree_work 
    -show headings  -columns "par val" -displaycolumns "par val"
    -xscrollcommand [list $frm_work.hsb set] 
    -yscrollcommand [list $frm_work.vsb set]
]
# Установка цветов для чередования в таблице
$tree_work tag configure dark -background $color(dark_table_bg)
$tree_work tag configure light -background $color(light_table_bg)

# Размещение элементов на форме
grid $tree_work -row 0 -column 0 -sticky nsew
grid $frm_work.vsb -row 0 -column 1 -sticky nsew
grid $frm_work.hsb -row 1 -column 0 -sticky nsew
grid columnconfigure $frm_work 0 -weight 1
grid rowconfigure $frm_work 0 -weight 1
pack $frm_tree $frm_work -side left -expand true -fill both

#.panel add $frm_tool -weight 1
.panel add $frm_tree -weight 1 
.panel add $frm_work -weight 1

Algorytm pracy z programem jest następujący:

1. Najpierw należy dodać główny serwer klastra (czyli serwer zarządzania klastrem (w systemie Linux zarządzanie uruchamia się poleceniem „/opt/1C/v8.3/x86_64/ras klaster —daemon”)).

Aby to zrobić, kliknij przycisk „+” i w oknie, które zostanie otwarte, wpisz adres serwera i port:

Pisanie GUI dla 1C RAC, czyli znowu o Tcl/Tk

Następnie nasz serwer pojawi się w drzewku po kliknięciu na niego, otworzy się lista klastrów lub wyświetli się błąd połączenia.

2. Kliknięcie na nazwę klastra spowoduje otwarcie listy dostępnych dla niego funkcji.

3. ...

I tak dalej, tj. aby dodać nowy klaster, wybierz dowolny dostępny na liście i naciśnij przycisk „+” na pasku narzędzi, a wyświetli się okno dialogowe dodawania nowego:

Pisanie GUI dla 1C RAC, czyli znowu o Tcl/Tk

Przyciski na pasku narzędzi realizują funkcje zależne od kontekstu, tj. W zależności od tego, który element drzewa lub listy zostanie wybrany, zostanie wykonana ta lub inna procedura.

Spójrzmy na przykład przycisku dodawania („+”):

Kod generacji przycisku:

ttk::button $frm_tool.btn_add  -command Add  -image add_grey_32

Tutaj widzimy, że po naciśnięciu przycisku zostanie wykonana procedura „Dodaj”, jej kod:

proc Add {} {
    global active_cluster host
    # Определяем идентификатор выделенного элемента
    set id  [.frm_tree.tree selection] 
    # Определяем значение этого элемента
    set values [.frm_tree.tree item [.frm_tree.tree selection] -values]
    set key [lindex [split $id "::"] 0]
    # в зависимости от того что выделили будет запущена нужная процедура
    if {$key eq "" || $key eq "server"} {
        set host [ Add::server ]
        return
    }
    Add::$key .frm_tree.tree $host $values
}

Oto jedna z zalet tickle: możesz przekazać wartość zmiennej jako nazwę procedury:

Add::$key .frm_tree.tree $host $values

Czyli np. jeśli wskażemy na serwer główny i wciśniemy „+”, to uruchomi się procedura Add::server, jeśli na klastrze – Add::cluster itd. (o tym napiszę, gdzie niezbędne „klucze” pochodzą nieco poniżej), wymienione procedury rysują elementy graficzne odpowiednie do kontekstu.

Jak być może zauważyłeś, formularze są podobne stylistycznie - nie jest to zaskakujące, ponieważ wyświetla je jedna procedura, a dokładniej główna ramka formularza (okno, przyciski, obraz, etykieta), nazwa procedury Dodaj najwyższy poziom

proc AddToplevel {lbl img {win_name .add}} {
    set cmd "destroy $win_name"
    if [winfo exists $win_name] {destroy $win_name}
    toplevel $win_name
    wm title $win_name $lbl
    wm iconphoto $win_name tcl
    # метка с иконкой
    ttk::label $win_name.lbl -image $img
    # фрейм с полями ввода
    set frm [ttk::labelframe $win_name.frm -text $lbl -labelanchor nw]
    
    grid columnconfigure $frm 0 -weight 1
    grid rowconfigure $frm 0 -weight 1
    # фрейм и кнопки
    set frm_btn [frame $win_name.frm_btn -border 0]
    ttk::button $frm_btn.btn_ok -image ok_grey_24 -command { }
    ttk::button $frm_btn.btn_cancel -command $cmd -image quit_grey_24 
    grid $win_name.lbl -row 0 -column 0 -sticky nw -padx 5 -pady 10
    grid $frm -row 0 -column 1 -sticky nw -padx 5 -pady 5
    grid $frm_btn -row 1 -column 1 -sticky se -padx 5 -pady 5
    pack  $frm_btn.btn_cancel  -side right
    pack  $frm_btn.btn_ok  -side right -padx 10
    return $frm
}

Parametry wywołania: tytuł, nazwa obrazu ikony z biblioteki (lib/images.tcl) i opcjonalny parametr nazwy okna (domyślnie .add). Zatem jeśli weźmiemy powyższe przykłady dodania głównego serwera i klastra, wywołanie będzie odpowiednio:

AddToplevel "Добавление основного сервера" server_grey_64

lub

AddToplevel "Добавление кластера" cluster_grey_64

Cóż, kontynuując te przykłady, pokażę procedury wyświetlające okna dialogowe dodawania dla serwera lub klastra.

Dodaj serwer

proc Add::server {} {
    global default
    # выводим основную форму
    set frm [AddToplevel "Добавление основного сервера" server_grey_64]
    # добавляем етки и поля ввода на эту форму
    label $frm.lbl_host -text "Адрес сервера"
    entry  $frm.ent_host
    label $frm.lbl_port -text "Порт"
    entry $frm.ent_port 
    $frm.ent_port  insert end $default(port)
    grid $frm.lbl_host -row 0 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_host -row 0 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_port -row 1 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_port -row 1 -column 1 -sticky nsew -padx 5 -pady 5
    grid columnconfigure $frm 0 -weight 1
    grid rowconfigure $frm 0 -weight 1
    #set frm_btn [frame .add.frm_btn -border 0]
    # переопределяем обработчик нажатия кнопки
    .add.frm_btn.btn_ok configure -command {
        set host [SaveMainServer [.add.frm.ent_host get] [.add.frm.ent_port get]]
        .frm_tree.tree insert {} end -id "server::$host" -text "$host" -values "$host"
        destroy .add
        return $host
    }
    return $frm
}

Dodaj::klaster

proc Add::cluster {tree host values} {
    global default lifetime_limit expiration_timeout session_fault_tolerance_level
    global max_memory_size max_memory_time_limit errors_count_threshold security_level
    global load_balancing_mode kill_problem_processes 
    agent_user agent_pwd cluster_user cluster_pwd auth_agent
    if {$agent_user ne "" && $agent_pwd ne ""} {
        set auth_agent "--agent-user=$agent_user --agent-pwd=$agent_pwd"
    } else {
        set auth_agent ""
    }
    # устанавливаем глобальные переменные ()
    set lifetime_limit $default(lifetime_limit)
    set expiration_timeout $default(expiration_timeout)
    set session_fault_tolerance_level $default(session_fault_tolerance_level)
    set max_memory_size $default(max_memory_size)
    set max_memory_time_limit $default(max_memory_time_limit)
    set errors_count_threshold $default(errors_count_threshold)
    set security_level [lindex $default(security_level) 0]
    set load_balancing_mode [lindex $default(load_balancing_mode) 0]
    
    set frm [AddToplevel "Добавление кластера" cluster_grey_64]
    
    label $frm.lbl_host -text "Адрес основного сервера"
    entry  $frm.ent_host
    label $frm.lbl_port -text "Порт"
    entry $frm.ent_port 
    $frm.ent_port  insert end $default(port)
    label $frm.lbl_name -text "Название кластера"
    entry  $frm.ent_name
    label $frm.lbl_secure_connect -text "Защищённое соединение"
    ttk::combobox $frm.cb_security_level -textvariable security_level -values $default(security_level)
    label $frm.lbl_expiration_timeout -text "Останавливать выключенные процессы через:"
    entry  $frm.ent_expiration_timeout -textvariable expiration_timeout
    label $frm.lbl_session_fault_tolerance_level -text "Уровень отказоустойчивости"
    entry  $frm.ent_session_fault_tolerance_level -textvariable session_fault_tolerance_level
    label $frm.lbl_load_balancing_mode -text "Режим распределения нагрузки"
    ttk::combobox $frm.cb_load_balancing_mode -textvariable load_balancing_mode 
    -values $default(load_balancing_mode)
    label $frm.lbl_errors_count_threshold -text "Допустимое отклонение количества ошибок сервера, %"
    entry  $frm.ent_errors_count_threshold -textvariable errors_count_threshold
    label $frm.lbl_processes -text "Рабочие процессы:"
    label $frm.lbl_lifetime_limit -text "Период перезапуска, сек."
    entry  $frm.ent_lifetime_limit -textvariable lifetime_limit
    label $frm.lbl_max_memory_size -text "Допустимый объём памяти, КБ"
    entry  $frm.ent_max_memory_size -textvariable max_memory_size
    label $frm.lbl_max_memory_time_limit -text "Интервал превышения допустимого объёма памяти, сек."
    entry  $frm.ent_max_memory_time_limit -textvariable max_memory_time_limit
    label $frm.lbl_kill_problem_processes -justify left -anchor nw -text "Принудительно завершать проблемные процессы"
    checkbutton $frm.check_kill_problem_processes -variable kill_problem_processes -onvalue yes -offvalue no    
    
    grid $frm.lbl_host -row 0 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_host -row 0 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_port -row 1 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_port -row 1 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_name -row 2 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_name -row 2 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_secure_connect -row 3 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.cb_security_level -row 3 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_expiration_timeout -row 4 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_expiration_timeout -row 4 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_session_fault_tolerance_level -row 5 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_session_fault_tolerance_level -row 5 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_load_balancing_mode -row 6 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.cb_load_balancing_mode -row 6 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_errors_count_threshold -row 7 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_errors_count_threshold -row 7 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_processes -row 8 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.lbl_lifetime_limit -row 9 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_lifetime_limit -row 9 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_max_memory_size -row 10 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_max_memory_size -row 10 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_max_memory_time_limit -row 11 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_max_memory_time_limit -row 11 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_kill_problem_processes -row 12 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.check_kill_problem_processes -row 12 -column 1 -sticky nw -padx 5 -pady 5
    # переопределяем обработчик
    .add.frm_btn.btn_ok configure -command {
        RunCommand "" "cluster insert 
        --host=[.add.frm.ent_host get] 
        --port=[.add.frm.ent_port get] 
        --name=[.add.frm.ent_name get] 
        --expiration-timeout=$expiration_timeout 
        --lifetime-limit=$lifetime_limit 
        --max-memory-size=$max_memory_size 
        --max-memory-time-limit=$max_memory_time_limit 
        --security-level=$security_level 
        --session-fault-tolerance-level=$session_fault_tolerance_level 
        --load-balancing-mode=$load_balancing_mode 
        --errors-count-threshold=$errors_count_threshold 
        --kill-problem-processes=$kill_problem_processes 
        $auth_agent $host"
        Run::server $tree $host ""
        destroy .add
    }
    return $frm
}

Porównując kod tych procedur różnicę widać gołym okiem, ja skupię się na obsłudze przycisku „Ok”. W Tk właściwości elementów graficznych można zmieniać w trakcie wykonywania programu za pomocą opcji skonfigurować. Na przykład początkowe polecenie wyświetlania przycisku:

ttk::button $frm_btn.btn_ok -image ok_grey_24 -command { }

Ale w naszych formularzach polecenie zależy od wymaganej funkcjonalności:

  .add.frm_btn.btn_ok configure -command {
        RunCommand "" "cluster insert 
        --host=[.add.frm.ent_host get] 
        --port=[.add.frm.ent_port get] 
        --name=[.add.frm.ent_name get] 
        --expiration-timeout=$expiration_timeout 
        --lifetime-limit=$lifetime_limit 
        --max-memory-size=$max_memory_size 
        --max-memory-time-limit=$max_memory_time_limit 
        --security-level=$security_level 
        --session-fault-tolerance-level=$session_fault_tolerance_level 
        --load-balancing-mode=$load_balancing_mode 
        --errors-count-threshold=$errors_count_threshold 
        --kill-problem-processes=$kill_problem_processes 
        $auth_agent $host"
        Run::server $tree $host ""
        destroy .add
    }

W powyższym przykładzie przycisk „zatkany” uruchamia procedurę dodawania klastra.

W tym miejscu warto zrobić dygresję w kierunku pracy z elementami graficznymi w Tk - dla różnych elementów wprowadzania danych (wpis, combobox, checkbutton itp.) wprowadzono parametr jako zmienną tekstową:

entry  $frm.ent_lifetime_limit -textvariable lifetime_limit

Zmienna ta jest zdefiniowana w globalnej przestrzeni nazw i zawiera aktualnie wprowadzoną wartość. Te. aby otrzymać wpisany tekst z pola wystarczy odczytać wartość odpowiadającą zmiennej (oczywiście pod warunkiem, że zostanie ona zdefiniowana podczas tworzenia elementu).

Drugą metodą pobrania wprowadzonego tekstu (dla elementów typu wpis) jest użycie komendy get:

.add.frm.ent_name get

Obie te metody można zobaczyć w powyższym kodzie.

Kliknięcie tego przycisku w tym przypadku uruchamia procedurę RunCommand z wygenerowaną linią poleceń służącą do dodania klastra w ujęciu rac:

/opt/1C/v8.3/x86_64/rac cluster insert  --host=localhost  --port=1540  --name=dsdsds  --expiration-timeout=0  --lifetime-limit=0  --max-memory-size=0  --max-memory-time-limit=0  --security-level=0  --session-fault-tolerance-level=0  --load-balancing-mode=performance  --errors-count-threshold=0  --kill-problem-processes=no   localhost:1545

Teraz dochodzimy do głównego polecenia, które steruje uruchomieniem rac z potrzebnymi parametrami, a także analizuje dane wyjściowe poleceń na listy i zwraca, jeśli to konieczne:

Uruchom polecenie

proc RunCommand {root par} {
    global dir rac_cmd cluster work_list_row_count agent_user agent_pwd cluster_user cluster_pwd
    puts "$rac_cmd $par"
    set work_list_row_count 0
    # открываем канал в неблокирующем режиме
    # $rac - команда с полным путём
    # $par - сформированные ключи запуска и опции    
    set pipe [open "|$rac_cmd $par" "r"]
    try {
        set lst ""
        set l ""
        # вывод команды добавляем в список списков
        while {[gets $pipe line]>=0} {
            #puts $line
            if {$line eq ""} {
                lappend l $lst
                set lst ""
            } else {
                lappend lst [string trim $line]
            }
        }
        close $pipe
        return $l
    } on error {result options} {
        # Запуск обработчика ошибок
        ErrorParcing $result $options
        return ""
    }
}

Po wprowadzeniu danych głównego serwera zostaną one dodane do drzewa, za to w powyższej procedurze Add:server odpowiedzialny jest następujący kod:

.frm_tree.tree insert {} end -id "server::$host" -text "$host" -values "$host"

Teraz klikając na nazwę serwera w drzewie otrzymamy listę klastrów zarządzanych przez ten serwer, a klikając na klaster otrzymamy listę elementów klastra (serwery, bazy danych itp.). Jest to zaimplementowane w procedurze TreePress (plik lib/function.tcl):

proc TreePress {tree} {
   global host server active_cluster infobase
   # определяем выделенный элемент
    set id  [$tree selection]
   # устанавливаем нужные глобальные переменные
    SetGlobalVarFromTreeItems $tree $id
   # Определяем ключ и значение, т.е. именно тип выбранного элемента
    set values [$tree item $id -values]
    set key [lindex [split $id "::"] 0]
   # и в зависимости от того что выбрали будет запущена соответствующая процедура 
   # в пространстве имён Run
    Run::$key $tree $host $values
}

Odpowiednio Run::server zostanie uruchomiony dla głównego serwera (dla klastra - Run::cluster, dla działającego serwera - Run::work_server itp.). Te. wartość zmiennej $key jest częścią nazwy elementu drzewa określonej przez opcję -ID.

Zwróćmy uwagę na procedurę

Uruchom::serwer

proc Run::server {tree host values} {
    # получаем список кластеров требуемого сервера
    set lst [RunCommand server::$host "cluster list $host"]
    if {$lst eq ""} {return}
    set l [lindex $lst 0]
    #puts $lst
    # удаляем лишнее из списка
    .frm_work.tree_work delete  [ .frm_work.tree_work children {}]
    # читаем список
    foreach cluster_list $lst {
        # Заполняем список полученными значениями
        InsertItemsWorkList $cluster_list
        # обрабатываем вывод (список) для добавления данных в дерево
        foreach i $cluster_list {
            #puts $i
            set cluster_list [split $i ":"]
            if  {[string trim [lindex $cluster_list 0]] eq "cluster"} {
                set cluster_id [string trim [lindex $cluster_list 1]]
                lappend cluster($cluster_id) $cluster_id
            }
            if  {[string trim [lindex $cluster_list 0]] eq "name"} {
                lappend  cluster($cluster_id) [string trim [lindex $cluster_list 1]]
            }
        }
    }
    # добавляем кластеры в дерево
    foreach x [array names cluster] {
        set id [lindex $cluster($x) 0]
        if { [$tree exists "cluster::$id"] == 0 } {
            $tree insert "server::$host" end -id "cluster::$id" -text "[lindex $cluster($x) 1]" -values "$id"
            # добавляем элементы в кластер
            InsertClusterItems $tree $id
        }
    }
    if { [$tree exists "agent_admins::$id"] == 0 } {
        $tree insert "server::$host" end -id "agent_admins::$id" -text "Администраторы" -values "$id"
        #InsertClusterItems $tree $id
    }
}

Ta procedura przetwarza dane otrzymane z serwera za pomocą polecenia RunCommand i dodaje do drzewa różne rzeczy - klastry, różne elementy główne (bazy, działające serwery, sesje itd.). Jeśli przyjrzysz się uważnie, zauważysz w środku wywołanie procedury InsertItemsWorkList. Służy do dodawania elementów do listy graficznej poprzez przetwarzanie danych wyjściowych narzędzia konsoli rac, które wcześniej zostały zwrócone jako lista do zmiennej $lst. To jest lista list zawierających pary elementów oddzielonych dwukropkiem.

Na przykład lista połączeń klastra:

svk@svk ~]$ /opt/1C/v8.3/x86_64/rac connection list --cluster=783d2170-56c3-11e8-c586-fc75165efbb2 localhost:1545
connection     : dcf5991c-7d24-11e8-1690-fc75165efbb2
conn-id        : 0
host           : svk.home
process        : 79de2e16-56c3-11e8-c586-fc75165efbb2
infobase       : 00000000-0000-0000-0000-000000000000
application    : "JobScheduler"
connected-at   : 2018-07-01T14:49:51
session-number : 0
blocked-by-ls  : 0

connection     : b993293a-7d24-11e8-1690-fc75165efbb2
conn-id        : 0
host           : svk.home
process        : 79de2e16-56c3-11e8-c586-fc75165efbb2
infobase       : 00000000-0000-0000-0000-000000000000
application    : "JobScheduler"
connected-at   : 2018-07-01T14:48:52
session-number : 0
blocked-by-ls  : 0

W formie graficznej będzie to wyglądać mniej więcej tak:

Pisanie GUI dla 1C RAC, czyli znowu o Tcl/Tk

Powyższa procedura wybiera nazwy elementów nagłówka oraz dane do wypełnienia tabeli:

WstawItemsWorkList

proc InsertItemsWorkList {lst} {
    global work_list_row_count
    # установка чередования цвета для строки
    if [expr $work_list_row_count % 2] {
        set tag dark
    } else {
        set tag light
    }
    # разбор строк на пары ключ - значение
    foreach i $lst {
        if [regexp -nocase -all -- {(D+)(s*?|)(:)(s*?|)(.*)} $i match param v2 v3 v4 value] {
            lappend column_list [string trim $param]
            lappend value_list [string trim $value]
        }
    }
     # заполнение таблицы
    .frm_work.tree_work configure -columns $column_list -displaycolumns $column_list
    .frm_work.tree_work insert {} end  -values $value_list -tags $tag
    .frm_work.tree_work column #0 -stretch
    # установка заголовков
    foreach j $column_list {
        .frm_work.tree_work heading $j -text $j
    }
    incr work_list_row_count
}

Tutaj zamiast prostego polecenia [split $str ":"], które dzieli ciąg znaków na elementy oddzielone znakiem ":" i zwraca listę, używane jest wyrażenie regularne, ponieważ niektóre elementy zawierają także dwukropek.

Procedura InsertClusterItems (jedna z kilku podobnych) po prostu dodaje listę elementów podrzędnych z odpowiadającymi im identyfikatorami do drzewa wymaganego elementu klastra
Wstaw elementy klastra

proc InsertClusterItems {tree id} {
    set parent "cluster::$id"
    $tree insert $parent end -id "infobases::$id" -text "Информационные базы" -values "$id"
    $tree insert $parent end -id "servers::$id" -text "Рабочие серверы" -values "$id"
    $tree insert $parent end -id "admins::$id" -text "Администраторы" -values "$id"
    $tree insert $parent end -id "managers::$id" -text "Менеджеры кластера" -values $id
    $tree insert $parent end -id "processes::$id" -text "Рабочие процессы" -values "workprocess-all"
    $tree insert $parent end -id "sessions::$id" -text "Сеансы" -values "sessions-all"
    $tree insert $parent end -id "locks::$id" -text "Блокировки" -values "blocks-all"
    $tree insert $parent end -id "connections::$id" -text "Соединения" -values "connections-all"
    $tree insert $parent end -id "profiles::$id" -text "Профили безопасности" -values $id
}

Możesz rozważyć jeszcze dwie opcje wdrożenia podobnej procedury, gdzie będzie wyraźnie widoczne, jak zoptymalizować i pozbyć się powtarzających się poleceń:

W tej procedurze dodawanie i sprawdzanie rozwiązuje się bezpośrednio:

Wstaw elementy bazowe

proc InsertBaseItems {tree id} {
    set parent "infobase::$id"
    if { [$tree exists "sessions::$id"] == 0 } {
        $tree insert $parent end -id "sessions::$id" -text "Сеансы" -values "$id"
    }
    if { [$tree exists "locks::$id"] == 0 } {
        $tree insert $parent end -id "locks::$id" -text "Блокировки" -values "$id"
    }
    if { [$tree exists "connections::$id"] == 0 } {
        $tree insert $parent end -id "connections::$id" -text "Соединения" -values "$id"
    }
}

Oto bardziej poprawne podejście:

Wstaw elementy profilu

proc InsertProfileItems {tree id} {
    set parent "profile::$id"
    set lst {
        {dir "Виртуальные каталоги"}
        {com "Разрешённые COM-классы"}
        {addin "Внешние компоненты"}
        {module "Внешние отчёты и обработки"}
        {app "Разрешённые приложения"}
        {inet "Ресурсы интернет"}
    }
    foreach i $lst {
        append item [lindex $i 0] "::$id"
        if { [$tree exists $item] == 0 } {
            $tree insert $parent end -id $item -text [lindex $i 1] -values "$id"
        }
        unset item 
    }
}

Różnica między nimi polega na zastosowaniu pętli, w której wykonywane są powtarzane polecenia. Wybór podejścia zależy od programisty.

Omówiliśmy już dodawanie elementów i pobieranie danych, teraz czas skupić się na edycji. Ponieważ do edycji i dodawania używane są w zasadzie te same parametry (z wyjątkiem bazy informacji), stosowane są te same formy dialogowe. Algorytm wywoływania procedur dodawania wygląda następująco:

Dodaj::$key->AddToplevel

A do edycji w ten sposób:

Edytuj::$key->Add::$key->AddTopLevel

Weźmy na przykład edycję klastra, tj. Po kliknięciu na nazwę skupienia w drzewie należy wcisnąć przycisk edycji na pasku narzędzi (ołówek), a na ekranie wyświetli się odpowiedni formularz:

Pisanie GUI dla 1C RAC, czyli znowu o Tcl/Tk
Edycja::klaster

proc Edit::cluster {tree host values} {
    global default lifetime_limit expiration_timeout session_fault_tolerance_level
    global max_memory_size max_memory_time_limit errors_count_threshold security_level
    global load_balancing_mode kill_problem_processes active_cluster 
    agent_user agent_pwd cluster_user cluster_pwd auth
    if {$cluster_user ne "" && $cluster_pwd ne ""} {
        set auth "--cluster-user=$cluster_user --cluster-pwd=$cluster_pwd"
    } else {
        set auth ""
    }
    # рисуем форму для кластера
    set frm [Add::cluster $tree $host $values]
    # меняем текст на метке
    $frm configure -text "Редактирование кластера"
    
    set active_cluster $values
    # получаем данные по выделенному кластеру
    set lst [RunCommand cluster::$values "cluster info --cluster=$active_cluster $host"]
    # заполняем поля
    FormFieldsDataInsert $frm $lst
    # выключаем поля, редактирование которых запрещено
    $frm.ent_host configure -state disable
    $frm.ent_port configure -state disable
    # переназначаем обработчик
    .add.frm_btn.btn_ok configure -command {
        RunCommand "" "cluster update 
        --cluster=$active_cluster $auth 
        --name=[.add.frm.ent_name get] 
        --expiration-timeout=$expiration_timeout 
        --lifetime-limit=$lifetime_limit 
        --max-memory-size=$max_memory_size 
        --max-memory-time-limit=$max_memory_time_limit 
        --security-level=$security_level 
        --session-fault-tolerance-level=$session_fault_tolerance_level 
        --load-balancing-mode=$load_balancing_mode 
        --errors-count-threshold=$errors_count_threshold 
        --kill-problem-processes=$kill_problem_processes 
        $auth $host"
        $tree delete "cluster::$active_cluster"
        Run::server $tree $host ""
        destroy .add
    }
}

Sądząc po komentarzach w kodzie, w zasadzie wszystko jest jasne, z wyjątkiem tego, że kod obsługi przycisku został nadpisany i istnieje procedura FormFieldsDataInsert, która wypełnia pola danymi i inicjuje zmienne:

FormFieldsWstawDane

proc FormFieldsDataInsert {frm lst} {
    foreach i [lindex $lst 0] {
        # получаем список параметров и значений
        if [regexp -nocase -all -- {(D+)(s*?|)(:)(s*?|)(.*)} $i match param v2 v3 v4 value] {
            # меняем символы
            regsub -all -- "-" [string trim $param] "_" entry_name
            # заполняем данными
            if [winfo exists $frm.ent_$entry_name] {
                $frm.ent_$entry_name delete 0 end
                $frm.ent_$entry_name insert end [string trim $value """]
            }
            if [winfo exists $frm.cb_$entry_name] {
                global $entry_name
                set $entry_name [string trim $value """]
            }
            # для чекбоксов меняем значения
            if [winfo exists $frm.check_$entry_name] {
                global $entry_name
                if {$value eq "0"} {
                    set $entry_name no
                } elseif {$value eq "1"} {
                    set $entry_name yes
                } else {
                    set $entry_name $value
                }
            }
        }
    }
}

W tej procedurze ujawniła się kolejna zaleta tcl - wartości innych zmiennych są zastępowane nazwami zmiennych. Te. aby zautomatyzować wypełnianie formularzy i inicjalizację zmiennych, nazwy pól i zmiennych odpowiadają przełącznikom wiersza poleceń narzędzia rac, a nazwom parametrów wyjściowych poleceń z pewnym wyjątkiem - myślnik zastępuje się znakiem podkreślenia. Np odmowa zaplanowanych zadań pasuje do pola ent_scheduled_jobs_deny i zmienna zaplanowane_zadania_odmowy.

Formularze dodawania i edycji mogą różnić się składem pól, np. pracą z bazą informacji:

Dodanie bezpieczeństwa informacji

Pisanie GUI dla 1C RAC, czyli znowu o Tcl/Tk

Edycja bezpieczeństwa informacji

Pisanie GUI dla 1C RAC, czyli znowu o Tcl/Tk

W procedurze edycyjnej Edit::infobase do formularza dodawane są wymagane pola, kod jest obszerny, dlatego nie prezentuję go tutaj.

Analogicznie procedury dodawania, edycji, usuwania są realizowane dla pozostałych elementów.

Ponieważ działanie narzędzia implikuje nieograniczoną liczbę serwerów, klastrów, baz informacji itp., aby określić, który klaster należy do którego serwera lub systemu bezpieczeństwa informacji, wprowadzono kilka zmiennych globalnych, których wartości są ustawiane każdy po kliknięciu elementów drzewa. Te. procedura rekurencyjnie przechodzi przez wszystkie elementy nadrzędne i ustawia zmienne:

UstawGlobalVarFromTreeItems

proc SetGlobalVarFromTreeItems {tree id} {
    global host server active_cluster infobase
    set parent [$tree parent $id]
    set values [$tree item $id -values]
    set key [lindex [split $id "::"] 0]
    switch -- $key {
        server {set host $values}
        work_server {set server $values}
        cluster {set active_cluster $values}
        infobase {set infobase $values}
    }
    if {$parent eq ""} {
        return
    } else {
        SetGlobalVarFromTreeItems $tree $parent
    }
}

Klaster 1C umożliwia pracę z autoryzacją lub bez. Istnieją dwa typy administratorów — administrator agenta klastra i administrator klastra. W związku z tym do poprawnego działania wprowadzono jeszcze 4 zmienne globalne zawierające login i hasło administratora. Te. jeżeli w klastrze znajduje się konto administratora, wyświetli się okno dialogowe umożliwiające wprowadzenie loginu i hasła, dane zostaną zapisane w pamięci i wstawione do każdego polecenia dla odpowiedniego klastra.

Za to odpowiedzialna jest procedura obsługi błędów.

Błąd parowania

proc ErrorParcing {err opt} {
    global cluster_user cluster_pwd agent_user agent_pwd
        switch -regexp -- $err {
        "Cluster administrator is not authenticated" {
            AuthorisationDialog "Администратор кластера"
            .auth_win.frm_btn.btn_ok configure -command {
                set cluster_user [.auth_win.frm.ent_name get]
                set cluster_pwd [.auth_win.frm.ent_pwd get]
                destroy .auth_win
            }
            #RunCommand $root $par
        }
        "Central server administrator is not authenticated" {
            AuthorisationDialog "Администратор агента кластера"
            .auth_win.frm_btn.btn_ok configure -command {
                set agent_user [.auth_win.frm.ent_name get]
                set agent_pwd [.auth_win.frm.ent_pwd get]
                destroy .auth_win
            }
        }
        "Администратор кластера не аутентифицирован" {
            AuthorisationDialog "Администратор кластера"
            .auth_win.frm_btn.btn_ok configure -command {
                set cluster_user [.auth_win.frm.ent_name get]
                set cluster_pwd [.auth_win.frm.ent_pwd get]
                destroy .auth_win
            }
            #RunCommand $root $par
        }
        "Администратор центрального сервера не аутентифицирован" {
            AuthorisationDialog "Администратор агента кластера"
            .auth_win.frm_btn.btn_ok configure -command {
                set agent_user [.auth_win.frm.ent_name get]
                set agent_pwd [.auth_win.frm.ent_pwd get]
                destroy .auth_win
            }
        }
        (.+) {
            tk_messageBox -type ok -icon error -message "$err"
        }
    }
}

Te. w zależności od tego, co zwróci polecenie, reakcja będzie odpowiednia.

W tej chwili zaimplementowano około 95 procent funkcjonalności, pozostaje tylko zaimplementować pracę z profilami bezpieczeństwa i przetestować =). To wszystko. Przepraszam za pokręconą historię.

Kod jest tradycyjnie dostępny tutaj.

Aktualizacja: Skończyłem pracę z profilami zabezpieczeń. Teraz funkcjonalność jest wdrożona w 100%.

Aktualizacja 2: dodano lokalizację na język angielski i rosyjski, przetestowano działanie w win7
Pisanie GUI dla 1C RAC, czyli znowu o Tcl/Tk

Źródło: www.habr.com

Dodaj komentarz