Allow server to make backups
This commit is contained in:
parent
aeceae11d5
commit
b3a827e042
27 changed files with 891 additions and 418 deletions
134
cmd/backup.go
134
cmd/backup.go
|
|
@ -1,18 +1,11 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/component/snapshots"
|
"github.com/FAU-CDI/wisski-distillery/internal/component/snapshots"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/core"
|
"github.com/FAU-CDI/wisski-distillery/internal/core"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/logging"
|
"github.com/FAU-CDI/wisski-distillery/pkg/logging"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/targz"
|
|
||||||
"github.com/tkw1536/goprogram/exit"
|
"github.com/tkw1536/goprogram/exit"
|
||||||
"github.com/tkw1536/goprogram/status"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Backup is the 'backup' command
|
// Backup is the 'backup' command
|
||||||
|
|
@ -53,13 +46,13 @@ func (bk backupC) Run(context wisski_distillery.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// do the handling
|
// do the handling
|
||||||
err := handleSnapshotLike(context, SnapshotFlags{
|
err := dis.SnapshotManager().HandleSnapshotLike(context.IOStream, snapshots.SnapshotFlags{
|
||||||
Dest: bk.Positionals.Dest,
|
Dest: bk.Positionals.Dest,
|
||||||
Slug: "",
|
Slug: "",
|
||||||
Title: "Backup",
|
Title: "Backup",
|
||||||
StagingOnly: bk.StagingOnly,
|
StagingOnly: bk.StagingOnly,
|
||||||
|
|
||||||
Do: func(dest string) SnapshotLike {
|
Do: func(dest string) snapshots.SnapshotLike {
|
||||||
backup := dis.SnapshotManager().NewBackup(context.IOStream, snapshots.BackupDescription{
|
backup := dis.SnapshotManager().NewBackup(context.IOStream, snapshots.BackupDescription{
|
||||||
Dest: dest,
|
Dest: dest,
|
||||||
ConcurrentSnapshots: bk.ConcurrentSnapshots,
|
ConcurrentSnapshots: bk.ConcurrentSnapshots,
|
||||||
|
|
@ -73,126 +66,3 @@ func (bk backupC) Run(context wisski_distillery.Context) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnapshotFlags struct {
|
|
||||||
Dest string
|
|
||||||
Slug string
|
|
||||||
Title string // "Backup" or "Snapshot"
|
|
||||||
StagingOnly bool
|
|
||||||
|
|
||||||
Do func(dest string) SnapshotLike
|
|
||||||
}
|
|
||||||
|
|
||||||
type SnapshotLike interface {
|
|
||||||
LogEntry() models.Snapshot
|
|
||||||
Report(w io.Writer) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSnapshotLike(context wisski_distillery.Context, flags SnapshotFlags) (err error) {
|
|
||||||
dis := context.Environment
|
|
||||||
|
|
||||||
// determine target paths
|
|
||||||
logging.LogMessage(context.IOStream, "Determining target paths")
|
|
||||||
var stagingDir, archivePath string
|
|
||||||
if flags.StagingOnly {
|
|
||||||
stagingDir = flags.Dest
|
|
||||||
} else {
|
|
||||||
archivePath = flags.Dest
|
|
||||||
}
|
|
||||||
if stagingDir == "" {
|
|
||||||
stagingDir, err = dis.SnapshotManager().NewStagingDir(flags.Slug)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !flags.StagingOnly && archivePath == "" {
|
|
||||||
archivePath = dis.SnapshotManager().NewArchivePath(flags.Slug)
|
|
||||||
}
|
|
||||||
context.Printf("Staging Directory: %s\n", stagingDir)
|
|
||||||
context.Printf("Archive Path: %s\n", archivePath)
|
|
||||||
|
|
||||||
// create the staging directory
|
|
||||||
logging.LogMessage(context.IOStream, "Creating staging directory")
|
|
||||||
err = dis.Core.Environment.Mkdir(stagingDir, environment.DefaultDirPerm)
|
|
||||||
if !environment.IsExist(err) && err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it was requested to not do staging only
|
|
||||||
// we need the staging directory to be deleted at the end
|
|
||||||
if !flags.StagingOnly {
|
|
||||||
defer func() {
|
|
||||||
logging.LogMessage(context.IOStream, "Removing staging directory")
|
|
||||||
dis.Environment.RemoveAll(stagingDir)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the actual snapshot or backup
|
|
||||||
// write out the report
|
|
||||||
// and retain a log entry
|
|
||||||
var entry models.Snapshot
|
|
||||||
logging.LogOperation(func() error {
|
|
||||||
// do the snapshot!
|
|
||||||
sl := flags.Do(stagingDir)
|
|
||||||
|
|
||||||
// create a log entry
|
|
||||||
entry = sl.LogEntry()
|
|
||||||
|
|
||||||
// find the report path
|
|
||||||
reportPath := filepath.Join(stagingDir, "report.txt")
|
|
||||||
context.Println(reportPath)
|
|
||||||
|
|
||||||
// create the path
|
|
||||||
report, err := dis.Environment.Create(reportPath, environment.DefaultFilePerm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// and write out the report
|
|
||||||
{
|
|
||||||
_, err := sl.Report(report)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}, context.IOStream, "Generating %s", flags.Title)
|
|
||||||
|
|
||||||
// if we only requested staging
|
|
||||||
// all that is left is to write the log entry
|
|
||||||
if flags.StagingOnly {
|
|
||||||
logging.LogMessage(context.IOStream, "Writing Log Entry")
|
|
||||||
|
|
||||||
// write out the log entry
|
|
||||||
entry.Path = stagingDir
|
|
||||||
entry.Packed = false
|
|
||||||
dis.Instances().AddSnapshotLog(entry)
|
|
||||||
|
|
||||||
context.Printf("Wrote %s\n", stagingDir)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// package everything up as an archive!
|
|
||||||
if err := logging.LogOperation(func() error {
|
|
||||||
var count int64
|
|
||||||
defer func() { context.Printf("Wrote %d byte(s) to %s\n", count, archivePath) }()
|
|
||||||
|
|
||||||
st := status.NewWithCompat(context.Stdout, 1)
|
|
||||||
st.Start()
|
|
||||||
defer st.Stop()
|
|
||||||
|
|
||||||
count, err = targz.Package(dis.Core.Environment, archivePath, stagingDir, func(dst, src string) {
|
|
||||||
st.Set(0, dst)
|
|
||||||
})
|
|
||||||
|
|
||||||
return err
|
|
||||||
}, context.IOStream, "Writing archive"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// write out the log entry
|
|
||||||
logging.LogMessage(context.IOStream, "Writing Log Entry")
|
|
||||||
entry.Path = archivePath
|
|
||||||
entry.Packed = true
|
|
||||||
dis.Instances().AddSnapshotLog(entry)
|
|
||||||
|
|
||||||
// and we're done!
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,13 @@ func (bi snapshot) Run(context wisski_distillery.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// do a snapshot of it!
|
// do a snapshot of it!
|
||||||
err = handleSnapshotLike(context, SnapshotFlags{
|
err = dis.SnapshotManager().HandleSnapshotLike(context.IOStream, snapshots.SnapshotFlags{
|
||||||
Dest: bi.Positionals.Dest,
|
Dest: bi.Positionals.Dest,
|
||||||
Slug: bi.Positionals.Slug,
|
Slug: bi.Positionals.Slug,
|
||||||
Title: "Snapshot",
|
Title: "Snapshot",
|
||||||
StagingOnly: bi.StagingOnly,
|
StagingOnly: bi.StagingOnly,
|
||||||
|
|
||||||
Do: func(dest string) SnapshotLike {
|
Do: func(dest string) snapshots.SnapshotLike {
|
||||||
snapshot := dis.SnapshotManager().NewSnapshot(instance, context.IOStream, snapshots.SnapshotDescription{
|
snapshot := dis.SnapshotManager().NewSnapshot(instance, context.IOStream, snapshots.SnapshotDescription{
|
||||||
Dest: dest,
|
Dest: dest,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -8,8 +8,9 @@ require (
|
||||||
github.com/alessio/shellescape v1.4.1
|
github.com/alessio/shellescape v1.4.1
|
||||||
github.com/feiin/sqlstring v0.3.0
|
github.com/feiin/sqlstring v0.3.0
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/tkw1536/goprogram v0.1.0
|
github.com/tkw1536/goprogram v0.1.1
|
||||||
golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741
|
golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741
|
||||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0
|
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0
|
||||||
gorm.io/driver/mysql v1.3.6
|
gorm.io/driver/mysql v1.3.6
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -8,6 +8,8 @@ github.com/feiin/sqlstring v0.3.0 h1:iyPEFijI2BxpY2M+AuhIvdNManzXa2OwGzuPaEMLUgo
|
||||||
github.com/feiin/sqlstring v0.3.0/go.mod h1:xpZTjVUw1nD3hMgF9SMRdPiooKSikLf4PS5j2NTn3RI=
|
github.com/feiin/sqlstring v0.3.0/go.mod h1:xpZTjVUw1nD3hMgF9SMRdPiooKSikLf4PS5j2NTn3RI=
|
||||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
|
github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
|
||||||
github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI=
|
github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI=
|
||||||
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||||
|
|
@ -25,6 +27,8 @@ github.com/tkw1536/goprogram v0.0.17 h1:SAD/rHtxm7CTEdUV1a37LUyE6G0MdcKhLzO8fZ8c
|
||||||
github.com/tkw1536/goprogram v0.0.17/go.mod h1:Jqs0sTMzhrAGCX3JQrlEwQ0WRWQACCvuQQkaBDp65pE=
|
github.com/tkw1536/goprogram v0.0.17/go.mod h1:Jqs0sTMzhrAGCX3JQrlEwQ0WRWQACCvuQQkaBDp65pE=
|
||||||
github.com/tkw1536/goprogram v0.1.0 h1:cP+Z7VKgRF93JApsau1azKxGrH/nTwcGLnSYfZP9XjE=
|
github.com/tkw1536/goprogram v0.1.0 h1:cP+Z7VKgRF93JApsau1azKxGrH/nTwcGLnSYfZP9XjE=
|
||||||
github.com/tkw1536/goprogram v0.1.0/go.mod h1:Jqs0sTMzhrAGCX3JQrlEwQ0WRWQACCvuQQkaBDp65pE=
|
github.com/tkw1536/goprogram v0.1.0/go.mod h1:Jqs0sTMzhrAGCX3JQrlEwQ0WRWQACCvuQQkaBDp65pE=
|
||||||
|
github.com/tkw1536/goprogram v0.1.1 h1:gamK9OuRqoX2yQlA/nkgfVHHZWd/u2uUj6vJMYrYa70=
|
||||||
|
github.com/tkw1536/goprogram v0.1.1/go.mod h1:Jqs0sTMzhrAGCX3JQrlEwQ0WRWQACCvuQQkaBDp65pE=
|
||||||
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 h1:lNtcVz/3bOstm7Vebox+5m3nLh/BYWnhmc3AhXOW6oI=
|
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 h1:lNtcVz/3bOstm7Vebox+5m3nLh/BYWnhmc3AhXOW6oI=
|
||||||
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||||
golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741 h1:fGZugkZk2UgYBxtpKmvub51Yno1LJDeEsRp2xGD+0gY=
|
golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741 h1:fGZugkZk2UgYBxtpKmvub51Yno1LJDeEsRp2xGD+0gY=
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ services:
|
||||||
# TODO: Mount docker socket properly!
|
# TODO: Mount docker socket properly!
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
- "${CONFIG_PATH}:${CONFIG_PATH}:ro"
|
- "${CONFIG_PATH}:${CONFIG_PATH}:ro"
|
||||||
- "${DEPLOY_ROOT}:${DEPLOY_ROOT}:ro"
|
- "${DEPLOY_ROOT}:${DEPLOY_ROOT}:rw"
|
||||||
- "${GLOBAL_AUTHORIZED_KEYS_FILE}:${GLOBAL_AUTHORIZED_KEYS_FILE}:ro"
|
- "${GLOBAL_AUTHORIZED_KEYS_FILE}:${GLOBAL_AUTHORIZED_KEYS_FILE}:ro"
|
||||||
- "${SELF_OVERRIDES_FILE}:${SELF_OVERRIDES_FILE}:ro"
|
- "${SELF_OVERRIDES_FILE}:${SELF_OVERRIDES_FILE}:ro"
|
||||||
- "${SELF_RESOLVER_BLOCK_FILE}:${SELF_RESOLVER_BLOCK_FILE}:ro"
|
- "${SELF_RESOLVER_BLOCK_FILE}:${SELF_RESOLVER_BLOCK_FILE}:ro"
|
||||||
|
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
package control
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
_ "embed"
|
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/config"
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
|
||||||
"github.com/tkw1536/goprogram/stream"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Info struct {
|
|
||||||
component.ComponentBase
|
|
||||||
|
|
||||||
Instances *instances.Instances
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Info) Name() string { return "control-info" }
|
|
||||||
|
|
||||||
func (*Info) Routes() []string { return []string{"/dis/"} }
|
|
||||||
|
|
||||||
func (info *Info) Handler(route string, context context.Context, io stream.IOStream) (http.Handler, error) {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
|
||||||
// handle everything under /dis/!
|
|
||||||
mux.HandleFunc("/dis/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path == "/dis/" {
|
|
||||||
http.Redirect(w, r, "/dis/index", http.StatusTemporaryRedirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.NotFound(w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
// render everything
|
|
||||||
mux.Handle("/dis/index", httpx.HTMLHandler[disIndex]{
|
|
||||||
Handler: info.disIndex,
|
|
||||||
Template: indexTemplate,
|
|
||||||
})
|
|
||||||
|
|
||||||
mux.Handle("/dis/instance/", httpx.HTMLHandler[disInstance]{
|
|
||||||
Handler: info.disInstance,
|
|
||||||
Template: instanceTemplate,
|
|
||||||
})
|
|
||||||
|
|
||||||
// api -- for future usage
|
|
||||||
mux.Handle("/dis/api/v1/instance/get/", httpx.JSON(info.getinstance))
|
|
||||||
mux.Handle("/dis/api/v1/instance/all", httpx.JSON(info.allinstances))
|
|
||||||
|
|
||||||
// ensure that everyone is logged in!
|
|
||||||
return httpx.BasicAuth(mux, "WissKI Distillery Admin", func(user, pass string) bool {
|
|
||||||
return user == info.Config.DisAdminUser && pass == info.Config.DisAdminPassword
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// disIndex is the context of the "/dis/index" page
|
|
||||||
type disIndex struct {
|
|
||||||
Time time.Time
|
|
||||||
|
|
||||||
Config *config.Config
|
|
||||||
|
|
||||||
Instances []instances.WissKIInfo
|
|
||||||
|
|
||||||
TotalCount int
|
|
||||||
RunningCount int
|
|
||||||
StoppedCount int
|
|
||||||
|
|
||||||
Backups []models.Snapshot
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info *Info) disIndex(r *http.Request) (idx disIndex, err error) {
|
|
||||||
var group errgroup.Group
|
|
||||||
|
|
||||||
group.Go(func() error {
|
|
||||||
// load instances
|
|
||||||
idx.Instances, err = info.allinstances(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// count how many are running and how many are stopped
|
|
||||||
for _, i := range idx.Instances {
|
|
||||||
if i.Running {
|
|
||||||
idx.RunningCount++
|
|
||||||
} else {
|
|
||||||
idx.StoppedCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
idx.TotalCount = len(idx.Instances)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// get the log entries
|
|
||||||
group.Go(func() (err error) {
|
|
||||||
idx.Backups, err = info.Instances.SnapshotLogFor("")
|
|
||||||
return
|
|
||||||
})
|
|
||||||
|
|
||||||
// get the static properties
|
|
||||||
idx.Config = info.Config
|
|
||||||
|
|
||||||
// current time
|
|
||||||
idx.Time = time.Now().UTC()
|
|
||||||
|
|
||||||
// wait for everything!
|
|
||||||
group.Wait()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// disInstance is the context of the "/dis/instance/*" page
|
|
||||||
type disInstance struct {
|
|
||||||
Time time.Time
|
|
||||||
|
|
||||||
Instance models.Instance
|
|
||||||
Info instances.WissKIInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info *Info) disInstance(r *http.Request) (is disInstance, err error) {
|
|
||||||
// find the slug as the last component of path!
|
|
||||||
slug := strings.TrimSuffix(r.URL.Path, "/")
|
|
||||||
slug = slug[strings.LastIndex(slug, "/")+1:]
|
|
||||||
|
|
||||||
// find the instance itself!
|
|
||||||
instance, err := info.Instances.WissKI(slug)
|
|
||||||
if err == instances.ErrWissKINotFound {
|
|
||||||
return is, httpx.ErrNotFound
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return is, err
|
|
||||||
}
|
|
||||||
is.Instance = instance.Instance
|
|
||||||
|
|
||||||
// get some more info about the wisski
|
|
||||||
is.Info, err = instance.Info(false)
|
|
||||||
if err != nil {
|
|
||||||
return is, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// current time
|
|
||||||
is.Time = time.Now().UTC()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:embed "html/index.html"
|
|
||||||
var indexTemplateStr string
|
|
||||||
var indexTemplate = template.Must(template.New("index.html").Parse(indexTemplateStr))
|
|
||||||
|
|
||||||
//go:embed "html/instance.html"
|
|
||||||
var instanceTemplateString string
|
|
||||||
var instanceTemplate = template.Must(template.New("instance.html").Parse(instanceTemplateString))
|
|
||||||
|
|
||||||
func (info *Info) getinstance(r *http.Request) (iinfo instances.WissKIInfo, err error) {
|
|
||||||
// find the slug as the last component of path!
|
|
||||||
slug := strings.TrimSuffix(r.URL.Path, "/")
|
|
||||||
slug = slug[strings.LastIndex(slug, "/")+1:]
|
|
||||||
|
|
||||||
// load the wisski instance!
|
|
||||||
wisski, err := info.Instances.WissKI(strings.TrimSuffix(slug, "/"))
|
|
||||||
if err == instances.ErrWissKINotFound {
|
|
||||||
return iinfo, httpx.ErrNotFound
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return iinfo, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// get info about it!
|
|
||||||
return wisski.Info(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info *Info) allinstances(*http.Request) (infos []instances.WissKIInfo, err error) {
|
|
||||||
var errgroup errgroup.Group
|
|
||||||
|
|
||||||
// list all the instances
|
|
||||||
all, err := info.Instances.All()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all of their info!
|
|
||||||
infos = make([]instances.WissKIInfo, len(all))
|
|
||||||
for i, instance := range all {
|
|
||||||
{
|
|
||||||
i := i
|
|
||||||
instance := instance
|
|
||||||
|
|
||||||
errgroup.Go(func() (err error) {
|
|
||||||
infos[i], err = instance.Info(true)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for the results, and return
|
|
||||||
err = errgroup.Wait()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
@ -16,7 +16,7 @@ import (
|
||||||
|
|
||||||
func (home *Home) updateInstances(ctx context.Context, io stream.IOStream) {
|
func (home *Home) updateInstances(ctx context.Context, io stream.IOStream) {
|
||||||
timex.SetInterval(ctx, home.RefreshInterval, func(t time.Time) {
|
timex.SetInterval(ctx, home.RefreshInterval, func(t time.Time) {
|
||||||
io.Printf("[%s]: reloading instance list", t.String())
|
io.Printf("[%s]: reloading instance list\n", t.Format(time.Stamp))
|
||||||
|
|
||||||
names, _ := home.instanceMap()
|
names, _ := home.instanceMap()
|
||||||
home.instanceNames.Set(names)
|
home.instanceNames.Set(names)
|
||||||
|
|
@ -38,7 +38,7 @@ func (home *Home) instanceMap() (map[string]struct{}, error) {
|
||||||
|
|
||||||
func (home *Home) updateRender(ctx context.Context, io stream.IOStream) {
|
func (home *Home) updateRender(ctx context.Context, io stream.IOStream) {
|
||||||
timex.SetInterval(ctx, home.RefreshInterval, func(t time.Time) {
|
timex.SetInterval(ctx, home.RefreshInterval, func(t time.Time) {
|
||||||
io.Printf("[%s]: reloading home render", t.String())
|
io.Printf("[%s]: reloading home render\n", t.Format(time.Stamp))
|
||||||
|
|
||||||
bytes, _ := home.homeRender()
|
bytes, _ := home.homeRender()
|
||||||
home.homeBytes.Set(bytes)
|
home.homeBytes.Set(bytes)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import (
|
||||||
|
|
||||||
func (home *Home) updateRedirect(ctx context.Context, io stream.IOStream) {
|
func (home *Home) updateRedirect(ctx context.Context, io stream.IOStream) {
|
||||||
timex.SetInterval(ctx, home.RefreshInterval, func(t time.Time) {
|
timex.SetInterval(ctx, home.RefreshInterval, func(t time.Time) {
|
||||||
io.Printf("[%s]: reloading overrides", t.String())
|
io.Printf("[%s]: reloading overrides\n", t.Format(time.Stamp))
|
||||||
redirect, _ := home.loadRedirect()
|
redirect, _ := home.loadRedirect()
|
||||||
home.redirect.Set(&redirect)
|
home.redirect.Set(&redirect)
|
||||||
})
|
})
|
||||||
|
|
@ -95,6 +95,10 @@ func (redirect Redirect) Redirect(r *http.Request) string {
|
||||||
func (redirect Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (redirect Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
dest := redirect.Redirect(r)
|
dest := redirect.Redirect(r)
|
||||||
if dest == "" {
|
if dest == "" {
|
||||||
|
if redirect.Fallback == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
redirect.Fallback.ServeHTTP(w, r)
|
redirect.Fallback.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,11 @@
|
||||||
<hr />
|
<hr />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<button class="remote-action" data-action="snapshot" data-param="{{ .Instance.Slug }}" data-target="#snapshot" data-buffer="20">Take a snapshot</button>
|
||||||
|
<pre class="remote-action-out" id="snapshot"></pre>
|
||||||
|
</p>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
Generated at <code>{{ .Time }}</code>
|
Generated at <code>{{ .Time }}</code>
|
||||||
</footer>
|
</footer>
|
||||||
85
internal/component/info/index.go
Normal file
85
internal/component/info/index.go
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package info
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/config"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed "html/index.html"
|
||||||
|
var indexTemplateStr string
|
||||||
|
var indexTemplate = template.Must(template.New("index.html").Parse(indexTemplateStr))
|
||||||
|
|
||||||
|
type indexPageContext struct {
|
||||||
|
Time time.Time
|
||||||
|
|
||||||
|
Config *config.Config
|
||||||
|
|
||||||
|
Instances []instances.WissKIInfo
|
||||||
|
|
||||||
|
TotalCount int
|
||||||
|
RunningCount int
|
||||||
|
StoppedCount int
|
||||||
|
|
||||||
|
Backups []models.Snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info *Info) indexPageAPI(r *http.Request) (idx indexPageContext, err error) {
|
||||||
|
var group errgroup.Group
|
||||||
|
|
||||||
|
group.Go(func() error {
|
||||||
|
// list all the instances
|
||||||
|
all, err := info.Instances.All()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all of their info!
|
||||||
|
idx.Instances = make([]instances.WissKIInfo, len(all))
|
||||||
|
for i, instance := range all {
|
||||||
|
{
|
||||||
|
i := i
|
||||||
|
instance := instance
|
||||||
|
|
||||||
|
// store the info for this group!
|
||||||
|
group.Go(func() (err error) {
|
||||||
|
idx.Instances[i], err = instance.Info(true)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// get the log entries
|
||||||
|
group.Go(func() (err error) {
|
||||||
|
idx.Backups, err = info.Instances.SnapshotLogFor("")
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
// get the static properties
|
||||||
|
idx.Config = info.Config
|
||||||
|
idx.Time = time.Now().UTC()
|
||||||
|
|
||||||
|
group.Wait()
|
||||||
|
|
||||||
|
// count how many are running and how many are stopped
|
||||||
|
for _, i := range idx.Instances {
|
||||||
|
if i.Running {
|
||||||
|
idx.RunningCount++
|
||||||
|
} else {
|
||||||
|
idx.StoppedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx.TotalCount = len(idx.Instances)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
59
internal/component/info/info.go
Normal file
59
internal/component/info/info.go
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package info
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/component/snapshots"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Info struct {
|
||||||
|
component.ComponentBase
|
||||||
|
|
||||||
|
SnapshotManager *snapshots.Manager
|
||||||
|
Instances *instances.Instances
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Info) Name() string { return "control-info" }
|
||||||
|
|
||||||
|
func (*Info) Routes() []string { return []string{"/dis/"} }
|
||||||
|
|
||||||
|
func (info *Info) Handler(route string, context context.Context, io stream.IOStream) (http.Handler, error) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// handle everything
|
||||||
|
mux.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == route {
|
||||||
|
http.Redirect(w, r, route+"/index", http.StatusTemporaryRedirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.NotFound(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
// add a handler for the index page
|
||||||
|
mux.Handle(route+"index", httpx.HTMLHandler[indexPageContext]{
|
||||||
|
Handler: info.indexPageAPI,
|
||||||
|
Template: indexTemplate,
|
||||||
|
})
|
||||||
|
|
||||||
|
// add a handler for the instance page
|
||||||
|
mux.Handle(route+"instance/", httpx.HTMLHandler[instancePageContext]{
|
||||||
|
Handler: info.instancePageAPI,
|
||||||
|
Template: instanceTemplate,
|
||||||
|
})
|
||||||
|
|
||||||
|
handler := &httpx.WebSocket{
|
||||||
|
Context: context,
|
||||||
|
Fallback: mux,
|
||||||
|
Handler: info.serveSocket,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that everyone is logged in!
|
||||||
|
return httpx.BasicAuth(handler, "WissKI Distillery Admin", func(user, pass string) bool {
|
||||||
|
return user == info.Config.DisAdminUser && pass == info.Config.DisAdminPassword
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
51
internal/component/info/instance.go
Normal file
51
internal/component/info/instance.go
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package info
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed "html/instance.html"
|
||||||
|
var instanceTemplateString string
|
||||||
|
var instanceTemplate = template.Must(template.New("instance.html").Parse(instanceTemplateString))
|
||||||
|
|
||||||
|
type instancePageContext struct {
|
||||||
|
Time time.Time
|
||||||
|
|
||||||
|
Instance models.Instance
|
||||||
|
Info instances.WissKIInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info *Info) instancePageAPI(r *http.Request) (is instancePageContext, err error) {
|
||||||
|
// find the slug as the last component of path!
|
||||||
|
slug := strings.TrimSuffix(r.URL.Path, "/")
|
||||||
|
slug = slug[strings.LastIndex(slug, "/")+1:]
|
||||||
|
|
||||||
|
// find the instance itself!
|
||||||
|
instance, err := info.Instances.WissKI(slug)
|
||||||
|
if err == instances.ErrWissKINotFound {
|
||||||
|
return is, httpx.ErrNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return is, err
|
||||||
|
}
|
||||||
|
is.Instance = instance.Instance
|
||||||
|
|
||||||
|
// get some more info about the wisski
|
||||||
|
is.Info, err = instance.Info(false)
|
||||||
|
if err != nil {
|
||||||
|
return is, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// current time
|
||||||
|
is.Time = time.Now().UTC()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
73
internal/component/info/socket.go
Normal file
73
internal/component/info/socket.go
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
package info
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/component/snapshots"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||||
|
"github.com/tkw1536/goprogram/status"
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (info *Info) serveSocket(conn httpx.WebSocketConnection) {
|
||||||
|
// read the next message to act on
|
||||||
|
message, ok := <-conn.Read()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch string(message.Bytes) {
|
||||||
|
case "snapshot":
|
||||||
|
slug, ok := <-conn.Read()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info.serverSocketSnapshot(string(slug.Bytes), info.socketWriter(conn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Info) socketWriter(conn httpx.WebSocketConnection) *status.LineBuffer {
|
||||||
|
return &status.LineBuffer{
|
||||||
|
Line: func(line string) {
|
||||||
|
<-conn.WriteText(line)
|
||||||
|
},
|
||||||
|
FlushLineOnClose: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info *Info) serverSocketSnapshot(slug string, writer *status.LineBuffer) {
|
||||||
|
stream := stream.NewIOStream(writer, writer, nil, 0)
|
||||||
|
|
||||||
|
// get the wisski
|
||||||
|
wissKI, err := info.Instances.WissKI(slug)
|
||||||
|
if err != nil {
|
||||||
|
stream.EPrintln(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
err := info.SnapshotManager.HandleSnapshotLike(
|
||||||
|
stream,
|
||||||
|
snapshots.SnapshotFlags{
|
||||||
|
Dest: "",
|
||||||
|
Slug: slug,
|
||||||
|
Title: "Snapshot",
|
||||||
|
StagingOnly: false,
|
||||||
|
Do: func(dest string) snapshots.SnapshotLike {
|
||||||
|
snapshot := info.SnapshotManager.NewSnapshot(
|
||||||
|
wissKI,
|
||||||
|
stream,
|
||||||
|
snapshots.SnapshotDescription{
|
||||||
|
Dest: dest,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return &snapshot
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
stream.EPrintln(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream.Println("Done")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
// updatePrefixes starts updating prefixes
|
// updatePrefixes starts updating prefixes
|
||||||
func (resolver *Resolver) updatePrefixes(io stream.IOStream, ctx context.Context) {
|
func (resolver *Resolver) updatePrefixes(io stream.IOStream, ctx context.Context) {
|
||||||
timex.SetInterval(ctx, resolver.RefreshInterval, func(t time.Time) {
|
timex.SetInterval(ctx, resolver.RefreshInterval, func(t time.Time) {
|
||||||
io.Printf("[%s]: reloading prefixes", t.String())
|
io.Printf("[%s]: reloading prefixes\n", t.Format(time.Stamp))
|
||||||
prefixes, _ := resolver.AllPrefixes()
|
prefixes, _ := resolver.AllPrefixes()
|
||||||
resolver.prefixes.Set(prefixes)
|
resolver.prefixes.Set(prefixes)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
134
internal/component/snapshots/iface.go
Normal file
134
internal/component/snapshots/iface.go
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
package snapshots
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/logging"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/targz"
|
||||||
|
"github.com/tkw1536/goprogram/status"
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SnapshotFlags struct {
|
||||||
|
Dest string
|
||||||
|
Slug string
|
||||||
|
Title string // "Backup" or "Snapshot"
|
||||||
|
StagingOnly bool
|
||||||
|
|
||||||
|
Do func(dest string) SnapshotLike
|
||||||
|
}
|
||||||
|
|
||||||
|
type SnapshotLike interface {
|
||||||
|
LogEntry() models.Snapshot
|
||||||
|
Report(w io.Writer) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *Manager) HandleSnapshotLike(context stream.IOStream, flags SnapshotFlags) (err error) {
|
||||||
|
// determine target paths
|
||||||
|
logging.LogMessage(context, "Determining target paths")
|
||||||
|
var stagingDir, archivePath string
|
||||||
|
if flags.StagingOnly {
|
||||||
|
stagingDir = flags.Dest
|
||||||
|
} else {
|
||||||
|
archivePath = flags.Dest
|
||||||
|
}
|
||||||
|
if stagingDir == "" {
|
||||||
|
stagingDir, err = manager.NewStagingDir(flags.Slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !flags.StagingOnly && archivePath == "" {
|
||||||
|
archivePath = manager.NewArchivePath(flags.Slug)
|
||||||
|
}
|
||||||
|
context.Printf("Staging Directory: %s\n", stagingDir)
|
||||||
|
context.Printf("Archive Path: %s\n", archivePath)
|
||||||
|
|
||||||
|
// create the staging directory
|
||||||
|
logging.LogMessage(context, "Creating staging directory")
|
||||||
|
err = manager.Environment.Mkdir(stagingDir, environment.DefaultDirPerm)
|
||||||
|
if !environment.IsExist(err) && err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it was requested to not do staging only
|
||||||
|
// we need the staging directory to be deleted at the end
|
||||||
|
if !flags.StagingOnly {
|
||||||
|
defer func() {
|
||||||
|
logging.LogMessage(context, "Removing staging directory")
|
||||||
|
manager.Environment.RemoveAll(stagingDir)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the actual snapshot or backup
|
||||||
|
// write out the report
|
||||||
|
// and retain a log entry
|
||||||
|
var entry models.Snapshot
|
||||||
|
logging.LogOperation(func() error {
|
||||||
|
// do the snapshot!
|
||||||
|
sl := flags.Do(stagingDir)
|
||||||
|
|
||||||
|
// create a log entry
|
||||||
|
entry = sl.LogEntry()
|
||||||
|
|
||||||
|
// find the report path
|
||||||
|
reportPath := filepath.Join(stagingDir, "report.txt")
|
||||||
|
context.Println(reportPath)
|
||||||
|
|
||||||
|
// create the path
|
||||||
|
report, err := manager.Environment.Create(reportPath, environment.DefaultFilePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// and write out the report
|
||||||
|
{
|
||||||
|
_, err := sl.Report(report)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}, context, "Generating %s", flags.Title)
|
||||||
|
|
||||||
|
// if we only requested staging
|
||||||
|
// all that is left is to write the log entry
|
||||||
|
if flags.StagingOnly {
|
||||||
|
logging.LogMessage(context, "Writing Log Entry")
|
||||||
|
|
||||||
|
// write out the log entry
|
||||||
|
entry.Path = stagingDir
|
||||||
|
entry.Packed = false
|
||||||
|
manager.Instances.AddSnapshotLog(entry)
|
||||||
|
|
||||||
|
context.Printf("Wrote %s\n", stagingDir)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// package everything up as an archive!
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
var count int64
|
||||||
|
defer func() { context.Printf("Wrote %d byte(s) to %s\n", count, archivePath) }()
|
||||||
|
|
||||||
|
st := status.NewWithCompat(context.Stdout, 1)
|
||||||
|
st.Start()
|
||||||
|
defer st.Stop()
|
||||||
|
|
||||||
|
count, err = targz.Package(manager.Environment, archivePath, stagingDir, func(dst, src string) {
|
||||||
|
st.Set(0, dst)
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}, context, "Writing archive"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write out the log entry
|
||||||
|
logging.LogMessage(context, "Writing Log Entry")
|
||||||
|
entry.Path = archivePath
|
||||||
|
entry.Packed = true
|
||||||
|
manager.Instances.AddSnapshotLog(entry)
|
||||||
|
|
||||||
|
// and we're done!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,6 @@ import (
|
||||||
// SnapshotDescription is a description for a snapshot
|
// SnapshotDescription is a description for a snapshot
|
||||||
type SnapshotDescription struct {
|
type SnapshotDescription struct {
|
||||||
Dest string // destination path
|
Dest string // destination path
|
||||||
Log bool // should we log the creation of this snapshot?
|
|
||||||
Keepalive bool // should we keep the instance alive while making the snapshot?
|
Keepalive bool // should we keep the instance alive while making the snapshot?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
html{color:#1a1a1a;background-color:#fdfdfd;font-family:Roboto;font-size:20px;line-height:1.5}body{max-width:36em;-webkit-hyphens:auto;hyphens:auto;overflow-wrap:break-word;text-rendering:optimizelegibility;font-kerning:normal;margin:0 auto;padding:50px}@media (max-width:600px){body{padding:1em;font-size:.9em}}h1{margin-top:1.4em}h2,h3{margin-top:1em}code{color:#00f;font-family:Roboto Mono}p{text-align:justify;margin:1em 0}a,a:visited{color:#1a1a1a}footer{text-align:center;border-top:1px solid #1a1a1a;font-size:small}.header-link{opacity:0;font-size:.8em;text-decoration:none;transition:opacity .2s ease-in-out .1s;position:relative;left:.5em}h2:hover .header-link,h3:hover .header-link,h4:hover .header-link,h5:hover .header-link,h6:hover .header-link{opacity:1}.wisski{padding-left:5px}.wisski.running{background-color:#9ada07}.wisski.stopped{background-color:#ff7a7a}
|
html{color:#1a1a1a;background-color:#fdfdfd;font-family:Roboto;font-size:20px;line-height:1.5}body{max-width:36em;-webkit-hyphens:auto;hyphens:auto;overflow-wrap:break-word;text-rendering:optimizelegibility;font-kerning:normal;margin:0 auto;padding:50px}@media (max-width:600px){body{padding:1em;font-size:.9em}}h1{margin-top:1.4em}h2,h3{margin-top:1em}code{color:#00f;font-family:Roboto Mono}p{text-align:justify;margin:1em 0}a,a:visited{color:#1a1a1a}footer{text-align:center;border-top:1px solid #1a1a1a;font-size:small}.header-link{opacity:0;font-size:.8em;text-decoration:none;transition:opacity .2s ease-in-out .1s;position:relative;left:.5em}h2:hover .header-link,h3:hover .header-link,h4:hover .header-link,h5:hover .header-link,h6:hover .header-link{opacity:1}.wisski{padding-left:5px}.wisski.running{background-color:#9ada07}.wisski.stopped{background-color:#ff7a7a}.remote-action-out{font-size:small}
|
||||||
|
|
@ -1 +1 @@
|
||||||
(()=>{const e=e=>{const t=document.getElementsByTagName("h"+e);Array.from(t).forEach((e=>{void 0!==e.id&&""!==e.id&&e.appendChild((e=>{const t=document.createElement("a");return t.className="header-link",t.href="#"+e,t.innerHTML="#",t})(e.id))}))};new Array(6).fill(0).forEach(((t,n)=>e(n+1)));const t={date:e=>new Date(e.innerText).toISOString(),path:e=>{const t=e.innerText.split("/");return t[t.length-1]},pathbuilders:()=>{const e=window.pathbuilders,t=document.createElement("span");let o=!1;if(Object.keys(e).forEach((r=>{o=!0;const a=r+".xml",c=e[r];t.append(n(a,r,c,"application/xml")),t.append(document.createTextNode(" "))})),!o)return"(none)";const r=document.createElement("small");return r.append(document.createTextNode("(click to download)")),t.append(r),t}},n=(e,t,n,o)=>{const r=new Blob([n],{type:o??"text/plain"}),a=document.createElement("a");return a.target="_blank",a.download=e,a.href=URL.createObjectURL(r),a.append(document.createTextNode(t)),a};Object.keys(t).forEach((e=>{const n=t[e];document.querySelectorAll("code."+e).forEach((e=>{const t=n(e);if("string"==typeof t)return e.innerHTML="",void e.appendChild(document.createTextNode(t));e.parentNode.replaceChild(t,e)}))}))})();
|
(()=>{const e=e=>{const t=document.getElementsByTagName("h"+e);Array.from(t).forEach((e=>{void 0!==e.id&&""!==e.id&&e.appendChild((e=>{const t=document.createElement("a");return t.className="header-link",t.href="#"+e,t.innerHTML="#",t})(e.id))}))};new Array(6).fill(0).forEach(((t,n)=>e(n+1)));const t={date:e=>new Date(e.innerText).toISOString(),path:e=>{const t=e.innerText.split("/");return t[t.length-1]},pathbuilders:()=>{const e=window.pathbuilders,t=document.createElement("span");let o=!1;if(Object.keys(e).forEach((r=>{o=!0;const a=r+".xml",c=e[r];t.append(n(a,r,c,"application/xml")),t.append(document.createTextNode(" "))})),!o)return"(none)";const r=document.createElement("small");return r.append(document.createTextNode("(click to download)")),t.append(r),t}},n=(e,t,n,o)=>{const r=new Blob([n],{type:o??"text/plain"}),a=document.createElement("a");return a.target="_blank",a.download=e,a.href=URL.createObjectURL(r),a.append(document.createTextNode(t)),a};Object.keys(t).forEach((e=>{const n=t[e];document.querySelectorAll("code."+e).forEach((e=>{const t=n(e);if("string"==typeof t)return e.innerHTML="",void e.appendChild(document.createTextNode(t));e.parentNode.replaceChild(t,e)}))}));const o=document.getElementsByClassName("remote-action");Array.from(o).forEach((e=>{const t=e.getAttribute("data-action"),n=e.getAttribute("data-param"),o=document.querySelector(e.getAttribute("data-target")),r=function(){const t=parseInt(e.getAttribute("data-buffer")??"",10)??0;return isFinite(t)&&t>0?t:0}();let a=!1;e.addEventListener("click",(function(c){if(c.preventDefault(),a)return;a=!0,e.setAttribute("disabled","disabled");const d=function(){e.removeAttribute("disabled"),a=!1};o.innerText="";const i=[],s=function(e){0!==r?(i.push(e),i.length>r&&i.splice(0,i.length-r),o.innerText=i.join("\n")):o.innerText+=e+"\n"};var l,u;s("Connecting ..."),(l=e=>{s("Connected"),e.send(t),"string"==typeof n&&e.send(n)},u=e=>{s(e)},new Promise(((e,t)=>{const n=new WebSocket(location.href.replace("http","ws"));n.onclose=e,n.onerror=t,n.onmessage=e=>u(e.data),n.onopen=()=>l(n)}))).then((()=>{s("Connection closed.\n"),d()})).catch((()=>{s("Connection errored.\n"),d()}))}))}))})();
|
||||||
65
internal/component/static/src/control/highlight.ts
Normal file
65
internal/component/static/src/control/highlight.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
|
||||||
|
const types: Record<string, (element: HTMLElement) => HTMLElement | string> = {
|
||||||
|
"date": (element) => {
|
||||||
|
return (new Date(element.innerText)).toISOString()
|
||||||
|
},
|
||||||
|
"path": (element) => {
|
||||||
|
const text = element.innerText.split("/");
|
||||||
|
return text[text.length - 1];
|
||||||
|
},
|
||||||
|
"pathbuilders": () => {
|
||||||
|
const pathbuilders: {[name: string]: string} = (window as any).pathbuilders; // must be declared globally on page!
|
||||||
|
const wrapper = document.createElement("span");
|
||||||
|
|
||||||
|
let found_one = false
|
||||||
|
Object.keys(pathbuilders).forEach(name => {
|
||||||
|
found_one = true
|
||||||
|
|
||||||
|
const filename = name + ".xml"
|
||||||
|
const data = pathbuilders[name]
|
||||||
|
const mime = "application/xml"
|
||||||
|
wrapper.append(make_download_link(filename, name, data, mime))
|
||||||
|
wrapper.append(document.createTextNode(" "))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!found_one) return '(none)';
|
||||||
|
|
||||||
|
const small = document.createElement('small')
|
||||||
|
small.append(document.createTextNode("(click to download)"))
|
||||||
|
wrapper.append(small)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const make_download_link = (filename: string, title: string, content: string, type: string) => {
|
||||||
|
const blob = new Blob(
|
||||||
|
[content],
|
||||||
|
{
|
||||||
|
type: type ?? "text/plain"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.target = "_blank"
|
||||||
|
link.download = filename
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.append(document.createTextNode(title))
|
||||||
|
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(types).forEach(key => {
|
||||||
|
const f = types[key];
|
||||||
|
const elements = document.querySelectorAll("code." + key) as NodeListOf<HTMLElement>
|
||||||
|
elements.forEach(element => {
|
||||||
|
const newElement = f(element)
|
||||||
|
if (typeof newElement === 'string') {
|
||||||
|
element.innerHTML = ""
|
||||||
|
element.appendChild(document.createTextNode(newElement))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
element.parentNode!.replaceChild(newElement, element)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -11,3 +11,7 @@
|
||||||
.wisski.stopped {
|
.wisski.stopped {
|
||||||
background-color: #ff7a7a;
|
background-color: #ff7a7a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remote-action-out {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,5 @@
|
||||||
import '../global.ts';
|
import '../global.ts';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
|
import './highlight.ts';
|
||||||
const types: Record<string, (element: HTMLElement) => HTMLElement | string> = {
|
import './remote.ts';
|
||||||
"date": (element) => {
|
|
||||||
return (new Date(element.innerText)).toISOString()
|
|
||||||
},
|
|
||||||
"path": (element) => {
|
|
||||||
const text = element.innerText.split("/");
|
|
||||||
return text[text.length - 1];
|
|
||||||
},
|
|
||||||
"pathbuilders": () => {
|
|
||||||
const pathbuilders: {[name: string]: string} = (window as any).pathbuilders; // must be declared globally on page!
|
|
||||||
const wrapper = document.createElement("span");
|
|
||||||
|
|
||||||
let found_one = false
|
|
||||||
Object.keys(pathbuilders).forEach(name => {
|
|
||||||
found_one = true
|
|
||||||
|
|
||||||
const filename = name + ".xml"
|
|
||||||
const data = pathbuilders[name]
|
|
||||||
const mime = "application/xml"
|
|
||||||
wrapper.append(make_download_link(filename, name, data, mime))
|
|
||||||
wrapper.append(document.createTextNode(" "))
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!found_one) return '(none)';
|
|
||||||
|
|
||||||
const small = document.createElement('small')
|
|
||||||
small.append(document.createTextNode("(click to download)"))
|
|
||||||
wrapper.append(small)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const make_download_link = (filename: string, title: string, content: string, type: string) => {
|
|
||||||
const blob = new Blob(
|
|
||||||
[content],
|
|
||||||
{
|
|
||||||
type: type ?? "text/plain"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const link = document.createElement("a")
|
|
||||||
link.target = "_blank"
|
|
||||||
link.download = filename
|
|
||||||
link.href = URL.createObjectURL(blob)
|
|
||||||
link.append(document.createTextNode(title))
|
|
||||||
|
|
||||||
return link
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(types).forEach(key => {
|
|
||||||
const f = types[key];
|
|
||||||
const elements = document.querySelectorAll("code." + key) as NodeListOf<HTMLElement>
|
|
||||||
elements.forEach(element => {
|
|
||||||
const newElement = f(element)
|
|
||||||
if (typeof newElement === 'string') {
|
|
||||||
element.innerHTML = ""
|
|
||||||
element.appendChild(document.createTextNode(newElement))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
element.parentNode!.replaceChild(newElement, element)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
|
||||||
62
internal/component/static/src/control/remote.ts
Normal file
62
internal/component/static/src/control/remote.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import connectSocket from '../socket/socket';
|
||||||
|
|
||||||
|
const elements = document.getElementsByClassName('remote-action')
|
||||||
|
Array.from(elements).forEach((element) => {
|
||||||
|
const action = element.getAttribute('data-action') as string;
|
||||||
|
const param = element.getAttribute('data-param') as string | undefined;
|
||||||
|
const target = document.querySelector(element.getAttribute('data-target')!) as HTMLElement;
|
||||||
|
const bufferSize = (function() {
|
||||||
|
const number = parseInt(element.getAttribute('data-buffer') ?? "", 10) ?? 0;
|
||||||
|
return (isFinite(number) && number > 0) ? number : 0;
|
||||||
|
})()
|
||||||
|
|
||||||
|
let running = false
|
||||||
|
element.addEventListener('click', function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
// already running
|
||||||
|
if (running) return
|
||||||
|
|
||||||
|
running = true
|
||||||
|
element.setAttribute('disabled', 'disabled');
|
||||||
|
const close = function() {
|
||||||
|
element.removeAttribute('disabled');
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.innerText = "";
|
||||||
|
|
||||||
|
const buffer: Array<string> = [];
|
||||||
|
const println = function(line: string) {
|
||||||
|
if(bufferSize === 0) {
|
||||||
|
target.innerText += line + "\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push(line);
|
||||||
|
if(buffer.length > bufferSize) {
|
||||||
|
buffer.splice(0, buffer.length - bufferSize)
|
||||||
|
}
|
||||||
|
target.innerText = buffer.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
println("Connecting ...")
|
||||||
|
|
||||||
|
// connect to the socket and send the action
|
||||||
|
connectSocket((socket) => {
|
||||||
|
println("Connected")
|
||||||
|
socket.send(action);
|
||||||
|
if (typeof param === 'string') {
|
||||||
|
socket.send(param);
|
||||||
|
}
|
||||||
|
}, (data) => {
|
||||||
|
println(data);
|
||||||
|
}).then(() => {
|
||||||
|
println("Connection closed.\n")
|
||||||
|
close();
|
||||||
|
}).catch(() => {
|
||||||
|
println("Connection errored.\n")
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
11
internal/component/static/src/socket/socket.ts
Normal file
11
internal/component/static/src/socket/socket.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export default function connectSocket(onOpen: (socket: WebSocket) => void, onData: (data: any) => void): Promise<CloseEvent> {
|
||||||
|
return new Promise((rs, rj) => {
|
||||||
|
const socket = new WebSocket(location.href.replace('http', 'ws'));
|
||||||
|
|
||||||
|
socket.onclose = rs;
|
||||||
|
socket.onerror = rj;
|
||||||
|
|
||||||
|
socket.onmessage = (ev) => onData(ev.data)
|
||||||
|
socket.onopen = () => onOpen(socket);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/component/control"
|
"github.com/FAU-CDI/wisski-distillery/internal/component/control"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/component/home"
|
"github.com/FAU-CDI/wisski-distillery/internal/component/home"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/component/info"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
|
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/component/resolver"
|
"github.com/FAU-CDI/wisski-distillery/internal/component/resolver"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/component/snapshots"
|
"github.com/FAU-CDI/wisski-distillery/internal/component/snapshots"
|
||||||
|
|
@ -52,7 +53,7 @@ func (dis *Distillery) register(context *component.PoolContext) []component.Comp
|
||||||
r(dis, context, func(resolver *resolver.Resolver) {
|
r(dis, context, func(resolver *resolver.Resolver) {
|
||||||
resolver.RefreshInterval = time.Minute
|
resolver.RefreshInterval = time.Minute
|
||||||
}),
|
}),
|
||||||
ra[*control.Info](dis, context),
|
ra[*info.Info](dis, context),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
315
pkg/httpx/socket.go
Normal file
315
pkg/httpx/socket.go
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
package httpx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebSocket implements serving a WebSocket
|
||||||
|
type WebSocket struct {
|
||||||
|
Context context.Context // context which closes all connections
|
||||||
|
Limits WebSocketLimits // limits for websocket operations
|
||||||
|
|
||||||
|
Handler func(ws WebSocketConnection)
|
||||||
|
Fallback http.Handler
|
||||||
|
|
||||||
|
pool lazy.Lazy[*sync.Pool] // pool holds *WebSocketConn objects
|
||||||
|
upgrader websocket.Upgrader // upgrades upgrades connections
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebSocketLimits struct {
|
||||||
|
WriteWait time.Duration // maximum time to wait for writing
|
||||||
|
PongWait time.Duration // time to wait for pong responses
|
||||||
|
PingInterval time.Duration // interval to send pings to the client
|
||||||
|
MaxMessageSize int64 // maximal message size in bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (limits *WebSocketLimits) SetDefaults() {
|
||||||
|
if limits.WriteWait == 0 {
|
||||||
|
limits.WriteWait = 10 * time.Second
|
||||||
|
}
|
||||||
|
if limits.PongWait == 0 {
|
||||||
|
limits.PongWait = time.Minute
|
||||||
|
}
|
||||||
|
if limits.PingInterval <= 0 {
|
||||||
|
limits.PingInterval = (limits.PongWait * 9) / 10
|
||||||
|
}
|
||||||
|
if limits.MaxMessageSize <= 0 {
|
||||||
|
limits.MaxMessageSize = 2048
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// makePoolSocket creates a new socket and makes sure that the pool is initialized
|
||||||
|
func (h *WebSocket) makePoolSocket() *webSocketConn {
|
||||||
|
return h.pool.Get(func() *sync.Pool {
|
||||||
|
return &sync.Pool{
|
||||||
|
New: func() any { return new(webSocketConn) },
|
||||||
|
}
|
||||||
|
}).Get().(*webSocketConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *WebSocket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// if the user did not request a websocket, go to the fallbacjk handler
|
||||||
|
if !websocket.IsWebSocketUpgrade(r) {
|
||||||
|
h.serveFallback(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// else deal with the websocket!
|
||||||
|
h.serveWebsocket(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *WebSocket) serveFallback(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.Fallback == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Fallback.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *WebSocket) serveWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// upgrade the connection or bail out!
|
||||||
|
conn, err := h.upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get a new socket from the pool
|
||||||
|
socket := h.makePoolSocket()
|
||||||
|
socket.Serve(h.Context, h.Limits, conn, h.Handler)
|
||||||
|
|
||||||
|
// return a reset socket to the pool
|
||||||
|
socket.reset()
|
||||||
|
h.pool.Get(nil).Put(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocketConnection represents a connected Websocket
|
||||||
|
type WebSocketConnection interface {
|
||||||
|
// Context returns a context that is closed once this websocket is closed.
|
||||||
|
Context() context.Context
|
||||||
|
|
||||||
|
// Read returns a channel that receives message.
|
||||||
|
// The channel is closed once no more messags are available.
|
||||||
|
Read() <-chan WebSocketMessage
|
||||||
|
|
||||||
|
// Write queues the provided message for sending
|
||||||
|
// and returns a channel that is closed once the message has been sent.
|
||||||
|
Write(WebSocketMessage) <-chan struct{}
|
||||||
|
|
||||||
|
// WriteText is a convenience method to send a TextMessage
|
||||||
|
WriteText(text string) <-chan struct{}
|
||||||
|
|
||||||
|
// Close closes the underlying connection
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocketMessage represents a connected Websocket
|
||||||
|
type WebSocketMessage struct {
|
||||||
|
Type int
|
||||||
|
Bytes []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type outWebSocketMessage struct {
|
||||||
|
WebSocketMessage
|
||||||
|
done chan<- struct{} // done should be closed when finished
|
||||||
|
}
|
||||||
|
|
||||||
|
// webSocketConn implements [WebSocketConnection]
|
||||||
|
type webSocketConn struct {
|
||||||
|
conn *websocket.Conn // underlying connection
|
||||||
|
limits WebSocketLimits
|
||||||
|
|
||||||
|
context context.Context // context to cancel the connection
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
wg sync.WaitGroup // blocks all the ongoing tasks
|
||||||
|
|
||||||
|
// incoming and outgoing tasks
|
||||||
|
incoming chan WebSocketMessage
|
||||||
|
outgoing chan outWebSocketMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve serves the provided connection
|
||||||
|
func (h *webSocketConn) Serve(ctx context.Context, limits WebSocketLimits, conn *websocket.Conn, handler func(ws WebSocketConnection)) {
|
||||||
|
// use the connection!
|
||||||
|
h.conn = conn
|
||||||
|
|
||||||
|
// setup limits
|
||||||
|
h.limits = limits
|
||||||
|
h.limits.SetDefaults()
|
||||||
|
|
||||||
|
// create a context for the connection
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
h.context, h.cancel = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
// start receiving and sending messages
|
||||||
|
h.wg.Add(2)
|
||||||
|
h.sendMessages()
|
||||||
|
h.recvMessages()
|
||||||
|
|
||||||
|
// wait for the context to be cancelled, then close the connection
|
||||||
|
h.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer h.wg.Done()
|
||||||
|
<-h.context.Done()
|
||||||
|
h.conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// start the application logic
|
||||||
|
h.wg.Add(1)
|
||||||
|
go h.handle(handler)
|
||||||
|
|
||||||
|
// wait for closing operations
|
||||||
|
h.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *webSocketConn) handle(handler func(ws WebSocketConnection)) {
|
||||||
|
defer func() {
|
||||||
|
h.wg.Done()
|
||||||
|
h.cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
handler(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *webSocketConn) sendMessages() {
|
||||||
|
h.outgoing = make(chan outWebSocketMessage)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// close connection when done!
|
||||||
|
defer func() {
|
||||||
|
h.wg.Done()
|
||||||
|
h.cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// setup a timer for pings!
|
||||||
|
ticker := time.NewTicker(h.limits.PingInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// everything is done!
|
||||||
|
case <-h.context.Done():
|
||||||
|
return
|
||||||
|
|
||||||
|
// send outgoing messages
|
||||||
|
case message := <-h.outgoing:
|
||||||
|
(func() {
|
||||||
|
defer close(message.done)
|
||||||
|
|
||||||
|
err := h.writeRaw(message.Type, message.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.done <- struct{}{}
|
||||||
|
})()
|
||||||
|
// send a ping message
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := h.writeRaw(websocket.PingMessage, []byte{}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeRaw writes to the underlying socket
|
||||||
|
func (h *webSocketConn) writeRaw(messageType int, data []byte) error {
|
||||||
|
h.conn.SetWriteDeadline(time.Now().Add(h.limits.WriteWait))
|
||||||
|
return h.conn.WriteMessage(messageType, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes a message to the websocket connection.
|
||||||
|
func (sh *webSocketConn) Write(message WebSocketMessage) <-chan struct{} {
|
||||||
|
callback := make(chan struct{}, 1)
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
// write an outgoing message
|
||||||
|
case sh.outgoing <- outWebSocketMessage{
|
||||||
|
WebSocketMessage: message,
|
||||||
|
done: callback,
|
||||||
|
}:
|
||||||
|
// context
|
||||||
|
case <-sh.context.Done():
|
||||||
|
close(callback)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sh *webSocketConn) WriteText(text string) <-chan struct{} {
|
||||||
|
return sh.Write(WebSocketMessage{
|
||||||
|
Type: websocket.TextMessage,
|
||||||
|
Bytes: []byte(text),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *webSocketConn) recvMessages() {
|
||||||
|
h.incoming = make(chan WebSocketMessage)
|
||||||
|
|
||||||
|
// set a read handler
|
||||||
|
h.conn.SetReadLimit(h.limits.MaxMessageSize)
|
||||||
|
|
||||||
|
// configure a pong handler
|
||||||
|
h.conn.SetReadDeadline(time.Now().Add(h.limits.PongWait))
|
||||||
|
h.conn.SetPongHandler(func(string) error { h.conn.SetReadDeadline(time.Now().Add(h.limits.PongWait)); return nil })
|
||||||
|
|
||||||
|
// handle incoming messages
|
||||||
|
go func() {
|
||||||
|
// close connection when done!
|
||||||
|
defer func() {
|
||||||
|
h.wg.Done()
|
||||||
|
h.cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
messageType, messageBytes, err := h.conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to send a message to the incoming message channel
|
||||||
|
select {
|
||||||
|
case h.incoming <- WebSocketMessage{
|
||||||
|
Type: messageType,
|
||||||
|
Bytes: messageBytes,
|
||||||
|
}:
|
||||||
|
case <-h.context.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read returns a channel that receives incoming messages.
|
||||||
|
// The channel is close once no more messages are available, or the context is canceled.
|
||||||
|
func (h *webSocketConn) Read() <-chan WebSocketMessage {
|
||||||
|
return h.incoming
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context returns a context that is closed once this connection is closed.
|
||||||
|
func (h *webSocketConn) Context() context.Context {
|
||||||
|
return h.context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *webSocketConn) Close() {
|
||||||
|
h.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset resets this websocket
|
||||||
|
func (h *webSocketConn) reset() {
|
||||||
|
h.limits = WebSocketLimits{}
|
||||||
|
h.conn = nil
|
||||||
|
h.incoming = nil
|
||||||
|
h.outgoing = nil
|
||||||
|
h.context, h.cancel = nil, nil
|
||||||
|
}
|
||||||
|
|
@ -22,8 +22,8 @@ func LogMessage(io stream.IOStream, format string, args ...interface{}) (int, er
|
||||||
|
|
||||||
func logOperation(io stream.IOStream, indent int, format string, args ...interface{}) (int, error) {
|
func logOperation(io stream.IOStream, indent int, format string, args ...interface{}) (int, error) {
|
||||||
message := "\033[1m" + strings.Repeat(" ", indent+1) + "=> " + format + "\033[0m\n"
|
message := "\033[1m" + strings.Repeat(" ", indent+1) + "=> " + format + "\033[0m\n"
|
||||||
if !io.StdinIsATerminal() {
|
if !io.StdoutIsATerminal() {
|
||||||
message = " => " + format
|
message = " => " + format + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
return io.Printf(message, args...)
|
return io.Printf(message, args...)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue