Schreiben einer GUI für 1C RAC oder noch einmal über Tcl/Tk

Als wir uns mit dem Thema der Funktionsweise von 1C-Produkten in der Linux-Umgebung befassten, wurde ein Nachteil entdeckt – das Fehlen eines praktischen grafischen Multiplattform-Tools zum Verwalten eines Clusters von 1C-Servern. Und es wurde beschlossen, diesen Nachteil zu beheben, indem eine GUI für das Rac-Konsolendienstprogramm geschrieben wurde. Als Entwicklungssprache wurde Tcl/tk gewählt, da es meiner Meinung nach für diese Aufgabe am besten geeignet ist. Deshalb möchte ich in diesem Material einige interessante Aspekte der Lösung vorstellen.

Zum Arbeiten benötigen Sie die Distributionen tcl/tk und 1C. Und da ich mich entschieden habe, die Möglichkeiten der Basislieferung tcl/tk optimal zu nutzen, ohne Pakete von Drittanbietern zu verwenden, benötige ich Version 8.6.7, die ttk enthält – ein Paket mit zusätzlichen grafischen Elementen, von denen wir hauptsächlich ttk benötigen ::TreeView ermöglicht die Anzeige von Daten sowohl in Form einer Baumstruktur als auch in Form einer Tabelle (Liste). Außerdem wurde in der neuen Version die Arbeit mit Ausnahmen überarbeitet (der try-Befehl, der im Projekt beim Ausführen externer Befehle verwendet wird).

Das Projekt besteht aus mehreren Dateien (obwohl nichts Sie daran hindert, alles in einer zu erledigen):

rac_gui.cfg – Standardkonfiguration
rac_gui.tcl – Hauptstartskript
Das lib-Verzeichnis enthält Dateien, die beim Start automatisch geladen werden:
function.tcl – Datei mit Prozeduren
gui.tcl – grafische Hauptoberfläche
images.tcl – Base64-Bildbibliothek

Die Datei rac_gui.tcl startet tatsächlich den Interpreter, initialisiert Variablen, lädt Module, Konfigurationen usw. Inhalt der Datei mit Kommentaren:

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"
    }    
}

Nachdem Sie alles Erforderliche heruntergeladen und überprüft haben, ob das Rac-Dienstprogramm vorhanden ist, wird ein grafisches Fenster geöffnet. Die Programmoberfläche besteht aus drei Elementen:

Symbolleiste, Baum und Liste

Ich habe den Inhalt des „Baums“ so ähnlich wie möglich an die Standard-Windows-Ausstattung von 1C angepasst.

Schreiben einer GUI für 1C RAC oder noch einmal über Tcl/Tk

Der Hauptcode, der dieses Fenster bildet, ist in der Datei enthalten
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

Der Algorithmus zum Arbeiten mit dem Programm ist wie folgt:

1. Zuerst müssen Sie den Hauptclusterserver hinzufügen (d. h. den Clusterverwaltungsserver (unter Linux wird die Verwaltung mit dem Befehl „/opt/1C/v8.3/x86_64/ras Cluster – Daemon“ gestartet)).

Klicken Sie dazu auf die Schaltfläche „+“ und geben Sie im sich öffnenden Fenster die Serveradresse und den Port ein:

Schreiben einer GUI für 1C RAC oder noch einmal über Tcl/Tk

Anschließend erscheint durch Anklicken unser Server im Baum, es öffnet sich eine Liste der Cluster oder es wird ein Verbindungsfehler angezeigt.

2. Durch Klicken auf den Clusternamen wird eine Liste der dafür verfügbaren Funktionen geöffnet.

3. ...

Und so weiter, d.h. Um einen neuen Cluster hinzuzufügen, wählen Sie einen beliebigen in der Liste verfügbaren aus und klicken Sie auf die Schaltfläche „+“ in der Symbolleiste. Das Dialogfeld „Neuen hinzufügen“ wird angezeigt:

Schreiben einer GUI für 1C RAC oder noch einmal über Tcl/Tk

Die Schaltflächen in der Symbolleiste führen kontextabhängig Funktionen aus, d. h. Abhängig davon, welches Element des Baums oder der Liste ausgewählt wird, wird der eine oder andere Vorgang ausgeführt.

Schauen wir uns das Beispiel der Schaltfläche „Hinzufügen“ („+“) an:

Code zur Schaltflächengenerierung:

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

Hier sehen wir, dass beim Drücken der Schaltfläche die Prozedur „Hinzufügen“ ausgeführt wird, ihr Code:

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
}

Einer der Vorteile von tickle: Sie können den Wert einer Variablen als Prozedurnamen übergeben:

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

Das heißt, wenn wir zum Beispiel auf den Hauptserver zeigen und „+“ drücken, wird die Add::server-Prozedur gestartet, wenn am Cluster – Add::cluster und so weiter (ich werde darüber schreiben, wo die Die notwendigen „Schlüssel“ finden Sie weiter unten), die aufgeführten Verfahren zeichnen grafische Elemente passend zum Kontext.

Wie Sie vielleicht bereits bemerkt haben, ähneln sich die Formulare im Stil – das ist nicht verwunderlich, da sie durch eine Prozedur angezeigt werden, genauer gesagt durch den Hauptrahmen des Formulars (Fenster, Schaltflächen, Bild, Beschriftung), den Namen der Prozedur AddTopLevel

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
}

Aufrufparameter: Titel, Bildname für das Symbol aus der Bibliothek (lib/images.tcl) und ein optionaler Fensternamenparameter (Standard .add). Wenn wir also die obigen Beispiele für das Hinzufügen des Hauptservers und Clusters nehmen, wird der Aufruf entsprechend lauten:

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

oder

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

Nun, um mit diesen Beispielen fortzufahren, werde ich die Verfahren zeigen, die Dialoge zum Hinzufügen für einen Server oder Cluster anzeigen.

Server hinzufügen

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
}

Hinzufügen::cluster

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
}

Wenn man den Code dieser Prozeduren vergleicht, ist der Unterschied mit bloßem Auge sichtbar; ich werde mich auf den „Ok“-Button-Handler konzentrieren. In Tk können die Eigenschaften grafischer Elemente während der Programmausführung mit der Option überschrieben werden konfigurieren. Zum Beispiel der erste Befehl zum Anzeigen der Schaltfläche:

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

In unseren Formularen hängt der Befehl jedoch von der erforderlichen Funktionalität ab:

  .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
    }

Im obigen Beispiel startet die Schaltfläche „verstopft“ den Vorgang zum Hinzufügen eines Clusters.

Hier lohnt es sich, einen Exkurs in die Arbeit mit grafischen Elementen in Tk zu machen – für verschiedene Dateneingabeelemente (Eingabe, Combobox, Checkbutton etc.) wurde ein Parameter als Textvariable eingeführt:

entry  $frm.ent_lifetime_limit -textvariable lifetime_limit

Diese Variable ist im globalen Namensraum definiert und enthält den aktuell eingegebenen Wert. Diese. Um den eingegebenen Text aus dem Feld zu erhalten, müssen Sie lediglich den Wert lesen, der der Variablen entspricht (natürlich vorausgesetzt, dass er beim Erstellen des Elements definiert wurde).

Die zweite Methode zum Abrufen des eingegebenen Textes (für Elemente vom Typ „Eintrag“) ist die Verwendung des Befehls „get“:

.add.frm.ent_name get

Beide Methoden sind im obigen Code zu sehen.

Durch Klicken auf diese Schaltfläche wird in diesem Fall die RunCommand-Prozedur mit der generierten Befehlszeile zum Hinzufügen eines Clusters in Bezug auf rac gestartet:

/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

Nun kommen wir zum Hauptbefehl, der den Start von rac mit den von uns benötigten Parametern steuert, außerdem die Ausgabe von Befehlen in Listen analysiert und bei Bedarf zurückgibt:

Führen Sie den Befehl aus

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 ""
    }
}

Nach Eingabe der Hauptserverdaten werden diese dem Baum hinzugefügt. Dafür ist in der obigen Add:Server-Prozedur der folgende Code verantwortlich:

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

Wenn wir nun auf den Servernamen in der Baumstruktur klicken, erhalten wir eine Liste der von diesem Server verwalteten Cluster. Wenn wir auf einen Cluster klicken, erhalten wir eine Liste der Clusterelemente (Server, Infobases usw.). Dies ist in der TreePress-Prozedur (Datei lib/function.tcl) implementiert:

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
}

Dementsprechend wird Run::server für den Hauptserver gestartet (für einen Cluster – Run::cluster, für einen funktionierenden Server – Run::work_server usw.). Diese. Der Wert der Variablen $key ist Teil des Namens des durch die Option angegebenen Baumelements -id.

Achten wir auf das Verfahren

Führen Sie::server aus

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
    }
}

Dieses Verfahren verarbeitet, was vom Server über den RunCommand-Befehl empfangen wurde, und fügt dem Baum alle möglichen Dinge hinzu – Cluster, verschiedene Stammelemente (Basen, funktionierende Server, Sitzungen usw.). Wenn Sie genau hinsehen, werden Sie einen Aufruf der darin enthaltenen InsertItemsWorkList-Prozedur bemerken. Es wird verwendet, um Elemente zu einer grafischen Liste hinzuzufügen, indem die Ausgabe des Rac-Konsolendienstprogramms verarbeitet wird, die zuvor als Liste an die Variable $lst zurückgegeben wurde. Dies ist eine Liste von Listen, die durch einen Doppelpunkt getrennte Elementpaare enthalten.

Zum Beispiel eine Liste von Clusterverbindungen:

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

In grafischer Form sieht es etwa so aus:

Schreiben einer GUI für 1C RAC oder noch einmal über Tcl/Tk

Das obige Verfahren wählt die Namen der Elemente für den Header und die Daten zum Ausfüllen der Tabelle aus:

InsertItemsWorkList

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
}

Hier wird anstelle eines einfachen Befehls [split $str ::], der den String in durch :: getrennte Elemente aufteilt und eine Liste zurückgibt, ein regulärer Ausdruck verwendet, da einige Elemente auch einen Doppelpunkt enthalten.

Die InsertClusterItems-Prozedur (eine von mehreren ähnlichen) fügt einfach eine Liste untergeordneter Elemente mit entsprechenden Bezeichnern zum Baum des erforderlichen Clusterelements hinzu
ClusterItems einfügen

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
}

Sie können zwei weitere Optionen zur Implementierung eines ähnlichen Verfahrens in Betracht ziehen, bei denen deutlich sichtbar ist, wie Sie sich wiederholende Befehle optimieren und entfernen können:

In diesem Verfahren werden das Hinzufügen und Überprüfen frontal gelöst:

InsertBaseItems

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"
    }
}

Hier ist ein korrekterer Ansatz:

InsertProfileItems

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 
    }
}

Der Unterschied zwischen ihnen besteht in der Verwendung einer Schleife, in der die wiederholten Befehle ausgeführt werden. Welcher Ansatz verwendet wird, liegt im Ermessen des Entwicklers.

Wir haben das Hinzufügen von Elementen und das Abrufen von Daten behandelt, jetzt ist es an der Zeit, sich auf die Bearbeitung zu konzentrieren. Da beim Bearbeiten und Hinzufügen grundsätzlich dieselben Parameter verwendet werden (mit Ausnahme der Informationsbasis), werden dieselben Dialogformen verwendet. Der Algorithmus zum Aufrufen von Prozeduren zum Hinzufügen sieht folgendermaßen aus:

Add::$key->AddToplevel

Und zum Bearbeiten so:

Bearbeiten::$key->Add::$key->AddTopLevel

Nehmen wir zum Beispiel die Bearbeitung eines Clusters, d. h. Nachdem Sie auf den Namen des Clusters in der Baumstruktur geklickt haben, klicken Sie auf die Schaltfläche „Bearbeiten“ in der Symbolleiste (Bleistift) und das entsprechende Formular wird auf dem Bildschirm angezeigt:

Schreiben einer GUI für 1C RAC oder noch einmal über Tcl/Tk
Bearbeiten::cluster

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
    }
}

Anhand der Kommentare im Code ist im Prinzip alles klar, außer dass der Button-Handler-Code überschrieben wird und es eine FormFieldsDataInsert-Prozedur gibt, die die Felder mit Daten füllt und die Variablen initialisiert:

FormFieldsDataInsert

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
                }
            }
        }
    }
}

Bei diesem Verfahren kam ein weiterer Vorteil von tcl zum Vorschein: Die Werte anderer Variablen werden durch Variablennamen ersetzt. Diese. Um das Ausfüllen von Formularen und die Initialisierung von Variablen zu automatisieren, entsprechen die Namen von Feldern und Variablen mit einigen Ausnahmen den Befehlszeilenschaltern des Rac-Dienstprogramms und den Namen von Befehlsausgabeparametern – der Bindestrich wird durch einen Unterstrich ersetzt. Z.B geplante-Jobs-verweigern passt zum Feld ent_scheduled_jobs_deny und variabel geplante_jobs_deny.

Formulare zum Hinzufügen und Bearbeiten können sich in der Zusammensetzung der Felder unterscheiden, beispielsweise beim Arbeiten mit einer Informationsdatenbank:

Informationssicherheit hinzufügen

Schreiben einer GUI für 1C RAC oder noch einmal über Tcl/Tk

Informationssicherheit bearbeiten

Schreiben einer GUI für 1C RAC oder noch einmal über Tcl/Tk

Im Bearbeitungsvorgang Edit::infobase werden dem Formular die erforderlichen Felder hinzugefügt; der Code ist umfangreich, daher präsentiere ich ihn hier nicht.

Analog dazu sind Verfahren zum Hinzufügen, Bearbeiten und Löschen für andere Elemente implementiert.

Da der Betrieb des Dienstprogramms eine unbegrenzte Anzahl von Servern, Clustern, Informationsdatenbanken usw. erfordert, wurden zur Bestimmung, welcher Cluster zu welchem ​​Server oder Informationssicherheitssystem gehört, mehrere globale Variablen eingeführt, deren Werte jeweils festgelegt werden Mal, wenn Sie auf die Elemente des Baums klicken. Diese. Die Prozedur durchläuft rekursiv alle übergeordneten Elemente und setzt die Variablen:

SetGlobalVarFromTreeItems

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
    }
}

Mit dem 1C-Cluster können Sie mit oder ohne Autorisierung arbeiten. Es gibt zwei Arten von Administratoren: Cluster-Agent-Administrator und Cluster-Administrator. Dementsprechend wurden für den korrekten Betrieb vier weitere globale Variablen eingeführt, die den Administrator-Login und das Passwort enthalten. Diese. Wenn im Cluster ein Administratorkonto vorhanden ist, wird ein Dialog zur Eingabe Ihres Benutzernamens und Passworts angezeigt. Die Daten werden im Speicher gespeichert und in jeden Befehl für den entsprechenden Cluster eingefügt.

Dies liegt in der Verantwortung des Fehlerbehandlungsverfahrens.

ErrorParcing

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"
        }
    }
}

Diese. Je nachdem, was der Befehl zurückgibt, wird die Reaktion entsprechend ausfallen.

Derzeit sind ca. 95 Prozent der Funktionalität implementiert, es bleibt nur noch die Arbeit mit Sicherheitsprofilen umzusetzen und zu testen =). Das ist alles. Ich entschuldige mich für die zerknitterte Geschichte.

Der Code ist traditionell verfügbar hier.

Update: Ich habe die Arbeit mit Sicherheitsprofilen abgeschlossen. Nun ist die Funktionalität zu 100 % implementiert.

Update 2: Lokalisierung in Englisch und Russisch wurde hinzugefügt, die Arbeit unter Win7 wurde getestet
Schreiben einer GUI für 1C RAC oder noch einmal über Tcl/Tk

Source: habr.com

Kommentar hinzufügen