Scriviamo GUI per 1C RAC, o di nuovo su Tcl/Tk

Con il progredire nell'argomento dei prodotti 1C in ambiente Linux, è emersa una mancanza: l'assenza di uno strumento grafico multipiattaforma per la gestione del cluster di server 1C. È stata quindi presa la decisione di colmare questa lacuna scrivendo un'interfaccia grafica per l'utilità da console rac. Il linguaggio scelto per lo sviluppo è stato tcl/tk, in quanto, a mio avviso, il più adatto per questo compito. Ora, desidero presentare alcuni aspetti interessanti della soluzione in questo materiale.

Per il lavoro saranno necessari i pacchetti tcl/tk e 1C. Poiché ho deciso di utilizzare al massimo le capacità della fornitura base di tcl/tk senza applicare pacchetti esterni, è necessaria la versione 8.6.7, che include ttk — un pacchetto con elementi grafici aggiuntivi, di cui avremo bisogno principalmente di ttk::TreeView, che consente di visualizzare i dati sia come struttura ad albero che come tabella (elenco). Inoltre, nella nuova versione è stato riprogettato il sistema di gestione delle eccezioni (il comando try, utilizzato nel progetto per l'esecuzione di comandi esterni).

Il progetto è composto da diversi file (anche se non c'è nulla che impedisca di fare tutto in uno):

rac_gui.cfg — configurazione di default
rac_gui.tcl — script principale di avvio
Nella cartella lib si trovano i file caricati automaticamente all'avvio:
function.tcl — file con procedure
gui.tcl — interfaccia grafica principale
images.tcl — libreria di immagini in base64

Il file rac_gui.tcl, di fatto, avvia l'interprete, inizializza le variabili, carica i moduli, le configurazioni e così via. Il contenuto del file con i commenti:

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

Dopo aver caricato tutto ciò che serve e verificato la presenza dell'utilità rac, verrà aperta una finestra grafica. L'interfaccia del programma è composta da tre elementi:

Barra degli strumenti, albero e lista

Il contenuto dell'«albero» l'ho fatto il più simile possibile all'applet standard di Windows di 1C.

Scriviamo GUI per 1C RAC, o di nuovo su Tcl/Tk

Il codice principale che crea questa finestra si trova nel file
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

L'algoritmo di lavoro con il programma è il seguente:

1. All'inizio, bisogna aggiungere il server principale del cluster (cioè il server di gestione del cluster (in Linux la gestione viene avviata con il comando «/opt/1C/v8.3/x86_64/ras cluster —daemon»)).

Per fare ciò, si preme il pulsante «+» e nella finestra che si apre, si inserisce l'indirizzo del server e la porta:

Scriviamo GUI per 1C RAC, o di nuovo su Tcl/Tk

Successivamente, nell'albero apparirà il nostro server su cui fare clic per aprire l'elenco dei cluster o verrà visualizzato un errore di connessione.

2. Facendo clic sul nome del cluster si aprirà un elenco di funzioni disponibili per esso.

3.…

E così via, cioè, per aggiungere un nuovo cluster, selezioniamo uno qualsiasi disponibile nell'elenco e facciamo clic sul pulsante «+» nella barra degli strumenti, apparirà una finestra di dialogo per l'aggiunta del nuovo:

Scriviamo GUI per 1C RAC, o di nuovo su Tcl/Tk

I pulsanti nella barra degli strumenti eseguono funzioni a seconda del contesto, cioè in base all'elemento dell'albero o dell'elenco selezionato, verrà eseguita una certa procedura.

Consideriamo l'esempio del pulsante di aggiunta («+»):

Codice di creazione del pulsante:

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

Qui vediamo che al clic del pulsante verrà eseguita la procedura «Add», il cui codice è:

proc Add {} {
    global active_cluster host
    # Definiamo l'identificatore dell'elemento selezionato
    set id  [.frm_tree.tree selection] 
    # Definiamo il valore di questo elemento
    set values [.frm_tree.tree item [.frm_tree.tree selection] -values]
    set key [lindex [split $id "::"] 0]
    # a seconda di cosa abbiamo selezionato verrà avviata la procedura corretta
    if {$key eq "" || $key eq "server"} {
        set host [ Add::server ]
        return
    }
    Add::$key .frm_tree.tree $host $values
}

Ecco uno dei vantaggi del Tcl: come nome della procedura possiamo passare il valore di una variabile:

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

Cioè, ad esempio, se clicchiamo sul server principale e premiamo «+», verrà avviata la procedura Add::server, se sul cluster — Add::cluster e così via (sul perché ci sono le «chiavi» giuste scriverò più avanti), le procedure elencate disegnano gli elementi grafici corrispondenti al contesto.

Come avete già potuto notare, le forme hanno uno stile simile — e non sorprende, poiché vengono generate da una procedura comune, più precisamente la struttura principale della forma (finestra, pulsanti, immagine, etichetta), il nome della procedura 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
    # etichetta con icona
    ttk::label $win_name.lbl -image $img
    # frame con campi di input
    set frm [ttk::labelframe $win_name.frm -text $lbl -labelanchor nw]
    
    grid columnconfigure $frm 0 -weight 1
    grid rowconfigure $frm 0 -weight 1
    # frame e pulsanti
    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
}

Parametri di chiamata: titolo, nome immagine per l'icona dalla libreria (lib/images.tcl) e un parametro facoltativo nome finestra (predefinito .add). Quindi, se prendiamo gli esempi precedenti per l'aggiunta del server principale e del cluster, la chiamata sarà rispettivamente:

AddToplevel "Aggiunta del server principale" server_grey_64

o

AddToplevel "Aggiunta del cluster" cluster_grey_64

E continuando con questi esempi, mostrerò le procedure che generano le finestre di dialogo per l'aggiunta di un server o un cluster.

Add::server

proc Add::server {} {
    global default
    # visualizziamo il modulo principale
    set frm [AddToplevel "Aggiunta del server principale" server_grey_64]
    # aggiungiamo etichette e campi di input a questo modulo
    label $frm.lbl_host -text "Indirizzo del server"
    entry  $frm.ent_host
    label $frm.lbl_port -text "Porta"
    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]
    # ridefiniamo il gestore per il click del pulsante
    .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 ""
    }
    # impostiamo le variabili globali ()
    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 "Aggiunta cluster" cluster_grey_64]
    
    label $frm.lbl_host -text "Indirizzo del server principale"
    entry  $frm.ent_host
    label $frm.lbl_port -text "Porta"
    entry $frm.ent_port 
    $frm.ent_port  insert end $default(port)
    label $frm.lbl_name -text "Nome del cluster"
    entry  $frm.ent_name
    label $frm.lbl_secure_connect -text "Connessione sicura"
    ttk::combobox $frm.cb_security_level -textvariable security_level -values $default(security_level)
    label $frm.lbl_expiration_timeout -text "Fermare i processi spenti dopo:"
    entry  $frm.ent_expiration_timeout -textvariable expiration_timeout
    label $frm.lbl_session_fault_tolerance_level -text "Livello di tolleranza ai guasti"
    entry  $frm.ent_session_fault_tolerance_level -textvariable session_fault_tolerance_level
    label $frm.lbl_load_balancing_mode -text "Modalità di bilanciamento del carico"
    ttk::combobox $frm.cb_load_balancing_mode -textvariable load_balancing_mode 
    -values $default(load_balancing_mode)
    label $frm.lbl_errors_count_threshold -text "Soglia consentita per il numero di errori del server, %"
    entry  $frm.ent_errors_count_threshold -textvariable errors_count_threshold
    label $frm.lbl_processes -text "Processi attivi:"
    label $frm.lbl_lifetime_limit -text "Periodo di riavvio, sec."
    entry  $frm.ent_lifetime_limit -textvariable lifetime_limit
    label $frm.lbl_max_memory_size -text "Dimensione massima della memoria, KB"
    entry  $frm.ent_max_memory_size -textvariable max_memory_size
    label $frm.lbl_max_memory_time_limit -text "Intervallo di superamento della dimensione massima della memoria, sec."
    entry  $frm.ent_max_memory_time_limit -textvariable max_memory_time_limit
    label $frm.lbl_kill_problem_processes -justify left -anchor nw -text "Terminare forzatamente i processi problematici"
    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
    # ridefiniamo il gestore
    .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
}

