From d36545bb519e25ba21be70bb2363bf0f5d6928ee Mon Sep 17 00:00:00 2001 From: Adis Durakovic Date: Sat, 13 Jan 2024 12:37:43 +0100 Subject: [PATCH] feat: context --- app.go | 9 -- frontend/components/HeaderNavBar.vue | 9 +- frontend/components/Schedule/List.vue | 26 +++-- frontend/composables/useApi.ts | 2 + frontend/wailsjs/go/main/App.d.ts | 3 - frontend/wailsjs/go/main/App.js | 4 - frontend/wailsjs/go/models.ts | 18 ---- restic.go | 27 ++++-- scheduler.go | 132 ++++++++++++++++++++------ server.go | 13 ++- settings.go | 1 + 11 files changed, 153 insertions(+), 91 deletions(-) delete mode 100755 frontend/wailsjs/go/models.ts diff --git a/app.go b/app.go index 6c5ba62..6a4db4f 100644 --- a/app.go +++ b/app.go @@ -20,11 +20,6 @@ type App struct { settings *Settings } -type BackupJob struct { - JobId uuid.UUID `json:"job_id"` - Schedule Schedule `json:"schedule"` -} - // NewApp creates a new App application struct func NewApp(restic *Restic, scheduler *Scheduler, settings *Settings) *App { return &App{restic: restic, scheduler: scheduler, settings: settings} @@ -95,10 +90,6 @@ func (a *App) startup(ctx context.Context) { } -func (a *App) GetBackupJobs() []BackupJob { - return a.scheduler.RunningJobs -} - func (a *App) StopBackup(id uuid.UUID) { // a.scheduler.RemoveJob(id) // a.RescheduleBackups() diff --git a/frontend/components/HeaderNavBar.vue b/frontend/components/HeaderNavBar.vue index 0aeb60c..5ac2b4d 100644 --- a/frontend/components/HeaderNavBar.vue +++ b/frontend/components/HeaderNavBar.vue @@ -5,31 +5,24 @@
+
diff --git a/frontend/components/Schedule/List.vue b/frontend/components/Schedule/List.vue index 82b191c..295a269 100644 --- a/frontend/components/Schedule/List.vue +++ b/frontend/components/Schedule/List.vue @@ -69,14 +69,24 @@ const items = (row: any) => [ [ - { - label: 'Run now', - icon: 'i-heroicons-arrow-uturn-right', - click: async () => { - const t = await useApi().runSchedule(row.id) - console.log(t) - }, - }, + !useJobs().scheduleIsRunning(row.id) + ? { + label: 'Run now', + icon: 'i-heroicons-arrow-uturn-right', + click: async () => { + const t = await useApi().runSchedule(row.id) + console.log(t) + }, + } + : { + label: 'Stop', + icon: 'i-heroicons-arrow-uturn-right', + click: async () => { + const t = await useApi().stopSchedule(row.id) + console.log(t) + }, + }, + { label: 'Delete', icon: 'i-heroicons-trash', diff --git a/frontend/composables/useApi.ts b/frontend/composables/useApi.ts index bc738a2..1fdd901 100644 --- a/frontend/composables/useApi.ts +++ b/frontend/composables/useApi.ts @@ -19,6 +19,7 @@ export const useApi = defineStore('useApi', () => { const statRepository = async (repoId: string) => (await useHttp.get(`/repositories/${repoId}/stats`)) ?? {} const runSchedule = async (scheduleId: string) => (await useHttp.get(`/schedules/${scheduleId}/run`)) ?? {} + const stopSchedule = async (scheduleId: string) => (await useHttp.get(`/schedules/${scheduleId}/stop`)) ?? {} const getConfig = async () => (await useHttp.get(`/config`)) ?? {} const saveConfig = async (config: any) => (await useHttp.post(`/config`, config, { title: 'Settings', text: 'Settings saved successfully' })) ?? {} const checkRepository = async (repo: any) => (await useHttp.post(`/check`, repo, { title: 'Check Repository', text: 'Repository can be used' })) ?? {} @@ -29,6 +30,7 @@ export const useApi = defineStore('useApi', () => { restoreFromSnapshot, getSnapshots, runSchedule, + stopSchedule, mount, unmount, getConfig, diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index d7a716e..09c0271 100755 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -1,10 +1,7 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -import {main} from '../models'; import {uuid} from '../models'; -export function GetBackupJobs():Promise>; - export function SelectDirectory(arg1:string):Promise; export function StopBackup(arg1:uuid.UUID):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index cb5704c..8ec422b 100755 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -2,10 +2,6 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -export function GetBackupJobs() { - return window['go']['main']['App']['GetBackupJobs'](); -} - export function SelectDirectory(arg1) { return window['go']['main']['App']['SelectDirectory'](arg1); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts deleted file mode 100755 index c4f7951..0000000 --- a/frontend/wailsjs/go/models.ts +++ /dev/null @@ -1,18 +0,0 @@ -export namespace main { - - export interface Schedule { - id: string; - action: string; - backup_id: string; - to_repository_id: string; - from_repository_id: string; - cron: string; - active: boolean; - } - export interface BackupJob { - job_id: number[]; - schedule: Schedule; - } - -} - diff --git a/restic.go b/restic.go index a7b2f1c..93fac87 100644 --- a/restic.go +++ b/restic.go @@ -71,13 +71,24 @@ type FileDescriptor struct { Mtime string `json:"mtime"` } -func (r *Restic) core(repository Repository, cmd []string, envs []string) (string, error) { +func (r *Restic) core( + repository Repository, + cmd []string, + envs []string, + sctx *ScheduleContext, +) (string, error) { cmds := []string{"-r", repository.Path, "--json"} cmds = append(cmds, cmd...) var sout bytes.Buffer var serr bytes.Buffer - c := exec.Command("/usr/bin/restic", cmds...) + var c *exec.Cmd + if sctx != nil { + c = exec.CommandContext(sctx.Ctx, "/usr/bin/restic", cmds...) + defer sctx.Cancel() + } else { + c = exec.Command("/usr/bin/restic", cmds...) + } c.Stderr = &serr c.Stdout = &sout c.Env = append( @@ -102,7 +113,7 @@ func (r *Restic) core(repository Repository, cmd []string, envs []string) (strin } func (r *Restic) Exec(repository Repository, cmds []string, envs []string) (string, error) { - if data, err := r.core(repository, cmds, envs); err != nil { + if data, err := r.core(repository, cmds, envs, nil); err != nil { return "", err } else { return data, nil @@ -114,7 +125,7 @@ func (r *Restic) BrowseSnapshot( snapshotId string, path string, ) ([]FileDescriptor, error) { - if res, err := r.core(repository, []string{"ls", "-l", "--human-readable", snapshotId, path}, []string{}); err == nil { + if res, err := r.core(repository, []string{"ls", "-l", "--human-readable", snapshotId, path}, []string{}, nil); err == nil { res = strings.ReplaceAll(res, "}", "},") res = strings.ReplaceAll(res, "\n", "") res = "[" + res + "]" @@ -134,11 +145,13 @@ func (r *Restic) BrowseSnapshot( } func (r *Restic) RunSchedule( + sctx *ScheduleContext, action string, backup *Backup, toRepository *Repository, fromRepository *Repository, ) { + switch action { case "backup": if backup == nil || toRepository == nil { @@ -152,7 +165,7 @@ func (r *Restic) RunSchedule( fmt.Println(cmds) - _, err := r.core(*toRepository, cmds, []string{}) + _, err := r.core(*toRepository, cmds, []string{}, sctx) if err != nil { fmt.Println(err) } @@ -177,9 +190,9 @@ func (r *Restic) RunSchedule( for _, p := range toRepository.PruneParams { cmds = append(cmds, p...) } - _, err := r.core(*toRepository, []string{"unlock"}, []string{}) + _, err := r.core(*toRepository, []string{"unlock"}, []string{}, sctx) if err == nil { - _, err := r.core(*toRepository, cmds, []string{}) + _, err := r.core(*toRepository, cmds, []string{}, sctx) if err != nil { fmt.Println(err) } diff --git a/scheduler.go b/scheduler.go index 70972b4..6ed00cc 100644 --- a/scheduler.go +++ b/scheduler.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "sync" @@ -8,14 +9,31 @@ import ( "github.com/google/uuid" ) +type Job struct { + job gocron.Job + schedule Schedule +} + +type ScheduleContext struct { + Id string + Ctx context.Context + Cancel context.CancelFunc +} + +type RunningJob struct { + JobId uuid.UUID `json:"job_id"` + Schedule Schedule `json:"schedule"` +} + type Scheduler struct { - gocron gocron.Scheduler - restic *Restic - RunningJobs []BackupJob - Jobs []gocron.Job - ManualJobs []string - jmu sync.Mutex - settings *Settings + gocron gocron.Scheduler + restic *Restic + RunningJobs []RunningJob + ScheduleContexts []ScheduleContext + Jobs []Job + ForceInactiveJobs []string + jmu sync.Mutex + settings *Settings } func NewScheduler(settings *Settings, restic *Restic) (*Scheduler, error) { @@ -23,8 +41,8 @@ func NewScheduler(settings *Settings, restic *Restic) (*Scheduler, error) { s := &Scheduler{} s.settings = settings s.restic = restic - s.ManualJobs = []string{} - s.RunningJobs = []BackupJob{} + s.ForceInactiveJobs = []string{} + s.RunningJobs = []RunningJob{} if gc, err := gocron.NewScheduler(); err == nil { s.gocron = gc s.gocron.Start() @@ -36,10 +54,12 @@ func NewScheduler(settings *Settings, restic *Restic) (*Scheduler, error) { } func (s *Scheduler) RunJobByName(name string) { - for _, job := range s.Jobs { - if job.Name() == name { - s.ManualJobs = append(s.ManualJobs, name) - if err := job.RunNow(); err != nil { + fmt.Println("should run", name) + for _, j := range s.Jobs { + if j.job.Name() == name { + fmt.Println("Running job by name", name) + s.ForceInactiveJobs = append(s.ForceInactiveJobs, name) + if err := j.job.RunNow(); err != nil { fmt.Println("Error running job manually", err) } break @@ -47,11 +67,21 @@ func (s *Scheduler) RunJobByName(name string) { } } -func (s *Scheduler) DeleteBackgroundJob(jobID uuid.UUID) { +func (s *Scheduler) StopJobByName(name string) { + for _, c := range s.ScheduleContexts { + if c.Id == name { + c.Cancel() + break + } + } +} + +func (s *Scheduler) DeleteRunningJob(jobID uuid.UUID) { s.jmu.Lock() defer s.jmu.Unlock() for i, j := range s.RunningJobs { if j.JobId == jobID { + fmt.Println("Deleting forced inactive job", jobID) s.RunningJobs = append( s.RunningJobs[:i], s.RunningJobs[i+1:]...) @@ -60,36 +90,62 @@ func (s *Scheduler) DeleteBackgroundJob(jobID uuid.UUID) { } } -func (s *Scheduler) DeleteManualJob(name string) { +func (s *Scheduler) RecreateCtx(name string) { + for i, c := range s.ScheduleContexts { + if c.Id == name { + fmt.Println("Recreating context for job", name) + ctx, cancel := context.WithCancel(context.Background()) + s.ScheduleContexts[i].Ctx = ctx + s.ScheduleContexts[i].Cancel = cancel + break + } + } +} + +func (s *Scheduler) DeleteForcedInactiveJob(name string) { s.jmu.Lock() defer s.jmu.Unlock() - for i, j := range s.ManualJobs { + for i, j := range s.ForceInactiveJobs { if j == name { - s.ManualJobs = append( - s.ManualJobs[:i], - s.ManualJobs[i+1:]...) + fmt.Println("Deleting forced inactive job", name) + s.ForceInactiveJobs = append( + s.ForceInactiveJobs[:i], + s.ForceInactiveJobs[i+1:]...) break } } } -func (s *Scheduler) GetRunningJobs() []BackupJob { +func (s *Scheduler) GetRunningJobs() []RunningJob { s.jmu.Lock() defer s.jmu.Unlock() return s.RunningJobs } -func (s *Scheduler) RescheduleBackups() { +func (s *Scheduler) GetContextById(id string) *ScheduleContext { + for _, sc := range s.ScheduleContexts { + if sc.Id == id { + return &sc + } + } + return nil +} - s.Jobs = []gocron.Job{} +func (s *Scheduler) RescheduleBackups() { + s.Jobs = []Job{} + s.ScheduleContexts = []ScheduleContext{} + s.RunningJobs = []RunningJob{} + s.ForceInactiveJobs = []string{} fmt.Println("Rescheduling backups") config := s.settings.Config for i := range config.Schedules { schedule := config.Schedules[i] - + ctx, cancel := context.WithCancel(context.Background()) + sctx := ScheduleContext{Id: schedule.Id, Ctx: ctx, Cancel: cancel} + s.ScheduleContexts = append(s.ScheduleContexts, sctx) jobDef := gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()) if schedule.Cron != "" { @@ -102,10 +158,17 @@ func (s *Scheduler) RescheduleBackups() { toRepository := s.settings.GetRepositoryById(schedule.ToRepositoryId) fromRepository := s.settings.GetRepositoryById(schedule.FromRepositoryId) backup := s.settings.GetBackupById(schedule.BackupId) - if !schedule.Active && !StringArrayContains(s.ManualJobs, schedule.Id) { + if !schedule.Active && !StringArrayContains(s.ForceInactiveJobs, schedule.Id) { + fmt.Println("MISSING", schedule.Id) return } - s.restic.RunSchedule(schedule.Action, backup, toRepository, fromRepository) + s.restic.RunSchedule( + s.GetContextById(schedule.Id), + schedule.Action, + backup, + toRepository, + fromRepository, + ) }), gocron.WithName(schedule.Id), gocron.WithTags( @@ -120,7 +183,7 @@ func (s *Scheduler) RescheduleBackups() { defer s.jmu.Unlock() s.RunningJobs = append( s.RunningJobs, - BackupJob{ + RunningJob{ JobId: jobID, Schedule: schedule, }, @@ -128,14 +191,18 @@ func (s *Scheduler) RescheduleBackups() { }), gocron.AfterJobRuns( func(jobID uuid.UUID, jobName string) { - s.DeleteBackgroundJob(jobID) - s.DeleteManualJob(jobName) + fmt.Println("after job run") + s.DeleteRunningJob(jobID) + s.DeleteForcedInactiveJob(jobName) + s.RecreateCtx(jobName) }, ), gocron.AfterJobRunsWithError( func(jobID uuid.UUID, jobName string, err error) { - s.DeleteBackgroundJob(jobID) - s.DeleteManualJob(jobName) + fmt.Println("after job run with error", err) + s.DeleteRunningJob(jobID) + s.DeleteForcedInactiveJob(jobName) + s.RecreateCtx(jobName) }, ), )) @@ -145,7 +212,10 @@ func (s *Scheduler) RescheduleBackups() { continue } - s.Jobs = append(s.Jobs, j) + s.Jobs = append( + s.Jobs, + Job{job: j, schedule: schedule}, + ) } diff --git a/server.go b/server.go index 50eb8b2..c170783 100644 --- a/server.go +++ b/server.go @@ -71,10 +71,17 @@ func RunServer( })) - api.Get("/schedules/:id/run", func(c *fiber.Ctx) error { - scheduler.RunJobByName(c.Params("id")) + api.Get("/schedules/:id/:action", func(c *fiber.Ctx) error { + switch c.Params("action") { + case "run": + scheduler.RunJobByName(c.Params("id")) + break + case "stop": + scheduler.StopJobByName(c.Params("id")) + break + } - return c.SendString("Running schedule in the background") + return c.SendString(c.Params("action") + " schedule in the background") }) api.Post("/check", func(c *fiber.Ctx) error { diff --git a/settings.go b/settings.go index bf405db..dc8a686 100644 --- a/settings.go +++ b/settings.go @@ -97,6 +97,7 @@ func settingsFile() string { } func (s *Settings) Save(data Config) error { + s.Config = data fmt.Println("Saving settings") if str, err := json.MarshalIndent(s.Config, " ", " "); err == nil {