Wir schreiben ein GUI für 1C RAC, oder wieder über Tcl/Tk

Beim Eintauchen in die Welt der 1C-Produkte unter Linux stellte sich ein Mangel heraus – das Fehlen eines benutzerfreundlichen, plattformübergreifenden Werkzeugs zur Verwaltung von 1C-Serverclustern. Daher wurde entschieden, diesen Mangel zu beheben, indem ein GUI für das Konsolenwerkzeug rac entwickelt wurde. Die Programmiersprache Tcl/Tk wurde aufgrund ihrer Eignung für diese Aufgabe ausgewählt. In diesem Artikel möchte ich einige interessante Aspekte dieser Lösung vorstellen.

Für die Arbeit werden die Distributionen Tcl/Tk und 1C benötigt. Da ich die Möglichkeiten der Basisversion von Tcl/Tk ohne externe Pakete maximal nutzen wollte, ist die Version 8.6.7 erforderlich, die ttk enthält – ein Paket mit zusätzlichen grafischen Elementen. Darunter benötigen wir hauptsächlich ttk::TreeView, das es ermöglicht, Daten sowohl in einer baumartigen Struktur als auch in Tabellenform anzuzeigen. Auch wurde in der neuen Version die Behandlung von Ausnahmen überarbeitet (der Befehl try, der im Projekt beim Starten externer Befehle verwendet wird).

Das Projekt besteht aus mehreren Dateien (obwohl es nichts im Wege steht, alles in einer einzigen Datei zu erledigen):

rac_gui.cfg – Standardkonfiguration
rac_gui.tcl — Hauptstartskript
Im Verzeichnis lib befinden sich Dateien, die beim Start automatisch geladen werden:
function.tcl — Datei mit Prozeduren
gui.tcl — Hauptbenutzeroberfläche
images.tcl — Bilderbibliothek im base64-Format

Die Datei rac_gui.tcl startet den Interpreter, initialisiert Variablen, lädt Module, Konfigurationen usw. Der 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"
    }    
}

Nach dem Laden aller erforderlichen Komponenten und der Überprüfung der Verfügbarkeit des Dienstprogramms rac wird ein grafisches Fenster geöffnet. Die Benutzeroberfläche besteht aus drei Elementen:

Symbolleiste, Baum und Liste

Inhalt des „Baums“, der so nah wie möglich an der standardmäßigen Windows-Konsole von 1C gestaltet wurde.

Wir schreiben ein GUI für 1C RAC, oder wieder über Tcl/Tk

Der Hauptcode zur Bildung dieses Fensters befindet sich in der Datei
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 Ablauf zur Verwendung des Programms ist folgender:

1. Zunächst muss der Hauptserver des Clusters hinzugefügt werden (d.h. der Clusterverwaltungsserver (in Linux wird die Verwaltung mit dem Befehl „/opt/1C/v8.3/x86_64/ras cluster —daemon“ gestartet)).

Dazu klicken Sie auf die Schaltfläche „+“ und geben Sie im geöffneten Fenster die Serveradresse und den Port ein:

Wir schreiben ein GUI für 1C RAC, oder wieder über Tcl/Tk

Nach der Eingabe wird unser Server im Baum angezeigt; durch Klicken darauf wird entweder die Liste der Cluster geöffnet oder eine Fehlermeldung zur Verbindung angezeigt.

2. Wenn Sie auf den Cluster-Namen klicken, öffnet sich eine Liste der verfügbaren Funktionen.

3.…

Um einen neuen Cluster hinzuzufügen, wählen Sie einen verfügbaren aus der Liste aus und drücken Sie die „+“-Taste in der Symbolleiste, woraufhin ein Dialog zum Hinzufügen eines neuen Clusters geöffnet wird.

Wir schreiben ein GUI für 1C RAC, oder wieder über Tcl/Tk

Die Schaltflächen in der Symbolleiste führen kontextabhängige Funktionen aus, d.h. je nachdem, welches Element des Baums oder der Liste ausgewählt ist, wird die entsprechende Prozedur ausgeführt.

Lassen Sie uns am Beispiel der Hinzufügen-Schaltfläche („+“) betrachten:

Code zum Erstellen der Schaltfläche:

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

Hier sehen wir, dass beim Drücken der Schaltfläche die Prozedur „Add“ ausgeführt wird. Ihr Code ist:

proc Add {} {
    global active_cluster host
    # Bestimmen Sie die ID des ausgewählten Elements
    set id  [.frm_tree.tree selection] 
    # Bestimmen Sie den Wert dieses Elements
    set values [.frm_tree.tree item [.frm_tree.tree selection] -values]
    set key [lindex [split $id "::"] 0]
    # Die erforderliche Prozedur wird abhängig davon gestartet, was ausgewählt wurde
    if {$key eq "" || $key eq "server"} {
        set host [ Add::server ]
        return
    }
    Add::$key .frm_tree.tree $host $values
}

Hier zeigt sich einer der Vorteile von Tcl: Als Namen der Prozedur kann der Wert einer Variable übergeben werden:

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

Das heißt, wenn wir zum Beispiel auf den Hauptserver klicken und auf „+“ drücken, wird die Prozedur Add::server gestartet, wenn wir auf den Cluster klicken — Add::cluster und so weiter. (Ich werde später darauf eingehen, woher die benötigten „Schlüssel“ stammen.) Die genannten Prozeduren zeichnen die grafischen Elemente entsprechend dem Kontext.

Wie Sie vielleicht bemerkt haben, ähneln sich die Formate im Stil — was nicht verwunderlich ist, da sie durch eine einzige Prozedur ausgegeben werden, genauer gesagt das Hauptgerüst der Form (Fenster, Schaltflächen, Bild, Beschriftung), Name 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
    # Beschriftung mit Icon
    ttk::label $win_name.lbl -image $img
    # Rahmen mit Eingabefeldern
    set frm [ttk::labelframe $win_name.frm -text $lbl -labelanchor nw]
    
    grid columnconfigure $frm 0 -weight 1
    grid rowconfigure $frm 0 -weight 1
    # Rahmen und Tasten
    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 Fenstername (standardmäßig .add). Daher wäre der Aufruf für das Hinzufügen des Hauptservers und des Clusters entsprechend.

AddToplevel "Hinzufügen eines Hauptservers" server_grey_64

oder

AddToplevel "Hinzufügen des Clusters" cluster_grey_64

Um mit diesen Beispielen fortzufahren, zeige ich die Verfahren, die die Dialoge zum Hinzufügen eines Servers oder Clusters ausgeben.

Add::server

proc Add::server {} {
    global default
    # Hauptformular anzeigen
    set frm [AddToplevel "Hinzufügen des Hauptservers" server_grey_64]
    # Labels und Eingabefelder zum Formular hinzufügen
    label $frm.lbl_host -text "Serveradresse"
    entry  $frm.ent_host
    label $frm.lbl_port -text "Port"
    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]
    # Handler für die Schaltflächenbetätigung neu definieren
    .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
}

Add::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 ""
    }
    # Globale Variablen festlegen
    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 hinzufügen" cluster_grey_64]
    
    label $frm.lbl_host -text "Adresse des Hauptservers"
    entry  $frm.ent_host
    label $frm.lbl_port -text "Port"
    entry $frm.ent_port 
    $frm.ent_port  insert end $default(port)
    label $frm.lbl_name -text "Clustername"
    entry  $frm.ent_name
    label $frm.lbl_secure_connect -text "Sichere Verbindung"
    ttk::combobox $frm.cb_security_level -textvariable security_level -values $default(security_level)
    label $frm.lbl_expiration_timeout -text "Inaktive Prozesse nach deaktivieren stoppen nach:"
    entry  $frm.ent_expiration_timeout -textvariable expiration_timeout
    label $frm.lbl_session_fault_tolerance_level -text "Fehlertoleranzlevel"
    entry  $frm.ent_session_fault_tolerance_level -textvariable session_fault_tolerance_level
    label $frm.lbl_load_balancing_mode -text "Lastverteilungsmodus"
    ttk::combobox $frm.cb_load_balancing_mode -textvariable load_balancing_mode 
    -values $default(load_balancing_mode)
    label $frm.lbl_errors_count_threshold -text "Erlaubte Fehlermarge, %"
    entry  $frm.ent_errors_count_threshold -textvariable errors_count_threshold
    label $frm.lbl_processes -text "Arbeitsprozesse:"
    label $frm.lbl_lifetime_limit -text "Restartintervall, s"
    entry  $frm.ent_lifetime_limit -textvariable lifetime_limit
    label $frm.lbl_max_memory_size -text "Zulässige Speichermenge, KB"
    entry  $frm.ent_max_memory_size -textvariable max_memory_size
    label $frm.lbl_max_memory_time_limit -text "Zeitüberschreitung des zulässigen Speichers, s"
    entry  $frm.ent_max_memory_time_limit -textvariable max_memory_time_limit
    label $frm.lbl_kill_problem_processes -justify left -anchor nw -text "Problematische Prozesse zwangszusammenfassen"
    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
    # Handler neu definieren
    .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
}