Confrontando il codice di queste procedure, la differenza è evidente ad occhio nudo, enfatizzerò il gestore del pulsante «Ok». In Tk, le proprietà degli elementi grafici possono essere ridefinite durante l'esecuzione del programma utilizzando l'opzione configure. Per esempio, il comando iniziale per mostrare il pulsante:

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

Ma nelle nostre maschere, il comando dipende dalla funzionalità richiesta:

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

Nell'esempio sopra, il pulsante «attiva» la procedura di aggiunta del cluster.

Qui vale la pena fare una digressione sul lavoro con gli elementi grafici in Tk: per diversi elementi di input (entry, combobox, checkbutton, ecc.) è stato introdotto un parametro chiamato variabile di testo (textvariable):

entry  $frm.ent_lifetime_limit -textvariable lifetime_limit

Questa variabile è definita nello spazio globale e contiene il valore attualmente inserito. Cioè, per ottenere il testo inserito nel campo, basta leggere il valore della variabile corrispondente (ovviamente a condizione che essa sia definita al momento della creazione dell'elemento).

Un secondo metodo per ottenere il testo inserito (per elementi del tipo entry) è l'utilizzo del comando get:

.add.frm.ent_name get

Entrambi questi metodi possono essere visti nel codice sopra.

La pressione di questo pulsante, in questo caso, avvia la procedura RunCommand con la stringa di comando formata per l'aggiunta del cluster nei termini di 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

Siamo arrivati al comando principale, che gestisce l'avvio di rac con i parametri necessari e analizza l'output dei comandi in liste e restituisce, se richiesto:

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
    # apriamo il canale in modalità non bloccante
    # $rac - comando con percorso completo
    # $par - chiavi e opzioni di avvio formate
    set pipe [open "|$rac_cmd $par" "r"]
    try {
        set lst ""
        set l ""
        # aggiungiamo l'output del comando all'elenco delle liste
        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} {
        # avviare il gestore degli errori
        ErrorParcing $result $options
        return ""
    }
}

