forked from Mirrors/gomuks
249 lines
7.3 KiB
Go
249 lines
7.3 KiB
Go
// gomuks - A Matrix client written in Go.
|
|
// Copyright (C) 2024 Tulir Asokan
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package gomuks
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/rs/zerolog"
|
|
"go.mau.fi/util/dbutil"
|
|
"go.mau.fi/util/exerrors"
|
|
"go.mau.fi/util/exzerolog"
|
|
"golang.org/x/net/http2"
|
|
|
|
"go.mau.fi/gomuks/pkg/hicli"
|
|
)
|
|
|
|
type Gomuks struct {
|
|
Log *zerolog.Logger
|
|
Server *http.Server
|
|
Client *hicli.HiClient
|
|
|
|
Version string
|
|
Commit string
|
|
LinkifiedVersion string
|
|
BuildTime time.Time
|
|
|
|
ConfigDir string
|
|
DataDir string
|
|
CacheDir string
|
|
TempDir string
|
|
LogDir string
|
|
|
|
FrontendFS embed.FS
|
|
indexWithETag []byte
|
|
frontendETag string
|
|
|
|
Config Config
|
|
DisableAuth bool
|
|
|
|
stopOnce sync.Once
|
|
stopChan chan struct{}
|
|
|
|
EventBuffer *EventBuffer
|
|
}
|
|
|
|
func NewGomuks() *Gomuks {
|
|
return &Gomuks{
|
|
stopChan: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (gmx *Gomuks) InitDirectories() {
|
|
// We need 4 directories: config, data, cache, logs
|
|
//
|
|
// 1. If GOMUKS_ROOT is set, all directories are created under that.
|
|
// 2. If GOMUKS_*_HOME is set, that value is used as the directory.
|
|
// 3. Use system-specific defaults as below
|
|
//
|
|
// *nix:
|
|
// - Config: $XDG_CONFIG_HOME/gomuks or $HOME/.config/gomuks
|
|
// - Data: $XDG_DATA_HOME/gomuks or $HOME/.local/share/gomuks
|
|
// - Cache: $XDG_CACHE_HOME/gomuks or $HOME/.cache/gomuks
|
|
// - Logs: $XDG_STATE_HOME/gomuks or $HOME/.local/state/gomuks
|
|
//
|
|
// Windows:
|
|
// - Config and Data: %AppData%\gomuks
|
|
// - Cache: %LocalAppData%\gomuks
|
|
// - Logs: %LocalAppData%\gomuks\logs
|
|
//
|
|
// macOS:
|
|
// - Config and Data: $HOME/Library/Application Support/gomuks
|
|
// - Cache: $HOME/Library/Caches/gomuks
|
|
// - Logs: $HOME/Library/Logs/gomuks
|
|
if gomuksRoot := os.Getenv("GOMUKS_ROOT"); gomuksRoot != "" {
|
|
exerrors.PanicIfNotNil(os.MkdirAll(gomuksRoot, 0700))
|
|
gmx.CacheDir = filepath.Join(gomuksRoot, "cache")
|
|
gmx.ConfigDir = filepath.Join(gomuksRoot, "config")
|
|
gmx.DataDir = filepath.Join(gomuksRoot, "data")
|
|
gmx.LogDir = filepath.Join(gomuksRoot, "logs")
|
|
} else {
|
|
homeDir := exerrors.Must(os.UserHomeDir())
|
|
if cacheDir := os.Getenv("GOMUKS_CACHE_HOME"); cacheDir != "" {
|
|
gmx.CacheDir = cacheDir
|
|
} else {
|
|
gmx.CacheDir = filepath.Join(exerrors.Must(os.UserCacheDir()), "gomuks")
|
|
}
|
|
if configDir := os.Getenv("GOMUKS_CONFIG_HOME"); configDir != "" {
|
|
gmx.ConfigDir = configDir
|
|
} else {
|
|
gmx.ConfigDir = filepath.Join(exerrors.Must(os.UserConfigDir()), "gomuks")
|
|
}
|
|
if dataDir := os.Getenv("GOMUKS_DATA_HOME"); dataDir != "" {
|
|
gmx.DataDir = dataDir
|
|
} else if dataDir = os.Getenv("XDG_DATA_HOME"); dataDir != "" {
|
|
gmx.DataDir = filepath.Join(dataDir, "gomuks")
|
|
} else if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
|
|
gmx.DataDir = gmx.ConfigDir
|
|
} else {
|
|
gmx.DataDir = filepath.Join(homeDir, ".local", "share", "gomuks")
|
|
}
|
|
if logDir := os.Getenv("GOMUKS_LOGS_HOME"); logDir != "" {
|
|
gmx.LogDir = logDir
|
|
} else if logDir = os.Getenv("XDG_STATE_HOME"); logDir != "" {
|
|
gmx.LogDir = filepath.Join(logDir, "gomuks")
|
|
} else if runtime.GOOS == "darwin" {
|
|
gmx.LogDir = filepath.Join(homeDir, "Library", "Logs", "gomuks")
|
|
} else if runtime.GOOS == "windows" {
|
|
gmx.LogDir = filepath.Join(gmx.CacheDir, "logs")
|
|
} else {
|
|
gmx.LogDir = filepath.Join(homeDir, ".local", "state", "gomuks")
|
|
}
|
|
}
|
|
if gmx.TempDir = os.Getenv("GOMUKS_TMPDIR"); gmx.TempDir == "" {
|
|
gmx.TempDir = filepath.Join(gmx.CacheDir, "tmp")
|
|
}
|
|
exerrors.PanicIfNotNil(os.MkdirAll(gmx.ConfigDir, 0700))
|
|
exerrors.PanicIfNotNil(os.MkdirAll(gmx.CacheDir, 0700))
|
|
exerrors.PanicIfNotNil(os.MkdirAll(gmx.TempDir, 0700))
|
|
exerrors.PanicIfNotNil(os.MkdirAll(gmx.DataDir, 0700))
|
|
exerrors.PanicIfNotNil(os.MkdirAll(gmx.LogDir, 0700))
|
|
defaultFileWriter.FileConfig.Filename = filepath.Join(gmx.LogDir, "gomuks.log")
|
|
}
|
|
|
|
func (gmx *Gomuks) SetupLog() {
|
|
gmx.Log = exerrors.Must(gmx.Config.Logging.Compile())
|
|
exzerolog.SetupDefaults(gmx.Log)
|
|
}
|
|
|
|
func (gmx *Gomuks) StartClient() {
|
|
hicli.HTMLSanitizerImgSrcTemplate = "_gomuks/media/%s/%s?encrypted=false"
|
|
rawDB, err := dbutil.NewFromConfig("gomuks", dbutil.Config{
|
|
PoolConfig: dbutil.PoolConfig{
|
|
Type: "sqlite3-fk-wal",
|
|
URI: fmt.Sprintf("file:%s/gomuks.db?_txlock=immediate", gmx.DataDir),
|
|
MaxOpenConns: 5,
|
|
MaxIdleConns: 1,
|
|
},
|
|
}, dbutil.ZeroLogger(gmx.Log.With().Str("component", "hicli").Str("db_section", "main").Logger()))
|
|
if err != nil {
|
|
gmx.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to open database")
|
|
os.Exit(10)
|
|
}
|
|
ctx := gmx.Log.WithContext(context.Background())
|
|
gmx.Client = hicli.New(
|
|
rawDB,
|
|
nil,
|
|
gmx.Log.With().Str("component", "hicli").Logger(),
|
|
[]byte("meow"),
|
|
gmx.EventBuffer.HicliEventHandler,
|
|
)
|
|
gmx.Client.LogoutFunc = gmx.Logout
|
|
httpClient := gmx.Client.Client.Client
|
|
httpClient.Transport.(*http.Transport).ForceAttemptHTTP2 = false
|
|
if !gmx.Config.Matrix.DisableHTTP2 {
|
|
h2, err := http2.ConfigureTransports(httpClient.Transport.(*http.Transport))
|
|
if err != nil {
|
|
gmx.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to configure HTTP/2")
|
|
os.Exit(13)
|
|
}
|
|
h2.ReadIdleTimeout = 30 * time.Second
|
|
}
|
|
userID, err := gmx.Client.DB.Account.GetFirstUserID(ctx)
|
|
if err != nil {
|
|
gmx.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to get first user ID")
|
|
os.Exit(11)
|
|
}
|
|
err = gmx.Client.Start(ctx, userID, nil)
|
|
if err != nil {
|
|
gmx.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to start client")
|
|
os.Exit(12)
|
|
}
|
|
gmx.Log.Info().Stringer("user_id", userID).Msg("Client started")
|
|
}
|
|
|
|
func (gmx *Gomuks) Stop() {
|
|
gmx.stopOnce.Do(func() {
|
|
close(gmx.stopChan)
|
|
})
|
|
}
|
|
|
|
func (gmx *Gomuks) WaitForInterrupt() {
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
select {
|
|
case <-c:
|
|
case <-gmx.stopChan:
|
|
}
|
|
}
|
|
|
|
func (gmx *Gomuks) DirectStop() {
|
|
for _, closer := range gmx.EventBuffer.GetClosers() {
|
|
closer(websocket.StatusServiceRestart, "Server shutting down")
|
|
}
|
|
gmx.Client.Stop()
|
|
if gmx.Server != nil {
|
|
err := gmx.Server.Close()
|
|
if err != nil {
|
|
gmx.Log.Error().Err(err).Msg("Failed to close server")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (gmx *Gomuks) Run() {
|
|
gmx.InitDirectories()
|
|
err := gmx.LoadConfig()
|
|
if err != nil {
|
|
_, _ = fmt.Fprintln(os.Stderr, "Failed to load config:", err)
|
|
os.Exit(9)
|
|
}
|
|
gmx.SetupLog()
|
|
gmx.Log.Info().
|
|
Str("version", gmx.Version).
|
|
Str("go_version", runtime.Version()).
|
|
Time("built_at", gmx.BuildTime).
|
|
Msg("Initializing gomuks")
|
|
gmx.StartServer()
|
|
gmx.StartClient()
|
|
gmx.Log.Info().Msg("Initialization complete")
|
|
gmx.WaitForInterrupt()
|
|
gmx.Log.Info().Msg("Shutting down...")
|
|
gmx.DirectStop()
|
|
gmx.Log.Info().Msg("Shutdown complete")
|
|
os.Exit(0)
|
|
}
|