549 lines
15 KiB
Go
549 lines
15 KiB
Go
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"encoding/json"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
_ "image/png"
|
|||
|
|
"io"
|
|||
|
|
"os"
|
|||
|
|
"path/filepath"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"fyne.io/fyne/v2"
|
|||
|
|
"fyne.io/fyne/v2/app"
|
|||
|
|
"fyne.io/fyne/v2/container"
|
|||
|
|
"fyne.io/fyne/v2/dialog"
|
|||
|
|
"fyne.io/fyne/v2/driver/desktop"
|
|||
|
|
"fyne.io/fyne/v2/widget"
|
|||
|
|
|
|||
|
|
_ "embed"
|
|||
|
|
|
|||
|
|
"github.com/emersion/go-autostart"
|
|||
|
|
"github.com/gofrs/flock"
|
|||
|
|
"github.com/gregorybednov/lbc/cli"
|
|||
|
|
toml "github.com/pelletier/go-toml/v2"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
//go:embed icon.png
|
|||
|
|
var iconPNG []byte
|
|||
|
|
|
|||
|
|
func appIcon() fyne.Resource {
|
|||
|
|
return fyne.NewStaticResource("icon.png", iconPNG)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* --------------------------- Конфиг лончера --------------------------- */
|
|||
|
|
|
|||
|
|
type LauncherCfg struct {
|
|||
|
|
FirstRun bool `json:"firstRun"`
|
|||
|
|
StartMinimized bool `json:"startMinimized"`
|
|||
|
|
AutoStart bool `json:"autoStart"`
|
|||
|
|
NodeHome string `json:"nodeHome"` // каталог узла (HOME)
|
|||
|
|
LastStarted string `json:"lastStarted"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type AppState struct {
|
|||
|
|
cfgPath string
|
|||
|
|
cfg LauncherCfg
|
|||
|
|
lockF *flock.Flock
|
|||
|
|
|
|||
|
|
// UI
|
|||
|
|
app fyne.App
|
|||
|
|
win fyne.Window
|
|||
|
|
statusLbl *widget.Label
|
|||
|
|
logArea *widget.Entry
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* --------------------------- TOML-модель (минимум) --------------------------- */
|
|||
|
|
|
|||
|
|
type ConfigTOML struct {
|
|||
|
|
Abci string `toml:"abci"`
|
|||
|
|
DBBackend string `toml:"db_backend"`
|
|||
|
|
DBDir string `toml:"db_dir"`
|
|||
|
|
FilterPeers bool `toml:"filter_peers"`
|
|||
|
|
GenesisFile string `toml:"genesis_file"`
|
|||
|
|
LogFormat string `toml:"log_format"`
|
|||
|
|
LogLevel string `toml:"log_level"`
|
|||
|
|
Moniker string `toml:"moniker"`
|
|||
|
|
NodeKeyFile string `toml:"node_key_file"`
|
|||
|
|
P2P struct {
|
|||
|
|
AddrBookFile string `toml:"addr_book_file"`
|
|||
|
|
AddrBookStrict bool `toml:"addr_book_strict"`
|
|||
|
|
AllowDuplicateIP bool `toml:"allow_duplicate_ip"`
|
|||
|
|
BootstrapPeers string `toml:"bootstrap_peers"`
|
|||
|
|
ExternalAddress string `toml:"external_address"`
|
|||
|
|
Laddr string `toml:"laddr"`
|
|||
|
|
PersistentPeers string `toml:"persistent_peers"`
|
|||
|
|
QueueType string `toml:"queue_type"`
|
|||
|
|
UPnP bool `toml:"upnp"`
|
|||
|
|
UseLegacy bool `toml:"use_legacy"`
|
|||
|
|
} `toml:"p2p"`
|
|||
|
|
PrivValidator struct {
|
|||
|
|
KeyFile string `toml:"key_file"`
|
|||
|
|
StateFile string `toml:"state_file"`
|
|||
|
|
} `toml:"priv_validator"`
|
|||
|
|
Ygg struct {
|
|||
|
|
AdminListen string `toml:"admin_listen"`
|
|||
|
|
AllowedPubKeys []string `toml:"allowed_public_keys"`
|
|||
|
|
Peers string `toml:"peers"`
|
|||
|
|
PrivateKeyFile string `toml:"private_key_file"`
|
|||
|
|
} `toml:"yggdrasil"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type uiWriter struct{ st *AppState }
|
|||
|
|
|
|||
|
|
func (w *uiWriter) Write(p []byte) (int, error) {
|
|||
|
|
w.st.appendLog(string(p))
|
|||
|
|
return len(p), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* -------------------------------- main -------------------------------- */
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
st := &AppState{}
|
|||
|
|
must(initConfig(st))
|
|||
|
|
must(acquireSingleInstanceLock(st))
|
|||
|
|
defer releaseSingleInstanceLock(st)
|
|||
|
|
|
|||
|
|
exe, _ := os.Executable()
|
|||
|
|
exe, _ = filepath.EvalSymlinks(exe)
|
|||
|
|
autoApp := &autostart.App{
|
|||
|
|
Name: "LBClient",
|
|||
|
|
DisplayName: "LBClient",
|
|||
|
|
Exec: []string{exe},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
a := app.New()
|
|||
|
|
w := a.NewWindow("LBС Launcher")
|
|||
|
|
st.app, st.win = a, w
|
|||
|
|
|
|||
|
|
// Статус + логи
|
|||
|
|
st.statusLbl = widget.NewLabel("Готов")
|
|||
|
|
st.logArea = widget.NewMultiLineEntry()
|
|||
|
|
st.logArea.SetPlaceHolder("Здесь будет вывод команд lbc…")
|
|||
|
|
st.logArea.Disable() // только для чтения
|
|||
|
|
|
|||
|
|
homeEntry := widget.NewEntry()
|
|||
|
|
homeEntry.SetPlaceHolder("Каталог HOME узла (будут data/, config/)")
|
|||
|
|
homeEntry.SetText(st.cfg.NodeHome)
|
|||
|
|
btnPickHome := widget.NewButton("Выбрать HOME…", func() {
|
|||
|
|
dd := dialog.NewFolderOpen(func(lu fyne.ListableURI, err error) {
|
|||
|
|
if lu == nil || err != nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
st.cfg.NodeHome = lu.Path()
|
|||
|
|
homeEntry.SetText(st.cfg.NodeHome)
|
|||
|
|
_ = saveConfig(st)
|
|||
|
|
}, w)
|
|||
|
|
dd.Show()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
topControls := container.NewVBox(
|
|||
|
|
widget.NewLabelWithStyle("HOME:", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|||
|
|
homeEntry, btnPickHome,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Вкладка: Init genesis
|
|||
|
|
initGenesisBtn := widget.NewButton("Инициализировать новую сеть (init genesis)", func() {
|
|||
|
|
if !st.ensurePathsOk(homeEntry.Text) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
go func() {
|
|||
|
|
w := &uiWriter{st}
|
|||
|
|
if err := cli.ExecuteWithArgs([]string{"init", "genesis"}, homeEntry.Text, w, w); err != nil {
|
|||
|
|
st.alert("Ошибка: " + err.Error())
|
|||
|
|
} else {
|
|||
|
|
st.notify("init genesis: готово")
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
tabInitGenesis := container.NewBorder(nil, nil, nil, nil,
|
|||
|
|
container.NewVBox(
|
|||
|
|
widget.NewLabel("Создать новую блокчейн-сеть"),
|
|||
|
|
widget.NewLabel("Команда: lbc init genesis"),
|
|||
|
|
initGenesisBtn,
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Вкладка: Export genesis.json
|
|||
|
|
exportBtn := widget.NewButton("Экспортировать genesis.json…", func() {
|
|||
|
|
// genesis лежит в HOME/config/genesis.json
|
|||
|
|
if homeEntry.Text == "" {
|
|||
|
|
st.alert("Сначала укажи HOME каталóg узла.")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
src := filepath.Join(homeEntry.Text, "config", "genesis.json")
|
|||
|
|
if _, err := os.Stat(src); err != nil {
|
|||
|
|
st.alert("Не найден файл: " + src)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
fd := dialog.NewFileSave(func(uw fyne.URIWriteCloser, err error) {
|
|||
|
|
if uw == nil || err != nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
defer uw.Close()
|
|||
|
|
f, e := os.Open(src)
|
|||
|
|
if e != nil {
|
|||
|
|
st.alert("Ошибка чтения genesis: " + e.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
defer f.Close()
|
|||
|
|
if _, e = io.Copy(uw, f); e != nil {
|
|||
|
|
st.alert("Ошибка записи: " + e.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
st.notify("Готово: экспортирован genesis.json")
|
|||
|
|
}, w)
|
|||
|
|
fd.SetFileName("genesis.json")
|
|||
|
|
fd.Show()
|
|||
|
|
})
|
|||
|
|
tabExportGenesis := container.NewVBox(
|
|||
|
|
widget.NewLabel("Экспорт genesis.json из текущего узла"),
|
|||
|
|
exportBtn,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Вкладка: Init joiner
|
|||
|
|
genesisPathEntry := widget.NewEntry()
|
|||
|
|
genesisPathEntry.SetPlaceHolder("Путь к существующему genesis.json")
|
|||
|
|
pickGenesis := widget.NewButton("Выбрать genesis.json…", func() {
|
|||
|
|
fd := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
|||
|
|
if uc == nil || err != nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
genesisPathEntry.SetText(uc.URI().Path())
|
|||
|
|
}, w)
|
|||
|
|
fd.Show()
|
|||
|
|
})
|
|||
|
|
joinBtn := widget.NewButton("Создать узел-присоединение (init joiner)", func() {
|
|||
|
|
if !st.ensurePathsOk(homeEntry.Text) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
// 1) скопировать genesis.json в HOME/config/genesis.json
|
|||
|
|
src := genesisPathEntry.Text
|
|||
|
|
if src == "" {
|
|||
|
|
st.alert("Укажи путь к genesis.json")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
dest := filepath.Join(homeEntry.Text, "config")
|
|||
|
|
_ = os.MkdirAll(dest, 0o755)
|
|||
|
|
destFile := filepath.Join(dest, "genesis.json")
|
|||
|
|
if err := fileCopy(src, destFile); err != nil {
|
|||
|
|
st.alert("Не удалось скопировать genesis.json: " + err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
// 2) lbc init joiner
|
|||
|
|
go func() {
|
|||
|
|
w := &uiWriter{st}
|
|||
|
|
if err := cli.ExecuteWithArgs([]string{"init", "joiner", genesisPathEntry.Text}, homeEntry.Text, w, w); err != nil {
|
|||
|
|
st.alert("Ошибка: " + err.Error())
|
|||
|
|
} else {
|
|||
|
|
st.notify("init joiner: готово")
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
})
|
|||
|
|
tabJoiner := container.NewVBox(
|
|||
|
|
widget.NewLabel("Создать ноду в существующей сети"),
|
|||
|
|
container.NewHBox(genesisPathEntry, pickGenesis),
|
|||
|
|
joinBtn,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Вкладка: Редактор config.toml (минималка)
|
|||
|
|
// Читаем/пишем HOME/config/config.toml
|
|||
|
|
cfgPathLbl := widget.NewLabel("Файл: (HOME)/config/config.toml")
|
|||
|
|
monikerEntry := widget.NewEntry()
|
|||
|
|
laddrEntry := widget.NewEntry()
|
|||
|
|
ppeersEntry := widget.NewEntry()
|
|||
|
|
logLevelSel := widget.NewSelect([]string{"debug", "info", "warn", "error"}, nil)
|
|||
|
|
|
|||
|
|
loadCfgBtn := widget.NewButton("Загрузить config.toml", func() {
|
|||
|
|
if homeEntry.Text == "" {
|
|||
|
|
st.alert("Укажи HOME узла.")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
cfgp := filepath.Join(homeEntry.Text, "config", "config.toml")
|
|||
|
|
if cfg, err := loadTOML(cfgp); err != nil {
|
|||
|
|
st.alert("Чтение TOML: " + err.Error())
|
|||
|
|
} else {
|
|||
|
|
cfgPathLbl.SetText("Файл: " + cfgp)
|
|||
|
|
monikerEntry.SetText(cfg.Moniker)
|
|||
|
|
laddrEntry.SetText(cfg.P2P.Laddr)
|
|||
|
|
ppeersEntry.SetText(cfg.P2P.PersistentPeers)
|
|||
|
|
if cfg.LogLevel == "" {
|
|||
|
|
cfg.LogLevel = "info"
|
|||
|
|
}
|
|||
|
|
logLevelSel.SetSelected(cfg.LogLevel)
|
|||
|
|
st.status("config.toml загружен")
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
saveCfgBtn := widget.NewButton("Сохранить", func() {
|
|||
|
|
if homeEntry.Text == "" {
|
|||
|
|
st.alert("Укажи HOME узла.")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
cfgp := filepath.Join(homeEntry.Text, "config", "config.toml")
|
|||
|
|
cfg, err := loadTOML(cfgp) // мягко читаем всё, меняем только интересующие поля
|
|||
|
|
if err != nil {
|
|||
|
|
st.alert("Чтение TOML: " + err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
cfg.Moniker = monikerEntry.Text
|
|||
|
|
cfg.P2P.Laddr = laddrEntry.Text
|
|||
|
|
cfg.P2P.PersistentPeers = ppeersEntry.Text
|
|||
|
|
cfg.LogLevel = logLevelSel.Selected
|
|||
|
|
if err := saveTOML(cfgp, cfg); err != nil {
|
|||
|
|
st.alert("Запись TOML: " + err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
st.notify("config.toml сохранён")
|
|||
|
|
})
|
|||
|
|
tabCfg := container.NewVBox(
|
|||
|
|
widget.NewLabel("Редактирование основных параметров config.toml"),
|
|||
|
|
cfgPathLbl,
|
|||
|
|
widget.NewForm(
|
|||
|
|
widget.NewFormItem("moniker", monikerEntry),
|
|||
|
|
widget.NewFormItem("p2p.laddr", laddrEntry),
|
|||
|
|
widget.NewFormItem("p2p.persistent_peers", ppeersEntry),
|
|||
|
|
widget.NewFormItem("log_level", logLevelSel),
|
|||
|
|
),
|
|||
|
|
container.NewHBox(loadCfgBtn, saveCfgBtn),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Вкладка: Логи
|
|||
|
|
clearLog := widget.NewButton("Очистить лог", func() { st.logArea.SetText("") })
|
|||
|
|
tabLogs := container.NewBorder(nil, clearLog, nil, nil, st.logArea)
|
|||
|
|
|
|||
|
|
// Собираем UI
|
|||
|
|
tabs := container.NewAppTabs(
|
|||
|
|
container.NewTabItem("Init genesis", tabInitGenesis),
|
|||
|
|
container.NewTabItem("Export genesis", tabExportGenesis),
|
|||
|
|
container.NewTabItem("Init joiner", tabJoiner),
|
|||
|
|
container.NewTabItem("Config.toml", tabCfg),
|
|||
|
|
container.NewTabItem("Logs", tabLogs),
|
|||
|
|
)
|
|||
|
|
tabs.SetTabLocation(container.TabLocationTop)
|
|||
|
|
|
|||
|
|
cbAutostart := widget.NewCheck("Запускать при входе в систему", func(v bool) {
|
|||
|
|
if v {
|
|||
|
|
if err := autoApp.Enable(); err != nil {
|
|||
|
|
st.alert("Автозапуск: " + err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
_ = autoApp.Disable()
|
|||
|
|
}
|
|||
|
|
st.cfg.AutoStart = v
|
|||
|
|
_ = saveConfig(st)
|
|||
|
|
})
|
|||
|
|
cbAutostart.SetChecked(autoApp.IsEnabled())
|
|||
|
|
|
|||
|
|
cbStartMin := widget.NewCheck("Стартовать свернутым в трей", func(v bool) {
|
|||
|
|
st.cfg.StartMinimized = v
|
|||
|
|
_ = saveConfig(st)
|
|||
|
|
})
|
|||
|
|
cbStartMin.SetChecked(true)
|
|||
|
|
|
|||
|
|
top := container.NewVBox(topControls, container.NewHBox(cbAutostart, cbStartMin), st.statusLbl)
|
|||
|
|
content := container.NewBorder(top, nil, nil, nil, tabs)
|
|||
|
|
w.SetContent(content)
|
|||
|
|
|
|||
|
|
// Закрытие = скрытие (фон)
|
|||
|
|
w.SetCloseIntercept(func() { w.Hide(); st.notify("Лончер работает в фоне") })
|
|||
|
|
|
|||
|
|
// Трей-меню
|
|||
|
|
if desk, ok := a.(desktop.App); ok {
|
|||
|
|
desk.SetSystemTrayIcon(appIcon())
|
|||
|
|
openItem := fyne.NewMenuItem("Открыть", func() { w.Show(); w.RequestFocus() })
|
|||
|
|
autoItem := fyne.NewMenuItem("Автозапуск", nil)
|
|||
|
|
autoItem.Checked = autoApp.IsEnabled()
|
|||
|
|
autoItem.Action = func() {
|
|||
|
|
ns := !autoItem.Checked
|
|||
|
|
if ns {
|
|||
|
|
if err := autoApp.Enable(); err != nil {
|
|||
|
|
st.alert(err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
_ = autoApp.Disable()
|
|||
|
|
}
|
|||
|
|
autoItem.Checked = ns
|
|||
|
|
st.cfg.AutoStart = ns
|
|||
|
|
_ = saveConfig(st)
|
|||
|
|
}
|
|||
|
|
quitItem := fyne.NewMenuItem("Выйти", func() { releaseSingleInstanceLock(st); a.Quit() })
|
|||
|
|
desk.SetSystemTrayMenu(fyne.NewMenu("", openItem, autoItem, fyne.NewMenuItemSeparator(), quitItem))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Первый запуск / показ окна
|
|||
|
|
now := time.Now().Format(time.RFC3339)
|
|||
|
|
if st.cfg.FirstRun {
|
|||
|
|
st.notify("Первый запуск: лончер будет работать в фоне")
|
|||
|
|
st.cfg.FirstRun = false
|
|||
|
|
}
|
|||
|
|
st.cfg.LastStarted = now
|
|||
|
|
_ = saveConfig(st)
|
|||
|
|
|
|||
|
|
if st.cfg.StartMinimized {
|
|||
|
|
w.Hide()
|
|||
|
|
} else {
|
|||
|
|
w.Show()
|
|||
|
|
}
|
|||
|
|
a.Run()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ------------------------------ Конфиг/локи ------------------------------ */
|
|||
|
|
|
|||
|
|
func initConfig(st *AppState) error {
|
|||
|
|
dir, err := os.UserConfigDir()
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
appDir := filepath.Join(dir, "lbc_launcher")
|
|||
|
|
if err := os.MkdirAll(appDir, 0o755); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
st.cfgPath = filepath.Join(appDir, "config.json")
|
|||
|
|
// defaults
|
|||
|
|
st.cfg = LauncherCfg{
|
|||
|
|
FirstRun: true,
|
|||
|
|
StartMinimized: true,
|
|||
|
|
AutoStart: false,
|
|||
|
|
NodeHome: "",
|
|||
|
|
}
|
|||
|
|
f, err := os.Open(st.cfgPath)
|
|||
|
|
if errors.Is(err, os.ErrNotExist) {
|
|||
|
|
return saveConfig(st)
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
defer f.Close()
|
|||
|
|
data, _ := io.ReadAll(f)
|
|||
|
|
_ = json.Unmarshal(data, &st.cfg)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func saveConfig(st *AppState) error {
|
|||
|
|
tmp := st.cfgPath + ".tmp"
|
|||
|
|
b, _ := json.MarshalIndent(st.cfg, "", " ")
|
|||
|
|
if err := os.WriteFile(tmp, b, 0o644); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return os.Rename(tmp, st.cfgPath)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func acquireSingleInstanceLock(st *AppState) error {
|
|||
|
|
cache, err := os.UserCacheDir()
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
lp := filepath.Join(cache, "lbc_launcher", "app.lock")
|
|||
|
|
if err := os.MkdirAll(filepath.Dir(lp), 0o755); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
l := flock.New(lp)
|
|||
|
|
ok, err := l.TryLock()
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("lock error: %w", err)
|
|||
|
|
}
|
|||
|
|
if !ok {
|
|||
|
|
return errors.New("another instance is running")
|
|||
|
|
}
|
|||
|
|
st.lockF = l
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
func releaseSingleInstanceLock(st *AppState) {
|
|||
|
|
if st.lockF != nil {
|
|||
|
|
_ = st.lockF.Unlock()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ------------------------------ TOML I/O ------------------------------ */
|
|||
|
|
|
|||
|
|
func loadTOML(path string) (*ConfigTOML, error) {
|
|||
|
|
f, err := os.Open(path)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
defer f.Close()
|
|||
|
|
var cfg ConfigTOML
|
|||
|
|
dec := toml.NewDecoder(f)
|
|||
|
|
if err := dec.Decode(&cfg); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return &cfg, nil
|
|||
|
|
}
|
|||
|
|
func saveTOML(path string, cfg *ConfigTOML) error {
|
|||
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
tmp := path + ".tmp"
|
|||
|
|
f, err := os.Create(tmp)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
enc := toml.NewEncoder(f)
|
|||
|
|
if err := enc.Encode(cfg); err != nil {
|
|||
|
|
_ = f.Close()
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
_ = f.Close()
|
|||
|
|
return os.Rename(tmp, path)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ------------------------------ Утилиты/UI ------------------------------ */
|
|||
|
|
|
|||
|
|
func (st *AppState) status(s string) {
|
|||
|
|
st.statusLbl.SetText(s)
|
|||
|
|
}
|
|||
|
|
func (st *AppState) appendLog(s string) {
|
|||
|
|
go func() {
|
|||
|
|
st.win.SetContent(st.win.Content()) // гарантируем вызов из UI потока
|
|||
|
|
st.logArea.SetText(st.logArea.Text + s)
|
|||
|
|
st.logArea.CursorRow = 999999
|
|||
|
|
}()
|
|||
|
|
}
|
|||
|
|
func (st *AppState) alert(msg string) {
|
|||
|
|
dialog.ShowInformation("Сообщение", msg, st.win)
|
|||
|
|
}
|
|||
|
|
func (st *AppState) notify(msg string) {
|
|||
|
|
st.app.SendNotification(&fyne.Notification{Title: "LBClient", Content: msg})
|
|||
|
|
}
|
|||
|
|
func (st *AppState) ensurePathsOk(home string) bool {
|
|||
|
|
if home == "" {
|
|||
|
|
st.alert("Укажи HOME каталог узла")
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
st.cfg.NodeHome = home
|
|||
|
|
_ = saveConfig(st)
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
func fileCopy(src, dst string) error {
|
|||
|
|
in, err := os.Open(src)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
defer in.Close()
|
|||
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
out, err := os.Create(dst)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
defer out.Close()
|
|||
|
|
if _, err = io.Copy(out, in); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return out.Close()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* --------------------------------- misc -------------------------------- */
|
|||
|
|
|
|||
|
|
func must(err error) {
|
|||
|
|
if err != nil {
|
|||
|
|
fmt.Fprintln(os.Stderr, err)
|
|||
|
|
os.Exit(1)
|
|||
|
|
}
|
|||
|
|
}
|