Dopo che i dati del server principale sono stati inseriti, verrà aggiunto all'albero; per questo, nel codice seguente della procedura Add:server:

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

Ora, facendo clic sul nome del server nell'albero, otterremo un elenco dei cluster gestiti da quel server, e cliccando sul cluster, otterremo un elenco degli elementi del cluster (server, basi di dati e così via). Questo è implementato nella procedura TreePress (file lib/function.tcl):

proc TreePress {tree} {
   global host server active_cluster infobase
   # definiamo l'elemento selezionato
    set id  [$tree selection]
   # impostiamo le variabili globali necessarie
    SetGlobalVarFromTreeItems $tree $id
   # definiamo chiave e valore, cioè il tipo di elemento selezionato
    set values [$tree item $id -values]
    set key [lindex [split $id "::"] 0]
   # e a seconda di cosa si è selezionato, verrà chiamata la procedura appropriata
   # nello spazio dei nomi Run
    Run::$key $tree $host $values
}

Di conseguenza, per il server principale verrà avviato Run::server (per il cluster — Run::cluster, per il server di lavoro — Run::work_server e così via). Cioè, il valore della variabile $key è una parte del nome dell'elemento dell'albero, definita dall'opzione -id.

Notiamo la procedura

Run::server

proc Run::server {tree host values} {
    # otteniamo l'elenco dei cluster per il server richiesto
    set lst [RunCommand server::$host "cluster list $host"]
    if {$lst eq ""} {return}
    set l [lindex $lst 0]
    #puts $lst
    # rimuoviamo i dati superflui dall'elenco
    .frm_work.tree_work delete  [ .frm_work.tree_work children {}]
    # leggiamo l'elenco
    foreach cluster_list $lst {
        # Riempie l'elenco con i valori ottenuti
        InsertItemsWorkList $cluster_list
        # gestiamo l'output (elenco) per aggiungere i dati all'albero
        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]]
            }
        }
    }
    # aggiungiamo i cluster all'albero
    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"
            # aggiungiamo elementi al cluster
            InsertClusterItems $tree $id
        }
    }
    if { [$tree exists "agent_admins::$id"] == 0 } {
        $tree insert "server::$host" end -id "agent_admins::$id" -text "Amministratori" -values "$id"
        #InsertClusterItems $tree $id
    }
}

Questa procedura elabora ciò che è stato ricevuto dal server tramite il comando RunCommand e aggiunge vari elementi all'albero: cluster, vari elementi radice (database, server di lavoro, sessioni e così via). Se osservate attentamente, potrete notare la chiamata alla procedura InsertItemsWorkList. Questa viene utilizzata per aggiungere elementi alla lista grafica, elaborando l'output dell'utility da console rac, che era precedentemente restituito sotto forma di elenco nella variabile $lst. Questo è un elenco di elenchi, contenente coppie di elementi separate da due punti.

