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

Während ich mich in die Arbeit mit den 1C-Produkten in der Linux-Umgebung vertiefte, stellte ich einen Nachteil fest – das Fehlen eines benutzerfreundlichen graphischen plattformübergreifenden Werkzeugs zur Verwaltung eines 1C-Serverclusters. Daher wurde beschlossen, diesen Nachteil zu beheben, indem ein GUI für das Konsolenprogramm rac entwickelt wurde. Die Programmiersprache für die Entwicklung wurde auf tcl/tk ausgewählt, da ich sie für die am besten geeignete Lösung für diese Aufgabe halte. Einige interessante Aspekte dieser Lösung möchte ich in diesem Material vorstellen.

Für die Arbeit werden die Distributionen tcl/tk und 1C benötigt. Da ich die Möglichkeiten der Standardversion von tcl/tk ohne den Einsatz externer Pakete maximal nutzen wollte, benötige ich die Version 8.6.7, die ttk – ein Paket mit zusätzlichen grafischen Elementen – enthält, von denen wir hauptsächlich ttk::TreeView benötigen. Dieses ermöglicht es, Daten sowohl in Form einer Baumstruktur als auch in Form einer Tabelle (Liste) darzustellen. Darüber hinaus wurde in der neuen Version die Verarbeitung von Ausnahmen (der Befehl try, der im Projekt beim Start externer Befehle verwendet wird) überarbeitet.

Das Projekt besteht aus mehreren Dateien (obwohl nichts dagegen spricht, alles in einer einzigen Datei zu machen):

rac_gui.cfg – die Standardkonfiguration
rac_gui.tcl – das Hauptstartskript
Im lib-Verzeichnis befinden sich Dateien, die beim Start automatisch geladen werden:
function.tcl – Datei mit Prozeduren
gui.tcl – die Hauptgrafikschnittstelle
images.tcl – Bildbibliothek in base64

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 des Vorhandenseins des rac-Utilities wird ein grafisches Fenster gestartet. Die Benutzeroberfläche des Programms besteht aus drei Elementen:

Symbolleiste, Baum und Liste

Der Inhalt des "Baums" wurde so gestaltet, dass er möglichst der Standard-Windows-Komponente von 1C ähnelt.

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

Der Hauptcode zur Erstellung 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 Arbeitsablauf mit dem Programm ist folgendermaßen:

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

Dafür drücken Sie die Taste „+“ und geben im geöffneten Fenster die Adresse des Servers und den Port ein:

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

Nach dem Klicken erscheint unser Server im Baum, und es öffnet sich entweder die Liste der Cluster oder es wird ein Verbindungsfehler ausgegeben.

2. Ein Klick auf den Cluster-Namen öffnet die verfügbaren Funktionen für diesen.

3.…

Und so weiter, d.h. um einen neuen Cluster hinzuzufügen, wählen wir einen beliebigen verfügbaren aus der Liste aus und drücken die Schaltfläche „+“ in der Symbolleiste; es wird ein Dialog zum Hinzufügen des Neuen angezeigt:

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

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

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

Code zur Generierung der Schaltfläche:

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

Hier sehen wir, dass beim Klicken auf die Schaltfläche die Prozedur „Add“ ausgeführt wird; deren Code lautet:

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]
    # Abhängig von dem, was ausgewählt wurde, wird die entsprechende Prozedur gestartet
    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 Variablen übergeben werden:

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

Das heißt, zum Beispiel, wenn wir auf den Hauptserver klicken und „+“ drücken, wird die Prozedur Add::server aufgerufen; wenn wir auf einen Cluster klicken — Add::cluster und so weiter (woher die benötigten „Schlüssel“ stammen, werde ich gleich erläutern); die aufgelisteten Prozeduren zeichnen die grafischen Elemente entsprechend dem Kontext.

Wie Sie wahrscheinlich bereits bemerkt haben, sind die Formulare stilistisch ähnlich — das ist nicht überraschend, da sie durch eine einzige Prozedur erstellt werden, genauer gesagt durch das Hauptgerüst des Formulars (Fenster, Schaltflächen, Bild, Beschriftung), der 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
    # Etikett 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 Buttons
    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 Icon aus der Bibliothek (lib/images.tcl) und optional der Fensternamen (Standard: .add). Somit lautet der Aufruf für die oben genannten Beispiele zum Hinzufügen des Hauptservers und des Clusters entsprechend:

AddToplevel "Hinzufügen des Hauptservers" server_grey_64

oder

AddToplevel "Hinzufügen des Clusters" cluster_grey_64

Und um mit diesen Beispielen fortzufahren, zeige ich die Prozeduren, die die Dialoge zum Hinzufügen für Server oder Cluster anzeigen.

Add::server

proc Add::server {} {
    global default
    # Hauptformular anzeigen
    set frm [AddToplevel "Hinzufügen des Hauptservers" server_grey_64]
    # Labels und Eingabefelder zu diesem 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 Button-Klick überschreiben
    .add.frm_btn.btn_ok configure -command {
        set host [SaveMainServer [.add.frm.ent_host get] [.add.frm.ent.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 setzen ()
    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 "Nicht laufende Prozesse 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 "Lastverteilung-Modus"
    ttk::combobox $frm.cb_load_balancing_mode -textvariable load_balancing_mode 
    -values $default(load_balancing_mode)
    label $frm.lbl_errors_count_threshold -text "Zulässige Abweichung der Serverfehleranzahl, %"
    entry  $frm.ent_errors_count_threshold -textvariable errors_count_threshold
    label $frm.lbl_processes -text "Arbeitsprozesse:"
    label $frm.lbl_lifetime_limit -text "Neustartintervall, Sek."
    entry  $frm.ent_lifetime_limit -textvariable lifetime_limit
    label $frm.lbl_max_memory_size -text "Erlaubte Speichermenge, KB"
    entry  $frm.ent_max_memory_size -textvariable max_memory_size
    label $frm.lbl_max_memory_time_limit -text "Zeitraum für Überschreitung der erlaubten Speichermenge, Sek."
    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 zwangsweise beenden"
    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 überschreiben
    .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 des Buttons „Ok“ lenken. In Tk können die Eigenschaften grafischer Elemente zur Laufzeit des Programms über die Option überschrieben werden. configure. Zum Beispiel der ursprüngliche Befehl zur Ausgabe des Buttons:

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

In unseren Formularen hängt der Befehl jedoch 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 beim Button „geblendet“ der Befehl zum Hinzufügen eines Clusters ausgeführt.

Hier ist es wichtig, einen kurzen Abstecher zur Arbeit mit grafischen Elementen in Tk zu machen – für verschiedene Dateneingabeelemente (Eingabefeld, Kombinationsfeld, Kontrollkästchen usw.) wurde ein Parameter wie die Textvariable (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. Das heißt, um den eingegebenen Text aus dem Feld zu erhalten, muss man einfach den Wert der entsprechenden Variable lesen (natürlich vorausgesetzt, dass sie beim Erstellen des Elements definiert wurde).

Die zweite Methode, um den eingegebenen Text (für Eingabeelemente) zu erhalten, besteht darin, den Befehl get zu verwenden:

.add.frm.ent_name get

Beide Methoden sind im oben genannten Code zu sehen.

Ein Druck auf diesen Button startet in diesem Fall die Prozedur RunCommand mit der gebildeten Befehlszeile 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 sind wir also bei dem Hauptbefehl angelangt, der den Start von rac mit den benötigten Parametern steuert, auch die Ausgaben von Befehlen in Listen analysiert und zurückgibt, wenn dies erforderlich ist:

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
    # Kanal im nicht blockierenden Modus öffnen
    # $rac - Befehl mit vollständigem Pfad
    # $par - erzeugte Startparameter und Optionen    
    set pipe [open "|$rac_cmd $par" "r"]
    try {
        set lst ""
        set l ""
        # Die 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} {
        # Fehlerbehandlungsroutine starten
        ErrorParcing $result $options
        return ""
    }
}

Nach der Eingabe der Daten des Hauptservers wird dieser in den Baum hinzugefügt, dafür ist im oben genannten Verfahren Add:server der folgende Code verantwortlich:

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

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

proc TreePress {tree} {
   global host server active_cluster infobase
   # Den ausgewählten Punkt definieren
    set id  [$tree selection]
   # Die benötigten globalen Variablen festlegen
    SetGlobalVarFromTreeItems $tree $id
   # Schlüssel und Wert definieren, 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 die entsprechende Prozedur im Namensraum Run gestartet
    Run::$key $tree $host $values
}

Entsprechend wird für den Hauptserver Run::server gestartet (für Cluster — Run::cluster, für den Arbeitsserver — Run::work_server usw.). Das bedeutet, dass der Wert der Variablen $key Teil des Namens des Baumelements ist, das durch die Option -id.

Auf die Prozedur

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 Elemente 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
        # Verarbeiten der Ausgabe (Liste), um Daten zum 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]]
            }
        }
    }
    # Fügen Sie die Cluster zum Baum hinzu
    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 Daten, die vom Server über den Befehl RunCommand erhalten wurden, und fügt verschiedene Elemente in den Baum ein – Cluster, verschiedene Wurzeln (Datenbanken, Arbeitsserver, Sitzungen usw.). Bei genauerem Hinsehen kann man den Aufruf des Verfahrens InsertItemsWorkList erkennen. Es wird verwendet, um Elemente in die grafische Liste einzufügen, indem die Ausgabe des Konsolenwerkzeugs rac verarbeitet wird, das zuvor in Form einer Liste in die Variable $lst zurückgegeben wurde. Dies ist eine Liste von Listen, die Elementpaare enthalten, die durch einen Doppelpunkt getrennt sind.

Zum Beispiel die Liste der 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

Grafisch sieht es etwa so aus:

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

Das genannte Verfahren extrahiert die Namen der Elemente für die Überschrift und die Daten zur Befüllung der Tabelle:

InsertItemsWorkList

proc InsertItemsWorkList {lst} {
    global work_list_row_count
    # Farbwechsel für die Zeile einstellen
    if [expr $work_list_row_count % 2] {
        set tag dunkel
    } else {
        set tag hell
    }
    # Aufteilen der Zeilen in Schlüssel-Wert-Paare
    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 fü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
    # Kopfzeilen einstellen
    foreach j $column_list {
        .frm_work.tree_work heading $j -text $j
    }
    incr work_list_row_count
}

Hier wird anstelle des einfachen Befehls [split $str «:»], der einen String an den durch „:“ getrennten Elementen aufteilt und eine Liste zurückgibt, ein regulärer Ausdruck verwendet, da einige Elemente ebenfalls einen Doppelpunkt enthalten.

Die Prozedur InsertClusterItems (eine von mehreren ähnlichen) fügt einfach der Baumstruktur für das erforderliche Element cluster eine Liste von untergeordneten Elementen mit den entsprechenden Identifikatoren hinzu.
InsertClusterItems

proc InsertClusterItems {tree id} {
    set parent "cluster::$id"
    $tree insert $parent end -id "infobases::$id" -text "Informationsdatenbanken" -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 Alternativen zur Implementierung einer ähnlichen Prozedur betrachtet werden, bei denen anschaulich zu sehen ist, wie man optimieren und sich wiederholende Befehle vermeiden kann:

In dieser Prozedur sind Hinzufügen und Überprüfen direkt umgesetzt:

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 weitaus passender:

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 "Internetressourcen"}
    }
    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 in der Anwendung der Schleife, in der der sich wiederholende Befehl (Befehle) ausgeführt wird. Welcher Ansatz angewendet wird, liegt im Ermessen des Entwicklers.

Wir haben die Hinzufügung von Elementen und das Abrufen von Daten betrachtet, jetzt ist es an der Zeit, uns mit der Bearbeitung zu befassen. Da in der Regel beim Bearbeiten und Hinzufügen dieselben Parameter verwendet werden (mit Ausnahme der Informationsdatenbank), werden auch dieselben Dialogformulare verwendet. Der Algorithmus zum Aufrufen der Prozeduren zum Hinzufügen sieht folgendermaßen aus:

Add::$key->AddToplevel

Und für die Bearbeitung so:

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

Zum Beispiel nehmen wir die Bearbeitung eines Clusters, d.h. wenn wir im Baum auf den Namen des Clusters klicken, drücken wir die Schaltfläche zum Bearbeiten in der Symbolleiste (Bleistift) und das entsprechende Formular wird auf dem Bildschirm angezeigt:

Wir entwickeln 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
    # erhalten der Daten zum ausgewählten Cluster
    set lst [RunCommand cluster::$values "cluster info --cluster=$active_cluster $host"]
    # füllen der Felder
    FormFieldsDataInsert $frm $lst
    # deaktivieren der Felder, deren Bearbeitung verboten ist
    $frm.ent_host configure -state disable
    $frm.ent_port configure -state disable
    # neu zuweisen 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 machen im Grunde alles klar, außer dass der Handler für die Schaltfläche umdefiniert wurde und die Prozedur FormFieldsDataInsert vorhanden ist, die die Felder mit Daten füllt und die Variablen initialisiert:

FormFieldsDataInsert

proc FormFieldsDataInsert {frm lst} {
    foreach i [lindex $lst 0] {
        # erhalten der Liste von Parametern und Werten
        if [regexp -nocase -all -- {(D+)(s*?|)(:)(s*?|)(.*)} $i match param v2 v3 v4 value] {
            # ändern der Zeichen
            regsub -all -- "-" [string trim $param] "_" entry_name
            # füllen mit Daten
            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 zeigt sich ein weiterer Vorteil von tcl – als Variablennamen werden die Werte anderer Variablen eingesetzt. Das heißt, zur Automatisierung der Formularbefüllung und der Initialisierung von Variablen entsprechen die Bezeichnungen von Feldern und Variablen den Schlüsseln der Befehlszeile des Tools rac sowie den Bezeichnungen der Ausgabeparameter von Befehlen mit einer gewissen Ausnahme – 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 der Zusammensetzung der Felder unterscheiden, zum Beispiel die Arbeit mit der Informationsdatenbank:

Hinzufügen von IB

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

Bearbeiten von IB

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

Im Bearbeitungsverfahren Edit::infobase werden die erforderlichen Felder zum Formular hinzugefügt, der Code ist umfangreich, daher lasse ich ihn hier weg.

Analog wurden die Verfahren für Hinzufügen, Bearbeiten, Löschen und für andere Elemente implementiert.

Da die Arbeit des Tools eine unbegrenzte Anzahl von Servern, Clustern, Informationsdatenbanken usw. umfasst, wurden mehrere globale Variablen eingeführt, deren Werte bei jedem Klicken auf die Baumelemente festgelegt werden. Das heißt, das Verfahren 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
    }
}

Der Cluster 1C ermöglicht die Arbeit sowohl mit als auch ohne Autorisierung. Es gibt zwei Arten von Administratoren – den Cluster-Agent-Administrator und den Cluster-Administrator. Entsprechend wurden weitere 4 globale Variablen eingeführt, die den Login und das Passwort des Administrators enthalten. Das heißt, wenn im Cluster ein Administrator-Konto vorhanden ist, wird ein Dialog zur Eingabe von Login und Passwort angezeigt, die Daten werden im Speicher gespeichert und in jeden Befehl für den entsprechenden Cluster eingesetzt.

Dafür ist die Fehlerbehandlungsprozedur verantwortlich

ErrorParcing

proc ErrorParcing {err opt} {
    global cluster_user cluster_pwd agent_user agent_pwd
        switch -regexp -- $err {
        "Cluster-Administrator ist nicht authentifiziert" {
            AuthorisationDialog "Administrator des Clusters"
            .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
            }
        }
        "Администратор кластера не аутентифицирован" {
            AuthorisationDialog "Administrator des Clusters"
            .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 entsprechend reagiert.

Im Moment ist die Funktionalität zu etwa 95 % umgesetzt, es fehlt nur noch die Arbeit mit Sicherheitsprofilen und die Testung =). Das ist alles. Ich bitte um Entschuldigung für die zusammenhanglose Erzählung.

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

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

Update 2: Englisch und Russisch wurden lokalisiert, die Funktionalität unter Win7 wurde überprüft
Wir entwickeln ein GUI für 1C RAC, oder wieder über Tcl/Tk

Quelle: habr.com

60GB SSD 8Gb DDR4