Display progress in UI
This commit is contained in:
parent
00a57489b5
commit
4c3004e113
@ -1,6 +1,6 @@
|
||||
# omc
|
||||
# omq
|
||||
|
||||
Oracle Multi Querier (omc) - программа для выгрузки результатов SQL-запроса с нескольких серверов Oracle.
|
||||
Oracle Multi Querier (omq) - программа для выгрузки результатов SQL-запроса с нескольких серверов Oracle.
|
||||
|
||||
![Скриншот главного окна](doc/main-window.png)
|
||||
|
||||
|
2
go.mod
2
go.mod
@ -4,6 +4,7 @@ go 1.21.3
|
||||
|
||||
require (
|
||||
github.com/dimchansky/utfbom v1.1.1
|
||||
github.com/gdamore/tcell/v2 v2.6.0
|
||||
github.com/rivo/tview v0.0.0-20231115183240-7c9e464bac02
|
||||
github.com/sijms/go-ora/v2 v2.7.21
|
||||
github.com/xuri/excelize/v2 v2.8.0
|
||||
@ -13,7 +14,6 @@ require (
|
||||
|
||||
require (
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.6.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
|
122
kernel.go
122
kernel.go
@ -14,7 +14,6 @@ import (
|
||||
|
||||
"github.com/dimchansky/utfbom"
|
||||
go_ora "github.com/sijms/go-ora/v2"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
type Row struct {
|
||||
@ -22,13 +21,34 @@ type Row struct {
|
||||
data []any
|
||||
}
|
||||
|
||||
func launch(configFilePath, scriptFilePath string, exportFileFormat ExportFormat, encoding Encoding) error {
|
||||
sqlBytes, err := readFileIgnoreBOM(scriptFilePath)
|
||||
type Job struct {
|
||||
configFilePath string
|
||||
scriptFilePath string
|
||||
exportFileFormat ExportFormat
|
||||
encoding Encoding
|
||||
|
||||
servers []Server
|
||||
status string
|
||||
|
||||
isFinished bool
|
||||
}
|
||||
|
||||
func (j *Job) init() error {
|
||||
j.status = "Чтение списка серверов..."
|
||||
|
||||
servers, err := loadConfig(j.configFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
servers, err := loadConfig(configFilePath)
|
||||
j.servers = servers
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *Job) launch() error {
|
||||
j.status = "Чтение файла SQL-скрипта..."
|
||||
sqlBytes, err := readFileIgnoreBOM(j.scriptFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -38,9 +58,9 @@ func launch(configFilePath, scriptFilePath string, exportFileFormat ExportFormat
|
||||
return fmt.Errorf("Некорректное значение номера поля филиала: %v", branchFieldNum)
|
||||
}
|
||||
|
||||
rowsChan := iterateServers(servers, string(sqlBytes), branchFieldNum)
|
||||
rowsChan := j.iterateServers(string(sqlBytes), branchFieldNum)
|
||||
|
||||
err = export(scriptFilePath, exportFileFormat, encoding, rowsChan)
|
||||
err = j.export(rowsChan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -48,13 +68,13 @@ func launch(configFilePath, scriptFilePath string, exportFileFormat ExportFormat
|
||||
return nil
|
||||
}
|
||||
|
||||
func export(scriptFilePath string, exportFileFormat ExportFormat, encoding Encoding, inputRows chan Row) error {
|
||||
converter, err := exportFileFormat.GetExporter(encoding)
|
||||
func (j *Job) export(inputRows chan Row) error {
|
||||
converter, err := j.exportFileFormat.GetExporter(j.encoding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileName := filepath.Base(scriptFilePath)
|
||||
fileName := filepath.Base(j.scriptFilePath)
|
||||
fileName = strings.TrimSuffix(fileName, filepath.Ext(fileName))
|
||||
|
||||
outputRows := make(chan []any)
|
||||
@ -81,50 +101,60 @@ func export(scriptFilePath string, exportFileFormat ExportFormat, encoding Encod
|
||||
outputRows <- cachedRow
|
||||
}
|
||||
} else {
|
||||
rowsCache = append(rowsCache, row.data)
|
||||
rowsCache = append(rowsCache, row.data) // store data row in cache until got a header row
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return converter.Convert(fileName, outputRows)
|
||||
err = converter.Convert(fileName, outputRows)
|
||||
j.status = "ЗАВЕРШЕНО."
|
||||
j.isFinished = true
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func iterateServers(servers []Server, sqlStr string, branchFieldNum int) chan Row {
|
||||
func (j *Job) iterateServers(sqlStr string, branchFieldNum int) chan Row {
|
||||
rowsChan := make(chan Row)
|
||||
|
||||
slog.Info("Выгрузка начата...")
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
close(rowsChan)
|
||||
}()
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(len(servers))
|
||||
wg.Add(len(j.servers))
|
||||
|
||||
for i, server := range servers {
|
||||
for i, server := range j.servers {
|
||||
i := i
|
||||
server := server
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
db, err := sql.Open("oracle", server.Url)
|
||||
j.servers[i].Status = "Подключение к серверу"
|
||||
db := sql.OpenDB(go_ora.NewConnector(server.Url))
|
||||
defer db.Close()
|
||||
|
||||
err := db.Ping()
|
||||
if err != nil {
|
||||
slog.Error("Ошибка подключения к серверу", slog.String("server", server.Url), slog.Any("err", err))
|
||||
j.servers[i].Error = err
|
||||
return
|
||||
}
|
||||
|
||||
j.servers[i].Status = "Выполнение SQL-запроса"
|
||||
rows, err := db.Query(sqlStr)
|
||||
if err != nil {
|
||||
slog.Error("Ошибка выполнения запроса", slog.String("server", server.Url), slog.Any("err", err))
|
||||
j.servers[i].Error = err
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
cols, err := rows.Columns()
|
||||
if err != nil {
|
||||
slog.Error("Ошибка получения списка колонок", slog.String("server", server.Url), slog.Any("err", err))
|
||||
j.servers[i].Error = err
|
||||
return
|
||||
}
|
||||
|
||||
@ -134,6 +164,8 @@ func iterateServers(servers []Server, sqlStr string, branchFieldNum int) chan Ro
|
||||
|
||||
rowNum := 0
|
||||
for rows.Next() {
|
||||
j.servers[i].Status = fmt.Sprintf("Выгружено %d строк", rowNum)
|
||||
|
||||
pointers := make([]any, len(cols))
|
||||
container := make([]any, len(cols))
|
||||
for i := range pointers {
|
||||
@ -142,66 +174,22 @@ func iterateServers(servers []Server, sqlStr string, branchFieldNum int) chan Ro
|
||||
|
||||
err = rows.Scan(pointers...)
|
||||
if err != nil {
|
||||
slog.Error("Ошибка получения строки", slog.String("server", server.Url), slog.Any("err", err))
|
||||
j.servers[i].Error = err
|
||||
break
|
||||
}
|
||||
rowsChan <- Row{isHeader: false, data: append(append(container[:branchFieldNum-1], server.Name), container[branchFieldNum:]...)} // Добавление имени сервера
|
||||
rowNum += 1
|
||||
}
|
||||
slog.Info("Получение строк завершено", slog.String("server", server.Name), slog.Int("rowCount", rowNum))
|
||||
j.servers[i].Status = fmt.Sprintf("ЗАВЕРШЕНО: Выгружено %d строк.", rowNum)
|
||||
}()
|
||||
}
|
||||
j.status = "Ожидание завершения работы с серверами"
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
return rowsChan
|
||||
}
|
||||
|
||||
// loadConfig считывает конфиг и возвращает список параметров серверов
|
||||
func loadConfig(filePath string) ([]Server, error) {
|
||||
servers := make([]Server, 0)
|
||||
|
||||
iniBytes, err := readFileIgnoreBOM(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := ini.Load(iniBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg.DeleteSection("DEFAULT")
|
||||
|
||||
for _, server := range cfg.SectionStrings() {
|
||||
loginKey, err := cfg.Section(server).GetKey("Login")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passwordKey, err := cfg.Section(server).GetKey("Password")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nameKey, err := cfg.Section(server).GetKey("Name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serv := strings.Split(server, "/")[0]
|
||||
service := strings.Split(server, "/")[1]
|
||||
dbUrl := go_ora.BuildUrl(serv, 1521, service, loginKey.String(), passwordKey.String(), nil)
|
||||
|
||||
server := Server{
|
||||
Url: dbUrl,
|
||||
Name: nameKey.String()}
|
||||
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
func sliceToAnySlice[T string | any](slice []T) []any {
|
||||
result := make([]any, len(slice))
|
||||
for i := range result {
|
||||
|
157
main.go
157
main.go
@ -1,12 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
@ -16,28 +17,17 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
configFilePath, scriptFilePath, exportFileFormat, encoding, err := getReportParams()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
err = launch(configFilePath, scriptFilePath, exportFileFormat, encoding)
|
||||
if err != nil {
|
||||
slog.Error("Ошибка при выполнении", slog.Any("err", err))
|
||||
fmt.Scanln()
|
||||
return
|
||||
}
|
||||
renderUI()
|
||||
}
|
||||
|
||||
func getReportParams() (configFilePath, scriptFilePath string, exportFileFormat ExportFormat, encoding Encoding, err error) {
|
||||
exportFileFormatStr := ""
|
||||
|
||||
// Список файлов с SQL-скриптами
|
||||
func renderUI() {
|
||||
// SQL-files list
|
||||
files, _ := filepath.Glob(filepath.Join(SQL_FILES_DIR, "*.sql"))
|
||||
for i := range files {
|
||||
files[i] = filepath.Base(files[i])
|
||||
}
|
||||
// Список файлов с настройками подключения к БД
|
||||
|
||||
// Servers file list
|
||||
configs, _ := filepath.Glob("*.ini")
|
||||
|
||||
configFileDropDown := tview.NewDropDown().
|
||||
@ -76,19 +66,136 @@ func getReportParams() (configFilePath, scriptFilePath string, exportFileFormat
|
||||
grid := tview.NewGrid().SetRows(-1, 1).
|
||||
AddItem(form, 0, 0, 1, 1, 0, 0, true).
|
||||
AddItem(tview.NewButton("Запуск").SetSelectedFunc(func() {
|
||||
_, scriptFilePath = sqlFileDropDown.GetCurrentOption()
|
||||
_, exportFileFormatStr = formatDropDown.GetCurrentOption()
|
||||
_, configFilePath = configFileDropDown.GetCurrentOption()
|
||||
_, scriptFilePath := sqlFileDropDown.GetCurrentOption()
|
||||
_, exportFileFormatStr := formatDropDown.GetCurrentOption()
|
||||
_, configFilePath := configFileDropDown.GetCurrentOption()
|
||||
_, encodingStr := encodingDropDown.GetCurrentOption()
|
||||
encoding = Encoding(encodingStr)
|
||||
app.Stop()
|
||||
|
||||
job := &Job{
|
||||
configFilePath: configFilePath,
|
||||
scriptFilePath: filepath.Join(SQL_FILES_DIR, scriptFilePath),
|
||||
exportFileFormat: ExportFormat(exportFileFormatStr),
|
||||
encoding: Encoding(encodingStr)}
|
||||
|
||||
err := job.init()
|
||||
if err != nil {
|
||||
showError(app, err)
|
||||
return
|
||||
}
|
||||
|
||||
renderProgressUI(app, job)
|
||||
}), 1, 0, 1, 1, 0, 0, false)
|
||||
grid.SetBorderPadding(0, 1, 1, 1)
|
||||
|
||||
form.SetTitle("Параметры выгрузки").SetTitleAlign(tview.AlignLeft)
|
||||
if err := app.SetRoot(grid, true).EnableMouse(true).Run(); err != nil {
|
||||
return "", "", "", "", err
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func renderProgressUI(app *tview.Application, job *Job) {
|
||||
statusTextViews := make([]*tview.TextView, 0)
|
||||
maxServerNameLen := 0
|
||||
for i := range job.servers {
|
||||
if len(job.servers[i].Name) > maxServerNameLen {
|
||||
maxServerNameLen = len([]rune(job.servers[i].Name)) + 2
|
||||
}
|
||||
}
|
||||
rows := make([]int, len(job.servers)+3)
|
||||
for i := range rows {
|
||||
rows[i] = 1
|
||||
}
|
||||
|
||||
return configFilePath, filepath.Join(SQL_FILES_DIR, scriptFilePath), ExportFormat(exportFileFormatStr), encoding, nil
|
||||
grid := tview.NewGrid().SetRows(rows...).SetColumns(1, maxServerNameLen+2)
|
||||
for i, server := range job.servers {
|
||||
grid.AddItem(tview.NewTextView().SetTextAlign(tview.AlignLeft).SetText(server.Name), i+1, 1, 1, 1, 0, 0, false)
|
||||
|
||||
p := tview.NewTextView().SetTextAlign(tview.AlignLeft).SetText("...")
|
||||
statusTextViews = append(statusTextViews, p)
|
||||
grid.AddItem(p, i+1, 2, 1, 1, 0, 0, false)
|
||||
}
|
||||
|
||||
grid.AddItem(tview.NewTextView().SetTextAlign(tview.AlignLeft).SetText(strings.Repeat("-", maxServerNameLen)), len(job.servers)+1, 1, 1, 1, 0, 0, false)
|
||||
grid.AddItem(tview.NewTextView().SetTextAlign(tview.AlignLeft), len(job.servers)+1, 2, 1, 1, 0, 0, false)
|
||||
|
||||
statusTextView := tview.NewTextView()
|
||||
grid.AddItem(statusTextView, len(job.servers)+2, 2, 1, 1, 0, 0, false)
|
||||
|
||||
// Last empty line
|
||||
grid.AddItem(tview.NewTextView(), len(job.servers)+3, 1, 1, 1, 0, 0, false)
|
||||
grid.AddItem(tview.NewTextView(), len(job.servers)+3, 2, 1, 1, 0, 0, false)
|
||||
|
||||
go func() {
|
||||
oneMoreUpdate := false
|
||||
for !job.isFinished || oneMoreUpdate {
|
||||
if !oneMoreUpdate {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
for i, server := range job.servers {
|
||||
s := []string{server.Status}
|
||||
if server.Error != nil {
|
||||
s = append(s, server.Error.Error())
|
||||
}
|
||||
statusTextViews[i].SetText(strings.Join(s, ": "))
|
||||
|
||||
if server.Error != nil {
|
||||
statusTextViews[i].SetTextColor(tcell.ColorRed)
|
||||
} else if strings.HasPrefix(server.Status, "ЗАВЕРШЕНО: ") {
|
||||
statusTextViews[i].SetTextColor(tcell.ColorGreen)
|
||||
}
|
||||
}
|
||||
|
||||
statusTextView.SetText(job.status)
|
||||
if strings.HasPrefix(job.status, "ЗАВЕРШЕНО") {
|
||||
statusTextView.SetTextColor(tcell.ColorGreen)
|
||||
}
|
||||
|
||||
app.ForceDraw()
|
||||
if job.isFinished {
|
||||
if !oneMoreUpdate {
|
||||
oneMoreUpdate = true
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO:
|
||||
/*finishButton := tview.NewButton("OK").SetSelectedFunc(func() { app.Stop() }).SetBackgroundColor(tcell.ColorDarkGreen)
|
||||
grid.AddItem(finishButton, len(job.servers)+2, 1, 1, 1, 0, 0, false)
|
||||
app.ForceDraw().SetFocus(finishButton)*/
|
||||
showMessage(app, "Выгрузка завершена.")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
err := job.launch()
|
||||
if err != nil {
|
||||
showError(app, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := app.SetRoot(grid, true).SetFocus(grid).Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func showError(app *tview.Application, err error) {
|
||||
modal := tview.NewModal().
|
||||
SetText(err.Error()).
|
||||
AddButtons([]string{"OK"}).
|
||||
SetBackgroundColor(tcell.ColorRed).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
os.Exit(1)
|
||||
})
|
||||
app.SetRoot(modal, true).SetFocus(modal)
|
||||
}
|
||||
|
||||
func showMessage(app *tview.Application, text string) {
|
||||
modal := tview.NewModal().
|
||||
SetText(text).
|
||||
AddButtons([]string{"OK"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
os.Exit(0)
|
||||
})
|
||||
app.SetRoot(modal, true).SetFocus(modal).ForceDraw()
|
||||
}
|
||||
|
59
servers.go
59
servers.go
@ -1,9 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
go_ora "github.com/sijms/go-ora/v2"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// Server - экземпляр сервера
|
||||
type Server struct {
|
||||
// Полная ссылка на БД, вкючая логин/пароль
|
||||
Url string
|
||||
|
||||
// Наименование филиала
|
||||
Name string
|
||||
|
||||
// Статус работы с сервером
|
||||
Status string
|
||||
|
||||
// Ошибка работы с сервером
|
||||
Error error
|
||||
}
|
||||
|
||||
// loadConfig считывает конфиг и возвращает список параметров серверов
|
||||
func loadConfig(filePath string) ([]Server, error) {
|
||||
servers := make([]Server, 0)
|
||||
|
||||
iniBytes, err := readFileIgnoreBOM(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := ini.Load(iniBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg.DeleteSection("DEFAULT")
|
||||
|
||||
for _, server := range cfg.SectionStrings() {
|
||||
loginKey, err := cfg.Section(server).GetKey("Login")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passwordKey, err := cfg.Section(server).GetKey("Password")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nameKey, err := cfg.Section(server).GetKey("Name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serv := strings.Split(server, "/")[0]
|
||||
service := strings.Split(server, "/")[1]
|
||||
dbUrl := go_ora.BuildUrl(serv, 1521, service, loginKey.String(), passwordKey.String(), map[string]string{"TIMEOUT": "0", "PREFETCH_ROWS": "1000"})
|
||||
|
||||
server := Server{
|
||||
Url: dbUrl,
|
||||
Name: nameKey.String()}
|
||||
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user