Ad esempio, l'elenco delle connessioni del 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

Visivamente, questo apparirebbe in questo modo:

Scriviamo GUI per 1C RAC, o di nuovo su Tcl/Tk

La procedura sopra menzionata estrae i nomi degli elementi per l'intestazione e i dati per riempire la tabella:

InsertItemsWorkList

proc InsertItemsWorkList {lst} {
    global work_list_row_count
    # impostazione dell'alternanza dei colori per la riga
    if [expr $work_list_row_count % 2] {
        set tag dark
    } else {
        set tag light
    }
    # analisi delle righe in coppie chiave - valore
    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]
        }
    }
     # riempimento della tabella
    .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
    # impostazione delle intestazioni
    foreach j $column_list {
        .frm_work.tree_work heading $j -text $j
    }
    incr work_list_row_count
}

Qui invece di un semplice comando [split $str «:»], che divide la stringa in elementi separati da «:» e restituisce un elenco, è stata applicata un'espressione regolare, poiché alcuni elementi contengono anch'essi i due punti.

La procedura InsertClusterItems (una delle diverse simili) aggiunge semplicemente all'albero l'elemento cluster richiesto con l'elenco di elementi figli e gli identificatori corrispondenti.
InsertClusterItems

proc InsertClusterItems {tree id} {
    set parent "cluster::$id"
    $tree insert $parent end -id "infobases::$id" -text "Basi di dati informative" -values "$id"
    $tree insert $parent end -id "servers::$id" -text "Server di lavoro" -values "$id"
    $tree insert $parent end -id "admins::$id" -text "Amministratori" -values "$id"
    $tree insert $parent end -id "managers::$id" -text "Manager di cluster" -values $id
    $tree insert $parent end -id "processes::$id" -text "Processi di lavoro" -values "workprocess-all"
    $tree insert $parent end -id "sessions::$id" -text "Sessioni" -values "sessions-all"
    $tree insert $parent end -id "locks::$id" -text "Lock" -values "blocks-all"
    $tree insert $parent end -id "connections::$id" -text "Connessioni" -values "connections-all"
    $tree insert $parent end -id "profiles::$id" -text "Profili di sicurezza" -values $id
}

Si possono considerare altri due modi di implementare una procedura simile, dove sarà chiaramente visibile come ottimizzare e liberarsi di comandi ripetitivi:

In questa procedura, l'aggiunta e la verifica sono state risolte in modo diretto:

InsertBaseItems

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

E qui l'approccio è più corretto:

InsertProfileItems

proc InsertProfileItems {tree id} {
    set parent "profile::$id"
    set lst {
        {dir "Cataloghi virtuali"}
        {com "Classi COM autorizzate"}
        {addin "Componenti esterni"}
        {module "Report e elaborazioni esterni"}
        {app "Applicazioni autorizzate"}
        {inet "Risorse internet"}
    }
    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 
    }
}

La differenza tra di loro sta nell'applicazione del ciclo, in cui viene eseguita la commanda ripetitiva (comandi). Quale approccio applicare è a discrezione dello sviluppatore.

Abbiamo trattato l'aggiunta di elementi e l'ottenimento di dati, è ora di soffermarci sulla modifica. Poiché, in generale, per la modifica e l'aggiunta vengono utilizzati gli stessi parametri (l'informazione base è un'eccezione), anche i moduli di dialogo sono gli stessi. L'algoritmo per richiamare le procedure per l'aggiunta è il seguente:

Add::$key->AddToplevel

E per la modifica così:

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

Per esempio, prendiamo la modifica di un cluster, ovvero cliccando sul nome del cluster nell'albero, premiamo il pulsante di modifica sulla barra degli strumenti (matita) e verrà visualizzato il modulo corrispondente:

Scriviamo GUI per 1C RAC, o di nuovo su 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 ""
    }
    # disegniamo il modulo per il cluster
    set frm [Add::cluster $tree $host $values]
    # cambiamo il testo sulla label
    $frm configure -text "Modifica cluster"
    
    set active_cluster $values
    # otteniamo i dati per il cluster selezionato
    set lst [RunCommand cluster::$values "cluster info --cluster=$active_cluster $host"]
    # riempiamo i campi
    FormFieldsDataInsert $frm $lst
    # disabilitiamo i campi che non possono essere modificati
    $frm.ent_host configure -state disable
    $frm.ent_port configure -state disable
    # riproponiamo il gestore
    .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
    }
}

Dai commenti nel codice, in linea di massima, è tutto chiaro, tranne per il fatto che il codice del gestore del pulsante è stato ridefinito e c'è una procedura FormFieldsDataInsert che riempie i campi con i dati e inizializza le variabili:

FormFieldsDataInsert

proc FormFieldsDataInsert {frm lst} {
    foreach i [lindex $lst 0] {
        # otteniamo l'elenco dei parametri e dei valori
        if [regexp -nocase -all -- {(D+)(s*?|)(:)(s*?|)(.*)} $i match param v2 v3 v4 value] {
            # cambiamo i simboli
            regsub -all -- "-" [string trim $param] "_" entry_name
            # riempiamo con i dati
            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 """]
            }
            # per i checkbox cambiamo i valori
            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 questa procedura si è rivelato un ulteriore vantaggio di tcl: i nomi delle variabili vengono sostituiti con i valori di altre variabili. Cioè, per automatizzare la compilazione dei moduli e l'inizializzazione delle variabili, i nomi dei campi e delle variabili corrispondono alle chiavi della riga di comando dell'utilità rac e ai nomi dei parametri di output dei comandi, con alcune eccezioni: il trattino è sostituito con un underscore. Ad esempio scheduled-jobs-deny corrisponde al campo ent_scheduled_jobs_deny e alla variabile scheduled_jobs_deny.

Le maschere per l'aggiunta e la modifica possono differire nella composizione dei campi, ad esempio, lavorando con la base informativa:

Aggiunta IB

Scriviamo GUI per 1C RAC, o di nuovo su Tcl/Tk

Modifica IB

Scriviamo GUI per 1C RAC, o di nuovo su Tcl/Tk

Nella procedura di modifica Edit::infobase vengono aggiunti al modulo i campi necessari, il codice è ampio quindi non lo riporto qui.

Analogamente sono state implementate procedure per l'aggiunta, modifica e cancellazione anche per gli altri elementi.

Poiché il funzionamento dell'utilità implica un numero illimitato di server, cluster, basi informative ecc., sono state introdotte diverse variabili globali che si impostano ad ogni clic sugli elementi dell'albero. Cioè, la procedura attraversa ricorsivamente tutti gli elementi genitori e imposta le variabili:

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

Il cluster 1C consente di lavorare sia con l'autenticazione sia senza. Esistono due tipi di amministratori: amministratore dell'agente del cluster e amministratore del cluster. Di conseguenza, per un funzionamento corretto, sono state introdotte altre 4 variabili globali che contengono il login e la password dell'amministratore. Cioè, se nel cluster è presente un account amministratore, verrà visualizzata una finestra di dialogo per l'inserimento del login e della password, i dati saranno salvati in memoria e verranno utilizzati in ogni comando per il relativo cluster.

Ciò è gestito dalla procedura di gestione degli errori

ErrorParcing

proc ErrorParcing {err opt} {
    global cluster_user cluster_pwd agent_user agent_pwd
        switch -regexp -- $err {
        "L'amministratore del cluster non è autenticato" {
            AuthorisationDialog "Amministratore del cluster"
            .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
        }
        "L'amministratore del server centrale non è autenticato" {
            AuthorisationDialog "Amministratore dell'agente del cluster"
            .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 "Amministratore del cluster"
            .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 "Amministratore dell'agente del cluster"
            .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"
        }
    }
}

Cioè, a seconda di ciò che restituisce il comando, ci sarà una reazione corrispondente.

Attualmente, la funzionalità è realizzata al 95%, resta da implementare il lavoro con i profili di sicurezza e testare =). Questo è tutto. Mi scuso per la narrazione confusa.

Il codice, come da tradizione, è disponibile. qui.

Aggiornamento: Ho completato il lavoro con i profili di sicurezza. Ora la funzionalità è realizzata al 100%.

Aggiornamento 2: aggiunta la localizzazione in inglese e russo, verificato il funzionamento in win7
Scriviamo GUI per 1C RAC, o di nuovo su Tcl/Tk

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster