add wake/shutdown cronjob

This commit is contained in:
Maxi Quoß
2023-01-28 12:52:20 +01:00
parent d4fdeb8051
commit b43cd7fe99
15 changed files with 188 additions and 43 deletions

View File

@@ -37,14 +37,16 @@ Open up [http://localhost:5173/](http://localhost:5173/)
- [x] ~~add per device settings~~
- [x] ~~theme toggle button~~
- [x] ~~add device ports to cards~~
- [ ] settings: add network scan
- [ ] settings: add option for website title
- backend
- [x] ~~make sure ping works~~
- [x] ~~make sure arp works~~
- [x] ~~make sure wake works~~
- [ ] add shutdown command
- [ ] add scheduled wake
- [x] ~~add shutdown command~~
- [x] ~~add scheduled wake~~
- [x] [~~#34 Add support for WOL passwords~~](https://github.com/seriousm4x/UpSnap/issues/34)
- [x] [~~#33 Seemingly High Ram usage~~](https://github.com/seriousm4x/UpSnap/issues/33)
- [x] [~~#32 API available?~~](https://github.com/seriousm4x/UpSnap/issues/32)

View File

@@ -9,17 +9,18 @@ import (
)
var Devices []*models.Record
var Jobs *cron.Cron
var CronPing *cron.Cron
var CronWakeShutdown *cron.Cron
func RunCron(app *pocketbase.PocketBase) {
func RunPing(app *pocketbase.PocketBase) {
settingsRecords, err := app.Dao().FindRecordsByExpr("settings")
if err != nil {
logger.Error.Println(err)
}
// init cronjob
Jobs = cron.New()
Jobs.AddFunc(settingsRecords[0].GetString("interval"), func() {
CronPing = cron.New()
CronPing.AddFunc(settingsRecords[0].GetString("interval"), func() {
// skip cron if no realtime clients connected
realtimeClients := len(app.SubscriptionsBroker().Clients())
if realtimeClients == 0 {
@@ -70,5 +71,31 @@ func RunCron(app *pocketbase.PocketBase) {
}(device)
}
})
Jobs.Run()
CronPing.Run()
}
func RunWakeShutdown() {
CronWakeShutdown = cron.New()
for _, device := range Devices {
wake_cron := device.GetString("wake_cron")
wake_cron_enabled := device.GetBool("wake_cron_enabled")
shutdown_cron := device.GetString("shutdown_cron")
shutdown_cron_enabled := device.GetBool("wake_cron_enabled")
if wake_cron_enabled && wake_cron != "" {
CronWakeShutdown.AddFunc(wake_cron, func() {
if err := networking.WakeDevice(device); err != nil {
logger.Error.Println(err)
}
})
}
if shutdown_cron_enabled && shutdown_cron != "" {
CronWakeShutdown.AddFunc(shutdown_cron, func() {
if err := networking.ShutdownDevice(device); err != nil {
logger.Error.Println(err)
}
})
}
}
}

View File

@@ -13,8 +13,8 @@ func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `{
"id": "z5lghx2r3tm45n1",
"created": "2023-01-27 21:09:32.861Z",
"updated": "2023-01-27 21:09:32.861Z",
"created": "2023-01-28 10:34:30.781Z",
"updated": "2023-01-28 10:34:30.781Z",
"name": "devices",
"type": "base",
"system": false,
@@ -122,6 +122,15 @@ func init() {
"pattern": ""
}
},
{
"system": false,
"id": "dcd3vuc3",
"name": "wake_cron_enabled",
"type": "bool",
"required": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "91bs6clw",
@@ -135,6 +144,15 @@ func init() {
"pattern": ""
}
},
{
"system": false,
"id": "vnlymcuw",
"name": "shutdown_cron_enabled",
"type": "bool",
"required": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "1a7yrwo9",

View File

@@ -13,8 +13,8 @@ func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `{
"id": "cti4l8f4mz8df3r",
"created": "2023-01-27 21:09:32.861Z",
"updated": "2023-01-27 21:09:32.861Z",
"created": "2023-01-28 10:34:30.781Z",
"updated": "2023-01-28 10:34:30.781Z",
"name": "ports",
"type": "base",
"system": false,

View File

@@ -13,8 +13,8 @@ func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `{
"id": "nmj3ko20gzkg8n3",
"created": "2023-01-27 21:09:32.861Z",
"updated": "2023-01-27 21:09:32.861Z",
"created": "2023-01-28 10:34:30.781Z",
"updated": "2023-01-28 10:34:30.781Z",
"name": "settings",
"type": "base",
"system": false,

View File

@@ -22,8 +22,8 @@ func init() {
}, func(db dbx.Builder) error {
jsonData := `{
"id": "_pb_users_auth_",
"created": "2023-01-27 21:08:42.303Z",
"updated": "2023-01-27 21:08:42.304Z",
"created": "2023-01-28 10:33:57.177Z",
"updated": "2023-01-28 10:33:57.178Z",
"name": "users",
"type": "auth",
"system": false,

View File

@@ -0,0 +1,28 @@
package networking
import (
"errors"
"os/exec"
"time"
"github.com/pocketbase/pocketbase/models"
)
func ShutdownDevice(device *models.Record) error {
shutdown_cmd := device.GetString("shutdown_cmd")
if shutdown_cmd != "" {
cmd := exec.Command(shutdown_cmd)
if err := cmd.Run(); err != nil {
return err
}
}
// we wait 1 minute for the device to come up
// after that, we check the state
time.Sleep(1 * time.Minute)
isOnline := PingDevice(device)
if isOnline {
return errors.New("device not offline after 1 min")
}
return nil
}

View File

@@ -1,17 +1,16 @@
package networking
import (
"errors"
"time"
"github.com/pocketbase/pocketbase/models"
"github.com/seriousm4x/upsnap/backend/logger"
)
func WakeDevice(device *models.Record) bool {
func WakeDevice(device *models.Record) error {
err := SendMagicPacket(device)
if err != nil {
logger.Error.Println(err)
return false
return err
}
// we wait 1 minute for the device to come up
@@ -19,8 +18,7 @@ func WakeDevice(device *models.Record) bool {
time.Sleep(1 * time.Minute)
isOnline := PingDevice(device)
if isOnline {
return true
} else {
return false
return nil
}
return errors.New("device not online after 1 min")
}

View File

@@ -24,11 +24,10 @@ func HandlerWake(c echo.Context) error {
go func(*models.Record) {
record.Set("status", "pending")
App.Dao().SaveRecord(record)
isOnline := networking.WakeDevice(record)
if isOnline {
record.Set("status", "online")
} else {
if err := networking.WakeDevice(record); err != nil {
record.Set("status", "offline")
} else {
record.Set("status", "online")
}
App.Dao().SaveRecord(record)
}(record)
@@ -40,13 +39,9 @@ func HandlerShutdown(c echo.Context) error {
if err != nil {
return apis.NewNotFoundError("The device does not exist.", err)
}
shutdown_cmd := record.GetString("shutdown_cmd")
if shutdown_cmd != "" {
cmd := exec.Command(shutdown_cmd)
if err := cmd.Run(); err != nil {
logger.Error.Println(err)
return apis.NewBadRequestError(err.Error(), record)
}
if err := networking.ShutdownDevice(record); err != nil {
logger.Error.Println(err)
return apis.NewBadRequestError(err.Error(), record)
}
return nil
}

View File

@@ -74,23 +74,29 @@ func StartPocketBase() {
return err
}
// run ping cronjob
go cronjobs.RunCron(App)
// run cronjobs
go cronjobs.RunPing(App)
go cronjobs.RunWakeShutdown()
// restart ping cronjobs or wake/shutdown cronjobs on model update
// add event hook before starting server.
// using this outside App.OnBeforeServe() would not work
App.OnModelAfterUpdate().Add(func(e *core.ModelEvent) error {
if e.Model.TableName() == "settings" {
for _, job := range cronjobs.Jobs.Entries() {
cronjobs.Jobs.Remove(job.ID)
for _, job := range cronjobs.CronPing.Entries() {
cronjobs.CronPing.Remove(job.ID)
}
go cronjobs.RunCron(App)
} else {
go cronjobs.RunPing(App)
} else if e.Model.TableName() == "devices" {
logger.Debug.Println(e.Model)
refreshDeviceList()
for _, job := range cronjobs.CronWakeShutdown.Entries() {
cronjobs.CronWakeShutdown.Remove(job.ID)
}
go cronjobs.RunWakeShutdown()
}
return nil
})
return nil
})

View File

@@ -1,7 +1,14 @@
<script>
import { dev } from '$app/environment';
import { parseISO, formatDistance } from 'date-fns';
import { faPowerOff, faEllipsisVertical, faCircle } from '@fortawesome/free-solid-svg-icons';
import {
faPowerOff,
faEllipsisVertical,
faCircle,
faCircleUp,
faCircleDown,
faLock
} from '@fortawesome/free-solid-svg-icons';
import Fa from 'svelte-fa';
export let device;
@@ -96,6 +103,39 @@
{/each}
</div>
{/if}
{#if device.wake_cron !== ''}
<span
class="badge rounded-pill {device.wake_cron_enabled
? 'text-bg-success'
: 'text-bg-danger'}"
data-toggle="tooltip"
title="Wake cron {device.wake_cron_enabled ? 'enabled' : 'disabled'}"
>
<Fa icon={faCircleUp} class="me-1" />
{device.wake_cron}
</span>
{/if}
{#if device.shutdown_cron !== ''}
<span
class="badge rounded-pill {device.shutdown_cron_enabled
? 'text-bg-success'
: 'text-bg-danger'}"
data-toggle="tooltip"
title="Shutdown cron {device.shutdown_cron_enabled ? 'enabled' : 'disabled'}"
>
<Fa icon={faCircleDown} class="me-1" />
{device.shutdown_cron}
</span>
{/if}
{#if device.password !== ''}
<span
class="badge rounded-pill text-bg-secondary"
data-toggle="tooltip"
title="Wake password set"
>
<Fa icon={faLock} class="mx-1" />
</span>
{/if}
<p class="text-muted m-0">
{formatDistance(parseISO(device.updated), now, {
includeSeconds: true,

View File

@@ -252,6 +252,17 @@
type="text"
bind:value={device.wake_cron}
/>
<div class="input-group-text form-switch">
<label class="form-check-label" for="wake-cron-enabled">Enable</label>
<input
id="wake-cron-enabled"
class="form-check-input mt-0 ms-1"
type="checkbox"
role="switch"
aria-label="Enable/Disable wake cron"
bind:checked={device.wake_cron_enabled}
/>
</div>
</div>
<div class="input-group mb-1">
<span class="input-group-text">Shutdown Cron<sup>(1)</sup></span>
@@ -263,6 +274,17 @@
type="text"
bind:value={device.shutdown_cron}
/>
<div class="input-group-text form-switch">
<label class="form-check-label" for="wake-shutdown-enabled">Enable</label>
<input
id="wake-shutdown-enabled"
class="form-check-input mt-0 ms-1"
type="checkbox"
role="switch"
aria-label="Enable/Disable wake cron"
bind:checked={device.shutdown_cron_enabled}
/>
</div>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Shutdown Cmd<sup>(2)</sup></span>

View File

@@ -18,6 +18,9 @@
const result = await pb.collection('devices').getOne(id, {
expand: 'ports'
});
if (!result.expand.ports) {
result.expand.ports = [];
}
device = result;
});
</script>

View File

@@ -28,7 +28,9 @@
ports: [],
link: '',
wake_cron: '',
wake_cron_enabled: false,
shutdown_cron: '',
shutdown_cron_enabled: false,
shutdown_cmd: '',
password: ''
};
@@ -115,8 +117,11 @@
netmask: device.netmask,
ports: thisDevicePorts,
link: device.link,
wake: device.wake,
shutdown: device.shutdown
wake_cron: device.wake.cron,
wake_cron_enabled: device.wake.enabled,
shutdown_cron: device.shutdown.cron,
shutdown_cron_enabled: device.shutdown.cron.enabled,
shutdown_cmd: device.shutdown.command
});
}
};

View File

@@ -40,6 +40,7 @@ $dropdown-min-width: 0;
@import '../../node_modules/bootstrap/scss/dropdown';
@import '../../node_modules/bootstrap/scss/buttons';
@import '../../node_modules/bootstrap/scss/spinners';
@import '../../node_modules/bootstrap/scss/badge';
@import '../../node_modules/bootstrap/scss/utilities/api';