@@ -11,7 +11,10 @@ import (
11
11
"os"
12
12
"os/exec"
13
13
"path"
14
+ "regexp"
15
+ "sort"
14
16
"strings"
17
+ "text/tabwriter"
15
18
16
19
"github.com/creativeprojects/resticprofile/calendar"
17
20
"github.com/creativeprojects/resticprofile/constants"
@@ -31,8 +34,9 @@ const (
31
34
GlobalAgentPath = "/Library/LaunchAgents"
32
35
GlobalDaemons = "/Library/LaunchDaemons"
33
36
34
- namePrefix = "local.resticprofile"
35
- agentExtension = ".agent.plist"
37
+ namePrefix = "local.resticprofile"
38
+ agentExtension = ".agent.plist"
39
+ daemonExtension = ".plist"
36
40
37
41
codeServiceNotFound = 113
38
42
)
@@ -89,10 +93,37 @@ func Close() {
89
93
90
94
// createJob creates a plist file and register it with launchd
91
95
func (j * Job ) createJob (schedules []* calendar.Event ) error {
92
- ok := j .checkPermission (j .config .Permission ())
96
+ permission := j .getSchedulePermission ()
97
+ ok := j .checkPermission (permission )
93
98
if ! ok {
94
99
return errors .New ("user is not allowed to create a system job: please restart resticprofile as root (with sudo)" )
95
100
}
101
+ filename , err := j .createPlistFile (schedules )
102
+ if err != nil {
103
+ if filename != "" {
104
+ os .Remove (filename )
105
+ }
106
+ return err
107
+ }
108
+
109
+ j .fixFileOwner (filename )
110
+ if err != nil {
111
+ return err
112
+ }
113
+
114
+ // load the service
115
+ cmd := exec .Command (launchctlBin , commandLoad , filename )
116
+ cmd .Stdout = os .Stdout
117
+ cmd .Stderr = os .Stderr
118
+ err = cmd .Run ()
119
+ if err != nil {
120
+ return err
121
+ }
122
+
123
+ return nil
124
+ }
125
+
126
+ func (j * Job ) createPlistFile (schedules []* calendar.Event ) (string , error ) {
96
127
name := getJobName (j .config .Title (), j .config .SubTitle ())
97
128
job := & LaunchJob {
98
129
Label : name ,
@@ -105,94 +136,110 @@ func (j *Job) createJob(schedules []*calendar.Event) error {
105
136
StartCalendarInterval : getCalendarIntervalsFromSchedules (schedules ),
106
137
}
107
138
108
- filename , err := getFilename (name , j .config . Permission ())
139
+ filename , err := getFilename (name , j .getSchedulePermission ())
109
140
if err != nil {
110
- return err
141
+ return "" , err
111
142
}
112
143
file , err := os .Create (filename )
113
144
if err != nil {
114
- return err
145
+ return "" , err
115
146
}
116
147
defer file .Close ()
117
148
118
149
encoder := plist .NewEncoder (file )
119
150
encoder .Indent ("\t " )
120
151
err = encoder .Encode (job )
121
152
if err != nil {
122
- return err
153
+ return filename , err
123
154
}
155
+ return filename , nil
156
+ }
124
157
125
- // load the service
126
- cmd := exec .Command (launchctlBin , commandLoad , filename )
127
- cmd .Stdout = os .Stdout
128
- cmd .Stderr = os .Stderr
129
- err = cmd .Run ()
130
- if err != nil {
131
- return err
158
+ // fixFileOwner gives the owner back to the user
159
+ func (j * Job ) fixFileOwner (filename string ) error {
160
+ if j .getSchedulePermission () == constants .SchedulePermissionSystem {
161
+ return nil
132
162
}
133
-
134
- return nil
163
+ if os .Geteuid () != 0 {
164
+ return nil
165
+ }
166
+ // this is the case of a launchd agent supposed to be of type user, but created by root
167
+ // well it doesn't even seem to work anyway
168
+ return os .Chown (filename , os .Getuid (), os .Getgid ())
135
169
}
136
170
137
171
// removeJob stops and unloads the agent from launchd, then removes the configuration file
138
172
func (j * Job ) removeJob () error {
139
- ok := j .checkPermission (j .config .Permission ())
173
+ permission := j .getSchedulePermission ()
174
+ ok := j .checkPermission (permission )
140
175
if ! ok {
141
176
return errors .New ("user is not allowed to remove a system job: please restart resticprofile as root (with sudo)" )
142
177
}
143
178
name := getJobName (j .config .Title (), j .config .SubTitle ())
144
- filename , err := getFilename (name , j .config . Permission ())
179
+ filename , err := getFilename (name , j .getSchedulePermission ())
145
180
if err != nil {
146
181
return err
147
182
}
148
183
149
- if _ , err := os .Stat (filename ); err == nil || os .IsExist (err ) {
150
- // stop the service in case it's already running
151
- stop := exec .Command (launchctlBin , commandStop , name )
152
- stop .Stdout = os .Stdout
153
- stop .Stderr = os .Stderr
154
- // keep going if there's an error here
155
- _ = stop .Run ()
156
-
157
- // unload the service
158
- unload := exec .Command (launchctlBin , commandUnload , filename )
159
- unload .Stdout = os .Stdout
160
- unload .Stderr = os .Stderr
161
- err = unload .Run ()
162
- if err != nil {
163
- return err
164
- }
165
- err = os .Remove (filename )
166
- if err != nil {
167
- return err
168
- }
184
+ if _ , err := os .Stat (filename ); err != nil && os .IsNotExist (err ) {
185
+ return ErrorServiceNotFound
186
+ }
187
+ // stop the service in case it's already running
188
+ stop := exec .Command (launchctlBin , commandStop , name )
189
+ stop .Stdout = os .Stdout
190
+ stop .Stderr = os .Stderr
191
+ // keep going if there's an error here
192
+ _ = stop .Run ()
193
+
194
+ // unload the service
195
+ unload := exec .Command (launchctlBin , commandUnload , filename )
196
+ unload .Stdout = os .Stdout
197
+ unload .Stderr = os .Stderr
198
+ err = unload .Run ()
199
+ if err != nil {
200
+ return err
201
+ }
202
+ err = os .Remove (filename )
203
+ if err != nil {
204
+ return err
169
205
}
170
206
171
207
return nil
172
208
}
173
209
174
210
func (j * Job ) displayStatus (command string ) error {
211
+ permission := j .getSchedulePermission ()
212
+ ok := j .checkPermission (permission )
213
+ if ! ok {
214
+ return errors .New ("user is not allowed view a system job: please restart resticprofile as root (with sudo)" )
215
+ }
175
216
cmd := exec .Command (launchctlBin , commandList , getJobName (j .config .Title (), j .config .SubTitle ()))
176
- cmd .Stdout = os .Stdout
177
- cmd .Stderr = os .Stderr
178
- err := cmd .Run ()
217
+ output , err := cmd .Output ()
179
218
if cmd .ProcessState .ExitCode () == codeServiceNotFound {
180
219
return ErrorServiceNotFound
181
220
}
182
- return err
183
- }
184
-
185
- func ( j * Job ) checkPermission ( permission string ) bool {
186
- if permission == constants . SchedulePermissionUser {
187
- // user mode is always available
188
- return true
221
+ if err != nil {
222
+ return err
223
+ }
224
+ status := parseStatus ( string ( output ))
225
+ if len ( status ) == 0 {
226
+ // output was not parsed, it could mean output format has changed
227
+ fmt . Println ( string ( output ))
189
228
}
190
- if os .Geteuid () == 0 {
191
- // user has sudoed
192
- return true
229
+ // order keys alphabetically
230
+ keys := make ([]string , 0 , len (status ))
231
+ for key := range status {
232
+ keys = append (keys , key )
193
233
}
194
- // last case is system (or undefined) + no sudo
195
- return false
234
+ sort .Strings (keys )
235
+ writer := tabwriter .NewWriter (os .Stdout , 0 , 0 , 0 , ' ' , tabwriter .AlignRight )
236
+ for _ , key := range keys {
237
+ fmt .Fprintf (writer , "%s:\t %s\n " , key , status [key ])
238
+ }
239
+ writer .Flush ()
240
+ fmt .Println ("" )
241
+
242
+ return nil
196
243
}
197
244
198
245
func getJobName (profileName , command string ) string {
@@ -201,7 +248,7 @@ func getJobName(profileName, command string) string {
201
248
202
249
func getFilename (name , permission string ) (string , error ) {
203
250
if permission == constants .SchedulePermissionSystem {
204
- return path .Join (GlobalDaemons , name + agentExtension ), nil
251
+ return path .Join (GlobalDaemons , name + daemonExtension ), nil
205
252
}
206
253
home , err := os .UserHomeDir ()
207
254
if err != nil {
@@ -279,3 +326,16 @@ func setCalendarIntervalValueFromType(entry *CalendarInterval, value int, typeVa
279
326
(* entry )["Minute" ] = value
280
327
}
281
328
}
329
+
330
+ func parseStatus (status string ) map [string ]string {
331
+ expr := regexp .MustCompile (`^\s*"(\w+)"\s*=\s*(.*);$` )
332
+ lines := strings .Split (status , "\n " )
333
+ output := make (map [string ]string , len (lines ))
334
+ for _ , line := range lines {
335
+ match := expr .FindStringSubmatch (line )
336
+ if len (match ) == 3 {
337
+ output [match [1 ]] = strings .Trim (match [2 ], "\" " )
338
+ }
339
+ }
340
+ return output
341
+ }
0 commit comments