Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e3174ad
refactoring handler darwin
creativeprojects Mar 10, 2025
00f7367
Merge branch 'master' into schedule_user_logged_in
creativeprojects Mar 11, 2025
a6f7741
refactoring string permission into an enum
creativeprojects Mar 11, 2025
f478aa1
fix tests
creativeprojects Mar 12, 2025
e789d0d
trying to use bootstrap commands
creativeprojects Mar 14, 2025
4ae52b3
investigations
creativeprojects Mar 15, 2025
2426fdd
fix permissions
creativeprojects Mar 15, 2025
3aff71d
fix launchctl system job
creativeprojects Mar 15, 2025
ace0195
parse launchctl print
creativeprojects Mar 16, 2025
a0b4d1a
print schedule status
creativeprojects Mar 16, 2025
e2ec6f7
remove agent before re-creating
creativeprojects Mar 17, 2025
5c5523a
update documentation with new features on launchd
creativeprojects Mar 17, 2025
523076f
check permission on cron scheduler
creativeprojects Mar 17, 2025
3db6a86
add user permission on test profile
creativeprojects Mar 17, 2025
223be77
move permission check to handler (as crond is different than systemd)
creativeprojects Mar 18, 2025
09e6f79
add tests on DetectSchedulePermission
creativeprojects Mar 18, 2025
6001fad
docs: inform we can set the cron user manually in config
creativeprojects Mar 18, 2025
f0002b4
detect user after sudo
creativeprojects Mar 18, 2025
593536c
fix getting username on windows
creativeprojects Mar 18, 2025
9041150
use root user on system jobs
creativeprojects Mar 19, 2025
0f423d5
docs: explain how systemd services are generated
creativeprojects Mar 19, 2025
a986823
generate systemd service with User field
creativeprojects Mar 19, 2025
8712bcd
don't display epoch when there's no next run
creativeprojects Mar 24, 2025
51be91b
fix wording
creativeprojects Mar 24, 2025
39fc1b2
detect systemd service type (system or user)
creativeprojects Mar 27, 2025
212917d
systemd unit type system for background user
creativeprojects Mar 27, 2025
8b27b43
add a few new tests
creativeprojects Mar 27, 2025
6335fcc
simplify String method
creativeprojects Mar 27, 2025
0dcacaf
add user parameter to CheckPermission
creativeprojects Mar 27, 2025
3d8f5db
remove support for crond on windows
creativeprojects Mar 27, 2025
49a9801
fix tests references to crond for windows
creativeprojects Mar 27, 2025
85514ca
Merge branch 'master' into schedule_user_logged_in
creativeprojects Mar 27, 2025
fe28b9c
avoid checking links not already available :facepalm
creativeprojects Mar 27, 2025
1286704
nitpick comments
creativeprojects Mar 28, 2025
659e422
add a few more tests
creativeprojects Mar 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/doc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
with:
url: "https://${{ env.BRANCH_NAME }}.resticprofile.pages.dev/"
pages_path: ./public/
cmd_params: '--exclude="(linux.die.net|stackoverflow.com|scoop.sh)" --buffer-size=8192 --max-connections=10 --color=always --skip-tls-verification --header="User-Agent:curl/7.54.0" --timeout=20'
cmd_params: '--exclude="(linux\.die\.net|stackoverflow\.com|scoop\.sh|0-18)" --buffer-size=8192 --max-connections=10 --color=always --skip-tls-verification --header="User-Agent:curl/7.54.0" --timeout=20'

- name: Publish to pages.dev
continue-on-error: true # secrets are not set for PRs from forks
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-doc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
with:
url: https://creativeprojects.github.io/resticprofile/
pages_path: ./www/
cmd_params: '--exclude="(linux.die.net|stackoverflow.com|scoop.sh)" --buffer-size=8192 --max-connections-per-host=5 --rate-limit=5 --timeout=20 --header="User-Agent:curl/7.54.0" --skip-tls-verification'
cmd_params: '--exclude="(linux\.die\.net|stackoverflow\.com|scoop\.sh|0-18)" --buffer-size=8192 --max-connections-per-host=5 --rate-limit=5 --timeout=20 --header="User-Agent:curl/7.54.0" --skip-tls-verification'

- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

.DS_Store
/examples/private
/examples/*-log.txt
/build/restic*
/build/rclone*
/dist
/crontab

# binary
/resticprofile*
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ ${TMP_MOUNT_DARWIN}:
# Mount tmpfs on linux
${TMP_MOUNT_LINUX}:
mkdir -p ${TMP_MOUNT_LINUX}
sudo mount -t tmpfs -o "rw,relatime,size=2097152k" tmpfs ${TMP_MOUNT_LINUX}
sudo mount -t tmpfs -o "rw,relatime,size=2097152k,uid=`id -u`,gid=`id -g`" tmpfs ${TMP_MOUNT_LINUX}

rest-server:
@echo "[*] $@"
Expand Down Expand Up @@ -298,7 +298,7 @@ checkdoc:
.PHONY: checklinks
checklinks:
@echo "[*] $@"
muffet -b 8192 --max-connections=10 --exclude="(linux.die.net|stackoverflow.com|scoop.sh)" http://localhost:1313/resticprofile/
muffet -b 8192 --max-connections=10 --exclude="(linux\.die\.net|stackoverflow\.com|scoop\.sh|0-18)" http://localhost:1313/resticprofile/

.PHONY: lint
lint: $(GOBIN)/golangci-lint
Expand Down
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ignore:
- syslog.go
- syslog_windows.go
- schtasks/permission.go
- user/user_systemd.go
- "**/mocks/*.go"

codecov:
Expand Down
4 changes: 4 additions & 0 deletions commands_schedule_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package main

import (
Expand Down Expand Up @@ -40,11 +42,13 @@ profiles:
profile-schedule-inline:
backup:
schedule: "*:00,30"
schedule-permission: "user"

profile-schedule-struct:
backup:
schedule:
at: "*:20,50"
permission: "user"

`

Expand Down
8 changes: 5 additions & 3 deletions constants/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package constants

// Parameter values
const (
SchedulePermissionAuto = "auto"
SchedulePermissionSystem = "system"
SchedulePermissionUser = "user"
SchedulePermissionUserLoggedOn = "user_logged_on"
SchedulePermissionUserLoggedIn = "user_logged_in"
SchedulePermissionSystem = "system"
SchedulePriorityBackground = "background"
SchedulePriorityStandard = "standard"

SchedulePriorityBackground = "background"
SchedulePriorityStandard = "standard"

VerbosityNone = 0
VerbosityLevel1 = 1
Expand Down
10 changes: 6 additions & 4 deletions crond/crontab.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
//go:build !windows

package crond

import (
"errors"
"fmt"
"io"
"os/user"
"regexp"
"strings"

"github.com/creativeprojects/resticprofile/user"
"github.com/spf13/afero"
)

Expand Down Expand Up @@ -172,9 +174,9 @@ func (c *Crontab) LoadCurrent() (content string, err error) {

func (c *Crontab) username() string {
if len(c.user) == 0 {
if current, err := user.Current(); err == nil {
c.user = current.Username
}
current := user.Current()
c.user = current.Username

if len(c.user) == 0 || strings.ContainsAny(c.user, "\t \n\r") {
c.user = "root"
}
Expand Down
2 changes: 2 additions & 0 deletions crond/crontab_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package crond

import (
Expand Down
2 changes: 2 additions & 0 deletions crond/entry.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package crond

import (
Expand Down
2 changes: 2 additions & 0 deletions crond/entry_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package crond

import (
Expand Down
2 changes: 2 additions & 0 deletions crond/io.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package crond

import (
Expand Down
2 changes: 2 additions & 0 deletions crond/parse_event.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package crond

import (
Expand Down
2 changes: 2 additions & 0 deletions crond/parse_event_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package crond

import (
Expand Down
6 changes: 3 additions & 3 deletions schedule/calendar_interval.go → darwin/calendar_interval.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//go:build darwin

package schedule
package darwin

import "github.com/creativeprojects/resticprofile/calendar"

Expand Down Expand Up @@ -64,7 +64,7 @@
// Day = 1
//
// Total of 1 rule
func getCalendarIntervalsFromSchedules(schedules []*calendar.Event) []CalendarInterval {
func GetCalendarIntervalsFromSchedules(schedules []*calendar.Event) []CalendarInterval {

Check warning on line 67 in darwin/calendar_interval.go

View check run for this annotation

Codecov / codecov/patch

darwin/calendar_interval.go#L67

Added line #L67 was not covered by tests
entries := make([]CalendarInterval, 0, len(schedules))
for _, schedule := range schedules {
entries = append(entries, getCalendarIntervalsFromScheduleTree(generateTreeOfSchedules(schedule))...)
Expand Down Expand Up @@ -116,7 +116,7 @@

// parseCalendarIntervals converts calendar intervals into a single calendar event.
// TODO: find a pattern on how to split into multiple events when needed
func parseCalendarIntervals(intervals []CalendarInterval) []string {
func ParseCalendarIntervals(intervals []CalendarInterval) []string {
event := calendar.NewEvent(func(e *calendar.Event) {
_ = e.Second.AddValue(0)
})
Expand Down
111 changes: 111 additions & 0 deletions darwin/calendar_interval_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//go:build darwin

package darwin

import (
"testing"

"github.com/creativeprojects/resticprofile/calendar"
"github.com/stretchr/testify/assert"
)

func TestParseCalendarIntervals(t *testing.T) {
tests := []struct {
name string
intervals []CalendarInterval
expected []string
}{
{
name: "Single interval",
intervals: []CalendarInterval{
{
intervalMinute: 30,
intervalHour: 14,
intervalWeekday: 3,
intervalDay: 15,
intervalMonth: 6,
},
},
expected: []string{"Wed *-06-15 14:30:00"},
},
{
name: "Multiple intervals",
intervals: []CalendarInterval{
{
intervalMinute: 0,
},
{
intervalMinute: 30,
},
},
expected: []string{"*-*-* *:00,30:00"},
},
{
name: "Empty intervals",
intervals: []CalendarInterval{},
expected: []string{"*-*-* *:*:00"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ParseCalendarIntervals(tt.intervals)
assert.Equal(t, tt.expected, result)
})
}
}

func TestGetCalendarIntervalsFromScheduleTree(t *testing.T) {
testData := []struct {
input string
expected []CalendarInterval
}{
{"*-*-*", []CalendarInterval{
{"Hour": 0, "Minute": 0},
}},
{"*:0,30", []CalendarInterval{
{"Minute": 0},
{"Minute": 30},
}},
{"0,12:20", []CalendarInterval{
{"Hour": 0, "Minute": 20},
{"Hour": 12, "Minute": 20},
}},
{"0,12:20,40", []CalendarInterval{
{"Hour": 0, "Minute": 20},
{"Hour": 0, "Minute": 40},
{"Hour": 12, "Minute": 20},
{"Hour": 12, "Minute": 40},
}},
{"Mon..Fri *-*-* *:0,30:00", []CalendarInterval{
{"Weekday": 1, "Minute": 0},
{"Weekday": 1, "Minute": 30},
{"Weekday": 2, "Minute": 0},
{"Weekday": 2, "Minute": 30},
{"Weekday": 3, "Minute": 0},
{"Weekday": 3, "Minute": 30},
{"Weekday": 4, "Minute": 0},
{"Weekday": 4, "Minute": 30},
{"Weekday": 5, "Minute": 0},
{"Weekday": 5, "Minute": 30},
}},
// First sunday of the month at 3:30am
{"Sun *-*-01..06 03:30:00", []CalendarInterval{
{"Day": 1, "Weekday": 0, "Hour": 3, "Minute": 30},
{"Day": 2, "Weekday": 0, "Hour": 3, "Minute": 30},
{"Day": 3, "Weekday": 0, "Hour": 3, "Minute": 30},
{"Day": 4, "Weekday": 0, "Hour": 3, "Minute": 30},
{"Day": 5, "Weekday": 0, "Hour": 3, "Minute": 30},
{"Day": 6, "Weekday": 0, "Hour": 3, "Minute": 30},
}},
}

for _, testItem := range testData {
t.Run(testItem.input, func(t *testing.T) {
event := calendar.NewEvent()
err := event.Parse(testItem.input)
assert.NoError(t, err)
assert.ElementsMatch(t, testItem.expected, getCalendarIntervalsFromScheduleTree(generateTreeOfSchedules(event)))
})
}
}
100 changes: 100 additions & 0 deletions darwin/launchd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//go:build darwin

package darwin

// LaunchdJob is an agent definition for launchd
// Documentation found from man launchd.plist(5)
type LaunchdJob struct {
// This required key uniquely identifies the job to launchd.
Label string `plist:"Label"`
// This key maps to the first argument of execv(3) and indicates the absolute path to the executable for the job. If this key is missing, then the first
// element of the array of strings provided to the ProgramArguments will be used instead. This key is required in the absence of the ProgramArguments and
// BundleProgram keys.
Program string `plist:"Program"`
// This key maps to the second argument of execvp(3) and specifies the argument vector to be passed to the job when a process is spawned. This key is
// required in the absence of the Program key. IMPORTANT: Many people are confused by this key. Please read execvp(3) very carefully!
//
// NOTE: The Program key must be an absolute path. Previous versions of launchd did not enforce this requirement but failed to run the job. In the absence
// of the Program key, the first element of the ProgramArguments array may be either an absolute path, or a relative path which is resolved using
// _PATH_STDPATH.
ProgramArguments []string `plist:"ProgramArguments"`
// This optional key is used to specify additional environmental variables to be set before running the job. Each key in the dictionary is the name of an
// environment variable, with the corresponding value being a string representing the desired value. NOTE: Values other than strings will be ignored.
EnvironmentVariables map[string]string `plist:"EnvironmentVariables,omitempty"`
// This optional key specifies that the given path should be mapped to the job's stdin(4), and that the contents of that file will be readable from the
// job's stdin(4). If the file does not exist, no data will be delivered to the process' stdin(4).
StandardInPath string `plist:"StandardInPath,omitempty"`
// This optional key specifies that the given path should be mapped to the job's stdout(4), and that any writes to the job's stdout(4) will go to the given
// file. If the file does not exist, it will be created with writable permissions and ownership reflecting the user and/or group specified as the UserName
// and/or GroupName, respectively (if set) and permissions reflecting the umask(2) specified by the Umask key, if set.
StandardOutPath string `plist:"StandardOutPath,omitempty"`
// This optional key specifies that the given path should be mapped to the job's stderr(4), and that any writes to the job's stderr(4) will go to the given
// file. Note that this file is opened as readable and writable as mandated by the POSIX specification for unclear reasons. If the file does not exist, it
// will be created with ownership reflecting the user and/or group specified as the UserName and/or GroupName, respectively (if set) and permissions
// reflecting the umask(2) specified by the Umask key, if set.
StandardErrorPath string `plist:"StandardErrorPath,omitempty"`
// This optional key is used to specify a directory to chdir(2) to before running the job.
WorkingDirectory string `plist:"WorkingDirectory"`
// This optional key causes the job to be started every calendar interval as specified. Missing arguments are considered to be wildcard. The semantics are
// similar to crontab(5) in how firing dates are specified. Multiple dictionaries may be specified in an array to schedule multiple calendar intervals.
//
// Unlike cron which skips job invocations when the computer is asleep, launchd will start the job the next time the computer wakes up. If multiple
// intervals transpire before the computer is woken, those events will be coalesced into one event upon wake from sleep.
//
// Note that StartInterval and StartCalendarInterval are not aware of each other. They are evaluated completely independently by the system.
//
// Minute <integer>
// The minute (0-59) on which this job will be run.
//
// Hour <integer>
// The hour (0-23) on which this job will be run.
//
// Day <integer>
// The day of the month (1-31) on which this job will be run.
//
// Weekday <integer>
// The weekday on which this job will be run (0 and 7 are Sunday). If both Day and Weekday are specificed, then the job will be started if either one
// matches the current date.
//
// Month <integer>
// The month (1-12) on which this job will be run.
StartCalendarInterval []CalendarInterval `plist:"StartCalendarInterval,omitempty"`
// ProcessType
// This optional key describes, at a high level, the intended purpose of the job. The system will apply resource limits based on what kind of job it is. If
// left unspecified, the system will apply light resource limits to the job, throttling its CPU usage and I/O bandwidth. This classification is preferable
// to using the HardResourceLimits, SoftResourceLimits and Nice keys. The following are valid values:
//
// Background
// Background jobs are generally processes that do work that was not directly requested by the user. The resource limits applied to Background jobs
// are intended to prevent them from disrupting the user experience.
//
// Standard
// Standard jobs are equivalent to no ProcessType being set.
//
// Adaptive
// Adaptive jobs move between the Background and Interactive classifications based on activity over XPC connections. See xpc_transaction_begin(3) for
// details.
//
// Interactive
// Interactive jobs run with the same resource limitations as apps, that is to say, none. Interactive jobs are critical to maintaining a responsive
// user experience, and this key should only be used if an app's ability to be responsive depends on it, and cannot be made Adaptive.
ProcessType ProcessType `plist:"ProcessType"`
// This optional key specifies whether the kernel should consider this daemon to be low priority when doing filesystem I/O.
LowPriorityIO bool `plist:"LowPriorityIO"`
// This optional key specifies whether the kernel should consider this daemon to be low priority when doing filesystem I/O when
// the process is throttled with the Darwin-background classification.
LowPriorityBackgroundIO bool `plist:"LowPriorityBackgroundIO"`
// This optional key specifies what nice(3) value should be applied to the daemon.
Nice int `plist:"Nice"`
// Aqua:
// a GUI agent; has access to all the GUI services
// LoginWindow:
// pre-login agent; runs in the login window context
// Background:
// runs in the parent context of the user
// StandardIO:
// runs only in non-GUI login session (e.g. SSH sessions)
// System:
// runs in the system context
LimitLoadToSessionType SessionType `plist:"LimitLoadToSessionType,omitempty"`
}
Loading