Beim Vergleich des Codes dieser Prozeduren ist der Unterschied mit bloßem Auge sichtbar. Ich möchte die Aufmerksamkeit auf den Handler der Schaltfläche "Ok" lenken. In Tk können die Eigenschaften grafischer Elemente zur Laufzeit über Optionen überschrieben werden. configureZum Beispiel, der ursprüngliche Befehl zur Ausgabe der Schaltfläche:

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

In unseren Formularen hingegen hängt der Befehl von der benötigten 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 wird der Schaltfläche "Ok" die Ausführung der Prozedur zum Hinzufügen eines Clusters zugewiesen.

Hier ist es wert, einen kurzen Abstecher zur Arbeit mit grafischen Elementen in Tk zu machen – für verschiedene Dateneingabeelemente (Entry, Combobox, Checkbutton usw.) wurde der Parameter der textlichen Variable (textvariable) eingeführt:

Eintrag $frm.ent_lifetime_limit - Textvariable lifetime_limit

Diese Variable ist im globalen Namensraum definiert und enthält den aktuell eingegebenen Wert. Um den eingegebenen Text aus dem Feld zu erhalten, muss man einfach den entsprechenden Variablenwert abfragen (natürlich vorausgesetzt, dass sie bei der Erstellung des Elements definiert wurde).

Die zweite Methode, um den eingegebenen Text (für Entry-Elemente) zu erhalten, ist die Verwendung des Kommandos get:

.add.frm.ent_name get

Beide Methoden sind im oben genannten Code zu sehen.

Das Drücken dieser Taste startet in diesem Fall das Verfahren RunCommand mit der gebildeten Befehlskette zum Hinzufügen eines Clusters in den Begriffen von 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

Hier gelangen wir zur Hauptkommando, das den Start von rac mit den benötigten Parametern steuert, auch die Ausgabe der Kommandos analysiert und zurückgibt, falls erforderlich:

RunCommand

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
    # Öffnen des Kanals im nicht blockierenden Modus
    # $rac - Befehl mit dem vollständigen Pfad
    # $par - formatierte Startschlüssel und Optionen
    set pipe [open "|$rac_cmd $par" "r"]
    try {
        set lst ""
        set l ""
        # Ausgabe des Befehls wird zur Liste hinzugefügt
        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} {
        # Fehlerbehandler ausführen
        ErrorParcing $result $options
        return ""
    }
}

Nachdem die Daten des Hauptservers eingegeben wurden, wird er dem Baum hinzugefügt. Dafür ist der folgende Code in der oben genannten Prozedur Add:server verantwortlich:

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

Jetzt erhalten wir durch einen Klick auf den Namen des Servers im Baum eine Liste der Cluster, die von diesem Server verwaltet werden. Wenn wir auf den Cluster klicken, erhalten wir die Liste der Cluster-Elemente (Server, Informationsdatenbanken usw.). Dies ist in der Prozedur TreePress (Datei lib/function.tcl) umgesetzt:

proc TreePress {tree} {
   global host server active_cluster infobase
   # Definieren Sie das ausgewählte Element
    set id  [$tree selection]
   # Stellen Sie die benötigten globalen Variablen ein
    SetGlobalVarFromTreeItems $tree $id
   # Bestimmen Sie den Schlüssel und den Wert, d.h. den Typ des ausgewählten Elements
    set values [$tree item $id -values]
    set key [lindex [split $id "::"] 0]
   # Je nachdem, was ausgewählt wurde, wird das entsprechende Verfahren im Namensraum Run ausgeführt
    Run::$key $tree $host $values
}

Dementsprechend wird für den Hauptserver Run::server ausgeführt (für den Cluster — Run::cluster, für den Arbeitsserver — Run::work_server usw.). Das bedeutet, dass der Wert der Variablen $key ein Teil des Namens des Baums ist, der durch die Option festgelegt wird -id.

Achten wir auf das Verfahren

Run::server

proc Run::server {tree host values} {
    # wir erhalten die Liste der Cluster des benötigten Servers
    set lst [RunCommand server::$host "cluster list $host"]
    if {$lst eq ""} {return}
    set l [lindex $lst 0]
    #puts $lst
    # entfernen überflüssiger Einträge aus der Liste
    .frm_work.tree_work delete  [ .frm_work.tree_work children {}]
    # lesen der Liste
    foreach cluster_list $lst {
        # Füllen der Liste mit den erhaltenen Werten
        InsertItemsWorkList $cluster_list
        # Verarbeitung der Ausgabe (Liste), um Daten in den Baum hinzuzufügen
        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]]
            }
        }
    }
    # hinzufügen von Clustern zum Baum
    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"
            # fügen Sie Elemente zum Cluster hinzu
            InsertClusterItems $tree $id
        }
    }
    if { [$tree exists "agent_admins::$id"] == 0 } {
        $tree insert "server::$host" end -id "agent_admins::$id" -text "Administratoren" -values "$id"
        #InsertClusterItems $tree $id
    }
}

Dieses Verfahren verarbeitet die vom Server über den Befehl RunCommand erhaltenen Informationen und fügt verschiedene Elemente in das Baumstruktur ein – Cluster, unterschiedliche Root-Elemente (Datenbanken, Arbeitsserver, Sitzungen usw.). Wenn man genau hinsieht, kann man den Aufruf des Verfahrens InsertItemsWorkList erkennen. Dies wird verwendet, um Elemente in die grafische Liste hinzuzufügen, indem die Ausgabe des Konsolenprogramms rac bearbeitet wird, das zuvor in Form einer Liste in die Variable $lst zurückgegeben wurde. Dies ist eine Liste von Listen, die Paare von Elementen enthält, die durch einen Doppelpunkt getrennt sind.

Zum Beispiel die Liste der Verbindungen im Cluster:

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

Grafisch dargestellt würde es ungefähr so aussehen:

Wir schreiben ein GUI für 1C RAC, oder wieder über Tcl/Tk

Das oben genannte Verfahren extrahiert die Bezeichnungen der Elemente für den Titel und die Daten zur Befüllung der Tabelle:

InsertItemsWorkList

proc InsertItemsWorkList {lst} {
    global work_list_row_count
    # Farbschema für die Zeile festlegen
    if [expr $work_list_row_count % 2] {
        set tag dark
    } else {
        set tag light
    }
    # Zeilen in Paare von Schlüssel - Wert aufteilen
    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]
        }
    }
     # Tabelle befüllen
    .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
    # Überschriften festlegen
    foreach j $column_list {
        .frm_work.tree_work heading $j -text $j
    }
    incr work_list_row_count
}

Hier wurde anstelle des einfachen Befehls [split $str „:“], der die Zeichenfolge in durch „:“ getrennte Elemente aufteilt und eine Liste zurückgibt, ein regulärer Ausdruck verwendet, da einige Elemente ebenfalls Doppelpunkt enthalten.

Die Prozedur InsertClusterItems (eine von mehreren ähnlichen) fügt einfach eine Liste von untergeordneten Elementen mit den entsprechenden Identifikatoren zum erforderlichen Element Cluster im Baum hinzu.
InsertClusterItems

proc InsertClusterItems {tree id} {
    set parent "cluster::$id"
    $tree insert $parent end -id "infobases::$id" -text "Datenbanken" -values "$id"
    $tree insert $parent end -id "servers::$id" -text "Arbeitsserver" -values "$id"
    $tree insert $parent end -id "admins::$id" -text "Administratoren" -values "$id"
    $tree insert $parent end -id "managers::$id" -text "Cluster-Manager" -values $id
    $tree insert $parent end -id "processes::$id" -text "Arbeitsprozesse" -values "workprocess-all"
    $tree insert $parent end -id "sessions::$id" -text "Sitzungen" -values "sessions-all"
    $tree insert $parent end -id "locks::$id" -text "Sperren" -values "blocks-all"
    $tree insert $parent end -id "connections::$id" -text "Verbindungen" -values "connections-all"
    $tree insert $parent end -id "profiles::$id" -text "Sicherheitsprofile" -values $id
}

Es können noch zwei weitere Varianten dieser Prozedur in Betracht gezogen werden, um zu zeigen, wie man die wiederholenden Befehle optimieren und vermeiden kann:

In dieser Prozedur sind die Hinzufügen und die Prüfung direkt angegangen:

InsertBaseItems

proc InsertBaseItems {tree id} {
    set parent "infobase::$id"
    if { [$tree exists "sessions::$id"] == 0 } {
        $tree insert $parent end -id "sessions::$id" -text "Sitzungen" -values "$id"
    }
    if { [$tree exists "locks::$id"] == 0 } {
        $tree insert $parent end -id "locks::$id" -text "Sperren" -values "$id"
    }
    if { [$tree exists "connections::$id"] == 0 } {
        $tree insert $parent end -id "connections::$id" -text "Verbindungen" -values "$id"
    }
}

Hier ist der Ansatz deutlich besser:

InsertProfileItems

proc InsertProfileItems {tree id} {
    set parent "profile::$id"
    set lst {
        {dir "Virtuelle Verzeichnisse"}
        {com "Erlaubte COM-Klassen"}
        {addin "Externe Komponenten"}
        {module "Externe Berichte und Verarbeitung"}
        {app "Erlaubte Anwendungen"}
        {inet "Internet-Ressourcen"}
    }
    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 liegt im Einsatz einer Schleife, in der der wiederholte Befehl (die Befehle) ausgeführt wird. Welcher Ansatz angewendet werden soll, liegt bereits im Ermessen des Entwicklers.

Das Hinzufügen von Elementen und das Abrufen von Daten haben wir bereits behandelt, nun ist es an der Zeit, uns dem Bearbeiten zu widmen. Da für das Bearbeiten und Hinzufügen meist die gleichen Parameter verwendet werden (eine Ausnahme bildet die Informationsdatenbank), sind auch die Dialogformulare identisch. Der Algorithmus zum Aufruf der Prozeduren für das Hinzufügen sieht folgendermaßen aus:

Add::$key->AddToplevel

Und für das Bearbeiten so:

Edit::$key->Add::$key->AddTopLevel

Nehmen wir als Beispiel die Bearbeitung eines Clusters. Dazu klicken wir im Baum auf den Namen des Clusters, drücken die Schaltfläche zum Bearbeiten in der Werkzeugleiste (das Bleistift-Symbol), und auf dem Bildschirm wird das entsprechende Formular angezeigt:

Wir schreiben ein GUI für 1C RAC, oder wieder über Tcl/Tk
Edit::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 ""
    }
    # Erstellen des Formulars für den Cluster
    set frm [Add::cluster $tree $host $values]
    # Ändern des Textes auf dem Label
    $frm configure -text "Cluster bearbeiten"
    
    set active_cluster $values
    # Abrufen der Daten des ausgewählten Clusters
    set lst [RunCommand cluster::$values "cluster info --cluster=$active_cluster $host"]
    # Ausfüllen der Felder
    FormFieldsDataInsert $frm $lst
    # Deaktivieren der Felder, deren Bearbeitung nicht erlaubt ist
    $frm.ent_host configure -state disable
    $frm.ent_port configure -state disable
    # Neuzuweisung des Handlers
    .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
    }
}

Die Kommentare im Code sind im Großen und Ganzen verständlich, abgesehen von der Tatsache, dass der Button-Handler überschrieben wurde und es eine Prozedur namens FormFieldsDataInsert gibt, die die Felder mit Daten befüllt und die Variablen initialisiert.

FormFieldsDataInsert

proc FormFieldsDataInsert {frm lst} {
    foreach i [lindex $lst 0] {
        # Parameter und Werte sammeln
        if [regexp -nocase -all -- {(D+)(s*?|)(:)(s*?|)(.*)} $i match param v2 v3 v4 value] {
            # Zeichen ersetzen
            regsub -all -- "-" [string trim $param] "_" entry_name
            # Daten befüllen
            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 """]
            }
            # Für Checkboxen Werte ändern
            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
                }
            }
        }
    }
}

In diesem Verfahren gibt es einen weiteren Vorteil von tcl - als Variablennamen werden die Werte anderer Variablen eingesetzt. Das heißt, zur Automatisierung der Formularausfüllung und der Initialisierung von Variablen entsprechen die Bezeichnungen der Felder und Variablen den Schlüsseln der Befehlszeile des Dienstprogramms rac und den Namen der Parameter der Ausgabe von Befehlen, mit einigen Ausnahmen - das Minuszeichen wird durch einen Unterstrich ersetzt. Zum Beispiel scheduled-jobs-deny entspricht dem Feld ent_scheduled_jobs_deny und der Variablen scheduled_jobs_deny.

Die Formulare zum Hinzufügen und Bearbeiten können sich in den Feldern unterscheiden, zum Beispiel bei der Arbeit mit der Informationsdatenbank:

Hinzufügen von IB

Wir schreiben ein GUI für 1C RAC, oder wieder über Tcl/Tk

Bearbeiten von IB

Wir schreiben ein GUI für 1C RAC, oder wieder über Tcl/Tk

In der Bearbeitungsverfahren Edit::infobase werden die erforderlichen Felder zum Formular hinzugefügt, der Code ist umfangreich, daher führe ich ihn hier nicht auf.

Ähnliche Verfahren wurden auch für das Hinzufügen, Bearbeiten und Löschen anderer Elemente implementiert.

Da die Funktionalität des Tools eine unbegrenzte Anzahl von Servern, Clustern, Datenbanken usw. umfasst, wurden mehrere globale Variablen eingeführt, um zu bestimmen, zu welchem Cluster ein Server oder eine Datenbank gehört. Das bedeutet, dass das Verfahren rekursiv durch alle Elternelemente läuft und die Variablen setzt:

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

Der 1C-Cluster ermöglicht den Betrieb sowohl mit als auch ohne Autorisierung. Es gibt zwei Arten von Administrators: den Cluster-Agenten-Administrator und den Cluster-Administrator. Für eine ordnungsgemäße Funktion wurden daher zusätzlich vier globale Variablen eingeführt, die den Benutzernamen und das Passwort des Administrators enthalten. Das bedeutet, wenn ein Administrator-Konto im Cluster vorhanden ist, wird ein Dialogfeld angezeigt, um Benutzernamen und Passwort einzugeben; die Daten werden im Speicher gespeichert und bei jedem Befehl für den entsprechenden Cluster eingesetzt.

Dies wird durch das Fehlerbehandlungsverfahren geregelt.

ErrorParcing

proc ErrorParcing {err opt} {
    global cluster_user cluster_pwd agent_user agent_pwd
        switch -regexp -- $err {
        "Clusteradministrator ist nicht authentifiziert" {
            AuthorisationDialog "Clusteradministrator"
            .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
        }
        "Zentralserveradministrator ist nicht authentifiziert" {
            AuthorisationDialog "Administrator des Cluster-Agenten"
            .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
            }
        }
        "Administrator des Clusters ist nicht authentifiziert" {
            AuthorisationDialog "Clusteradministrator"
            .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
        }
        "Administrator des zentralen Servers ist nicht authentifiziert" {
            AuthorisationDialog "Administrator des Cluster-Agenten"
            .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"
        }
    }
}

Das heißt, je nachdem, was der Befehl zurückgibt, wird die Reaktion entsprechend ausfallen.

Aktuell ist die Funktionalität zu etwa 95 % implementiert. Es bleibt noch die Arbeit mit Sicherheitsprofilen und das Testen zu erledigen =). Damit ist alles gesagt. Ich bitte um Entschuldigung für die holprige Erzählweise.

Der Code ist, wie gewohnt, verfügbar. hier.

Update: Die Arbeit mit den Sicherheitsprofilen wurde abgeschlossen. Nun ist die Funktionalität zu 100 % realisiert.

Update 2: Es wurde eine Lokalisierung für Englisch und Russisch hinzugefügt, die Funktionalität in Windows 7 wurde überprüft.
Wir schreiben ein GUI für 1C RAC, oder wieder über Tcl/Tk

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster