Skip to content

Commit f7b8efd

Browse files
Added crontab scheduler for jiggler (#316)
1 parent 33ac9fe commit f7b8efd

File tree

11 files changed

+425
-73
lines changed

11 files changed

+425
-73
lines changed

config.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ type Config struct {
8282
CloudToken string `json:"cloud_token"`
8383
GoogleIdentity string `json:"google_identity"`
8484
JigglerEnabled bool `json:"jiggler_enabled"`
85+
JigglerConfig *JigglerConfig `json:"jiggler_config"`
8586
AutoUpdateEnabled bool `json:"auto_update_enabled"`
8687
IncludePreRelease bool `json:"include_pre_release"`
8788
HashedPassword string `json:"hashed_password"`
@@ -117,7 +118,13 @@ var defaultConfig = &Config{
117118
DisplayMaxBrightness: 64,
118119
DisplayDimAfterSec: 120, // 2 minutes
119120
DisplayOffAfterSec: 1800, // 30 minutes
120-
TLSMode: "",
121+
// This is the "Standard" jiggler option in the UI
122+
JigglerConfig: &JigglerConfig{
123+
InactivityLimitSeconds: 60,
124+
JitterPercentage: 25,
125+
ScheduleCronTab: "0 * * * * *",
126+
},
127+
TLSMode: "",
121128
UsbConfig: &usbgadget.Config{
122129
VendorId: "0x1d6b", //The Linux Foundation
123130
ProductId: "0x0104", //Multifunction Composite Gadget

go.mod

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/fsnotify/fsnotify v1.9.0
1212
github.com/gin-contrib/logger v1.2.6
1313
github.com/gin-gonic/gin v1.10.1
14+
github.com/go-co-op/gocron/v2 v2.16.3
1415
github.com/google/uuid v1.6.0
1516
github.com/guregu/null/v6 v6.0.0
1617
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341
@@ -28,9 +29,9 @@ require (
2829
github.com/stretchr/testify v1.10.0
2930
github.com/vishvananda/netlink v1.3.1
3031
go.bug.st/serial v1.6.4
31-
golang.org/x/crypto v0.39.0
32+
golang.org/x/crypto v0.40.0
3233
golang.org/x/net v0.41.0
33-
golang.org/x/sys v0.33.0
34+
golang.org/x/sys v0.34.0
3435
)
3536

3637
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
@@ -50,6 +51,7 @@ require (
5051
github.com/go-playground/universal-translator v0.18.1 // indirect
5152
github.com/go-playground/validator/v10 v10.26.0 // indirect
5253
github.com/goccy/go-json v0.10.5 // indirect
54+
github.com/jonboulle/clockwork v0.5.0 // indirect
5355
github.com/json-iterator/go v1.1.12 // indirect
5456
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
5557
github.com/leodido/go-urn v1.4.0 // indirect
@@ -75,14 +77,15 @@ require (
7577
github.com/pion/turn/v4 v4.0.2 // indirect
7678
github.com/pmezard/go-difflib v1.0.0 // indirect
7779
github.com/prometheus/client_model v0.6.2 // indirect
80+
github.com/robfig/cron/v3 v3.0.1 // indirect
7881
github.com/rogpeppe/go-internal v1.14.1 // indirect
7982
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
8083
github.com/ugorji/go/codec v1.3.0 // indirect
8184
github.com/vishvananda/netns v0.0.5 // indirect
8285
github.com/wlynxg/anet v0.0.5 // indirect
8386
golang.org/x/arch v0.18.0 // indirect
8487
golang.org/x/oauth2 v0.30.0 // indirect
85-
golang.org/x/text v0.26.0 // indirect
88+
golang.org/x/text v0.27.0 // indirect
8689
google.golang.org/protobuf v1.36.6 // indirect
8790
gopkg.in/yaml.v3 v3.0.1 // indirect
8891
)

go.sum

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
3838
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
3939
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
4040
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
41+
github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
42+
github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
4143
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
4244
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
4345
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -62,6 +64,8 @@ github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341 h1:zPrkLSKi7kKJoN
6264
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
6365
github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs=
6466
github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
67+
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
68+
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
6569
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
6670
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
6771
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -146,6 +150,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
146150
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
147151
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
148152
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
153+
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
154+
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
149155
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
150156
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
151157
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
@@ -175,10 +181,12 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
175181
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
176182
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
177183
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
184+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
185+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
178186
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
179187
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
180-
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
181-
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
188+
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
189+
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
182190
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
183191
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
184192
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
@@ -188,10 +196,10 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
188196
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
189197
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
190198
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
191-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
192-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
193-
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
194-
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
199+
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
200+
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
201+
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
202+
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
195203
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
196204
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
197205
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

jiggler.go

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
package kvm
22

33
import (
4+
"fmt"
5+
"math/rand"
46
"time"
7+
8+
"github.com/go-co-op/gocron/v2"
59
)
610

7-
var lastUserInput = time.Now()
11+
type JigglerConfig struct {
12+
InactivityLimitSeconds int `json:"inactivity_limit_seconds"`
13+
JitterPercentage int `json:"jitter_percentage"`
14+
ScheduleCronTab string `json:"schedule_cron_tab"`
15+
}
816

917
var jigglerEnabled = false
18+
var jobDelta time.Duration = 0
19+
var scheduler gocron.Scheduler = nil
1020

1121
func rpcSetJigglerState(enabled bool) {
1222
jigglerEnabled = enabled
@@ -15,25 +25,112 @@ func rpcGetJigglerState() bool {
1525
return jigglerEnabled
1626
}
1727

28+
func rpcGetJigglerConfig() (JigglerConfig, error) {
29+
return *config.JigglerConfig, nil
30+
}
31+
32+
func rpcSetJigglerConfig(jigglerConfig JigglerConfig) error {
33+
logger.Info().Msgf("jigglerConfig: %v, %v, %v", jigglerConfig.InactivityLimitSeconds, jigglerConfig.JitterPercentage, jigglerConfig.ScheduleCronTab)
34+
config.JigglerConfig = &jigglerConfig
35+
err := removeExistingCrobJobs(scheduler)
36+
if err != nil {
37+
return fmt.Errorf("error removing cron jobs from scheduler %v", err)
38+
}
39+
err = runJigglerCronTab()
40+
if err != nil {
41+
return fmt.Errorf("error scheduling jiggler crontab: %v", err)
42+
}
43+
err = SaveConfig()
44+
if err != nil {
45+
return fmt.Errorf("failed to save config: %w", err)
46+
}
47+
return nil
48+
}
49+
50+
func removeExistingCrobJobs(s gocron.Scheduler) error {
51+
for _, j := range s.Jobs() {
52+
err := s.RemoveJob(j.ID())
53+
if err != nil {
54+
return err
55+
}
56+
}
57+
return nil
58+
}
59+
1860
func initJiggler() {
19-
go runJiggler()
61+
ensureConfigLoaded()
62+
err := runJigglerCronTab()
63+
if err != nil {
64+
logger.Error().Msgf("Error scheduling jiggler crontab: %v", err)
65+
return
66+
}
67+
}
68+
69+
func runJigglerCronTab() error {
70+
cronTab := config.JigglerConfig.ScheduleCronTab
71+
s, err := gocron.NewScheduler()
72+
if err != nil {
73+
return err
74+
}
75+
scheduler = s
76+
_, err = s.NewJob(
77+
gocron.CronJob(
78+
cronTab,
79+
true,
80+
),
81+
gocron.NewTask(
82+
func() {
83+
runJiggler()
84+
},
85+
),
86+
)
87+
if err != nil {
88+
return err
89+
}
90+
s.Start()
91+
delta, err := calculateJobDelta(s)
92+
jobDelta = delta
93+
logger.Info().Msgf("Time between jiggler runs: %v", jobDelta)
94+
if err != nil {
95+
return err
96+
}
97+
return nil
2098
}
2199

22100
func runJiggler() {
23-
for {
24-
if jigglerEnabled {
25-
if time.Since(lastUserInput) > 20*time.Second {
26-
//TODO: change to rel mouse
27-
err := rpcAbsMouseReport(1, 1, 0)
28-
if err != nil {
29-
logger.Warn().Err(err).Msg("Failed to jiggle mouse")
30-
}
31-
err = rpcAbsMouseReport(0, 0, 0)
32-
if err != nil {
33-
logger.Warn().Err(err).Msg("Failed to reset mouse position")
34-
}
101+
if jigglerEnabled {
102+
if config.JigglerConfig.JitterPercentage != 0 {
103+
jitter := calculateJitterDuration(jobDelta)
104+
time.Sleep(jitter)
105+
}
106+
inactivitySeconds := config.JigglerConfig.InactivityLimitSeconds
107+
timeSinceLastInput := time.Since(gadget.GetLastUserInputTime())
108+
logger.Debug().Msgf("Time since last user input %v", timeSinceLastInput)
109+
if timeSinceLastInput > time.Duration(inactivitySeconds)*time.Second {
110+
logger.Debug().Msg("Jiggling mouse...")
111+
//TODO: change to rel mouse
112+
err := rpcAbsMouseReport(1, 1, 0)
113+
if err != nil {
114+
logger.Warn().Msgf("Failed to jiggle mouse: %v", err)
115+
}
116+
err = rpcAbsMouseReport(0, 0, 0)
117+
if err != nil {
118+
logger.Warn().Msgf("Failed to reset mouse position: %v", err)
35119
}
36120
}
37-
time.Sleep(20 * time.Second)
38121
}
39122
}
123+
124+
func calculateJobDelta(s gocron.Scheduler) (time.Duration, error) {
125+
j := s.Jobs()[0]
126+
runs, err := j.NextRuns(2)
127+
if err != nil {
128+
return 0.0, err
129+
}
130+
return runs[1].Sub(runs[0]), nil
131+
}
132+
133+
func calculateJitterDuration(delta time.Duration) time.Duration {
134+
jitter := rand.Float64() * float64(config.JigglerConfig.JitterPercentage) / 100 * delta.Seconds()
135+
return time.Duration(jitter * float64(time.Second))
136+
}

jsonrpc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,8 @@ var rpcHandlers = map[string]RPCHandler{
10561056
"rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}},
10571057
"setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}},
10581058
"getJigglerState": {Func: rpcGetJigglerState},
1059+
"setJigglerConfig": {Func: rpcSetJigglerConfig, Params: []string{"jigglerConfig"}},
1060+
"getJigglerConfig": {Func: rpcGetJigglerConfig},
10591061
"sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}},
10601062
"getStreamQualityFactor": {Func: rpcGetStreamQualityFactor},
10611063
"setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}},

ui/src/components/FieldLabel.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default function FieldLabel({
2727
>
2828
{label}
2929
{description && (
30-
<span className="my-0.5 text-[13px] font-normal text-slate-600 dark:text-slate-400">
30+
<span className="mb-0.5 text-[13px] font-normal text-slate-600 dark:text-slate-400">
3131
{description}
3232
</span>
3333
)}
@@ -36,11 +36,11 @@ export default function FieldLabel({
3636
} else if (as === "span") {
3737
return (
3838
<div className="flex select-none flex-col">
39-
<span className="font-display text-[13px] font-medium leading-snug text-black dark:text-white">
39+
<span className="font-display text-[13px] font-semibold leading-snug text-black dark:text-white">
4040
{label}
4141
</span>
4242
{description && (
43-
<span className="my-0.5 text-[13px] font-normal text-slate-600 dark:text-slate-400">
43+
<span className="mb-0.5 text-[13px] font-normal text-slate-600 dark:text-slate-400">
4444
{description}
4545
</span>
4646
)}
@@ -49,4 +49,4 @@ export default function FieldLabel({
4949
} else {
5050
return <></>;
5151
}
52-
}
52+
}

ui/src/components/InputField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ type InputFieldProps = {
2626

2727
type InputFieldWithLabelProps = InputFieldProps & {
2828
label: React.ReactNode;
29-
description?: string | null;
29+
description?: React.ReactNode | string | null;
3030
};
3131

3232
const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputField(

0 commit comments

Comments
 (0)