-
Notifications
You must be signed in to change notification settings - Fork 1
/
storage.go
322 lines (252 loc) · 10.9 KB
/
storage.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
package main
// This is the persistant storage of program state such as which commands were executed when
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"time"
"github.com/google/uuid"
// support for locking read/write access to a file within and across processes
"github.com/gofrs/flock"
)
// Note on file locking:
// create file locks in functions and not once, so different goroutines
// create and have different locks
// and thus have to wait for the other lock on the same file being released
// Waiting for other processes also is handled by that, because file lock
// is visible to other processes through OS mechanisms
var pathToCommandStoreFile = "./commandStore.json"
// CommandStore contains all commands to be executed some time
type CommandStore struct {
Commands []CommandWithArguments
}
// TODO remove all uuid usages, cause it was replaced with name as unique identifier for now
// CommandWithArguments is a full path to a command with all arguments to it, in whole representing what should be executed
type CommandWithArguments struct {
Name string
UUID uuid.UUID
AbsolutePath string
CommandArguments []string
State CommandState
DurationBetweenRuns time.Duration
LastRun time.Time
}
// CommandState is one of the states for a command to be in, this will be saved to disk, too
type CommandState string
// states for a command to be in, this will be saved to disk, too
const (
CommandWaitingToBeRun CommandState = "WaitingToBeRun"
CommandRunning CommandState = "Running"
CommandFailed CommandState = "Failed"
CommandSuccessful CommandState = "Successful"
)
func changeStateOfCommand(uuidOfCommandToChangeState uuid.UUID, newState CommandState) error {
var fileLockOnCommandStoreFile = flock.New(pathToCommandStoreFile)
// locking for reading, modifying and writing command store
// todo handle/relay errors when locking
fileLockOnCommandStoreFile.Lock()
defer fileLockOnCommandStoreFile.Unlock()
commandStore, readError := readAndParseCommandStoreAlreadyLocked()
if readError != nil {
return readError
}
foundCommandToChangeState := false
// remove command with specified uuid from list
for index, currentCommand := range commandStore.Commands {
if currentCommand.UUID == uuidOfCommandToChangeState {
//fmt.Println("Changing state of command:", currentCommand, "to state", newState)
commandStore.Commands[index].State = newState
if newState == CommandSuccessful {
commandStore.Commands[index].LastRun = time.Now()
}
foundCommandToChangeState = true
break
}
}
if !foundCommandToChangeState {
return fmt.Errorf("UUID %v of command to change state to %v not found", uuidOfCommandToChangeState, newState)
}
writeError := marshalAndWriteCommandStore(commandStore)
// is nil on success
return writeError
}
func addCommandToCommandStore(absolutePathToExecutable string, commandArguments []string, durationBetweenExecutions time.Duration, uniqueCommandName string) (bool, error) {
hasUpdatedCommandInCommandStore := false
var fileLockOnCommandStoreFile = flock.New(pathToCommandStoreFile)
// locking for reading, modifying and writing command store
fileLockOnCommandStoreFile.Lock()
defer fileLockOnCommandStoreFile.Unlock()
commandStore, readError := readAndParseCommandStoreAlreadyLocked()
if readError != nil {
return hasUpdatedCommandInCommandStore, readError
}
uuidOfNewCommand := uuid.New()
newCommandWithArguments := CommandWithArguments{
Name: uniqueCommandName,
UUID: uuidOfNewCommand,
AbsolutePath: absolutePathToExecutable,
CommandArguments: commandArguments,
State: CommandWaitingToBeRun,
DurationBetweenRuns: durationBetweenExecutions,
// zero value of time indicates was never run before, year 1 is unlikely to come up otherwise
LastRun: time.Time{},
}
// check if command with same name was already in command store
// That could be from last run or was added by other config file already
// Just update its contents in both cases and report it was updated, not added
for index, currentCommand := range commandStore.Commands {
if currentCommand.Name == newCommandWithArguments.Name {
// overwrite values that can be specified in config
updatedCommand := updateContentsOfCommand(currentCommand, newCommandWithArguments)
fmt.Println("Updated command:", updatedCommand)
commandStore.Commands[index] = updatedCommand
hasUpdatedCommandInCommandStore = true
break
}
}
// if the command is new, we need to append it, otherwise, it was already updated
if !hasUpdatedCommandInCommandStore {
commandStore.Commands = append(commandStore.Commands, newCommandWithArguments)
}
writeError := marshalAndWriteCommandStore(commandStore)
// writeError is nil on success
return hasUpdatedCommandInCommandStore, writeError
}
// update absolute path, command arguments and duration between runs of a CommandWithArguments
func updateContentsOfCommand(oldCommand CommandWithArguments, newCommandFromConfig CommandWithArguments) CommandWithArguments {
// make real copy of struct values to keep in order to not influence values passed into this function
newCommandArguments := make([]string, len(oldCommand.CommandArguments))
copy(oldCommand.CommandArguments, newCommandArguments)
updatedCommand := CommandWithArguments{
// name should always be the same in new command anyway
Name: oldCommand.Name,
UUID: oldCommand.UUID,
AbsolutePath: newCommandFromConfig.AbsolutePath,
CommandArguments: newCommandArguments,
State: oldCommand.State,
DurationBetweenRuns: time.Duration(newCommandFromConfig.DurationBetweenRuns.Nanoseconds()),
// LastRun should stay from the old value in case it was already run, the new value can only
// come from a config and is therefore always empty
LastRun: oldCommand.LastRun,
}
return updatedCommand
}
// not needed anymore if all uuid code is removed
func removeCommandFromCommandStore(uuidOfCommandToRemove uuid.UUID) error {
var fileLockOnCommandStoreFile = flock.New(pathToCommandStoreFile)
// locking for reading, modifying and writing command store
fileLockOnCommandStoreFile.Lock()
defer fileLockOnCommandStoreFile.Unlock()
commandStore, readError := readAndParseCommandStoreAlreadyLocked()
if readError != nil {
return readError
}
foundCommandToRemove := false
// remove command with specified uuid from list
for index, currentCommand := range commandStore.Commands {
if currentCommand.UUID == uuidOfCommandToRemove {
//fmt.Println("Removing command:", currentCommand)
// overwrite current element with last element of list
commandStore.Commands[index] = commandStore.Commands[len(commandStore.Commands)-1]
// take list without the last element
commandStore.Commands = commandStore.Commands[:len(commandStore.Commands)-1]
foundCommandToRemove = true
break
}
}
if !foundCommandToRemove {
return fmt.Errorf("UUID %v of command to remove not found", uuidOfCommandToRemove)
}
writeError := marshalAndWriteCommandStore(commandStore)
// is nil on success
return writeError
}
func removeCommandFromCommandStoreByName(commandNameToRemove string) error {
var fileLockOnCommandStoreFile = flock.New(pathToCommandStoreFile)
// locking for reading, modifying and writing command store
fileLockOnCommandStoreFile.Lock()
defer fileLockOnCommandStoreFile.Unlock()
commandStore, readError := readAndParseCommandStoreAlreadyLocked()
if readError != nil {
return readError
}
foundCommandToRemove := false
// remove command with specified name from list
for index, currentCommand := range commandStore.Commands {
if currentCommand.Name == commandNameToRemove {
//fmt.Println("Removing command:", currentCommand)
// overwrite current element with last element of list
commandStore.Commands[index] = commandStore.Commands[len(commandStore.Commands)-1]
// take list without the last element
commandStore.Commands = commandStore.Commands[:len(commandStore.Commands)-1]
foundCommandToRemove = true
break
}
}
if !foundCommandToRemove {
return fmt.Errorf("Name %v of command to remove not found", commandNameToRemove)
}
writeError := marshalAndWriteCommandStore(commandStore)
// is nil on success
return writeError
}
// called internally by storage functions when changing state of command, adding or removing commands
// because these actions need to lock the file from before reading until after
// writing their changes so the read function should not take a lock again
func readAndParseCommandStoreAlreadyLocked() (CommandStore, error) {
return readAndParseCommandStoreFromFile(pathToCommandStoreFile, true)
}
// called from main program to read command store to do something with the
// commands in it
func readAndParseCommandStore() (CommandStore, error) {
return readAndParseCommandStoreFromFile(pathToCommandStoreFile, false)
}
func readAndParseCommandStoreFromFile(pathToCommandStoreFile string, alreadyLocked bool) (CommandStore, error) {
var fileLockOnCommandStoreFile = flock.New(pathToCommandStoreFile)
if !alreadyLocked {
fileLockOnCommandStoreFile.Lock()
}
commandStore := CommandStore{}
// empty file is created by this, if it does not exist
marshalledJSONData, readingError := ioutil.ReadFile(pathToCommandStoreFile)
if readingError != nil {
return commandStore, readingError
}
if !alreadyLocked {
fileLockOnCommandStoreFile.Unlock()
}
// Empty file is not valid json, so just return empty command store here before trying to unmarshal.
// A write to the command store will create valid json in the future.
if len(marshalledJSONData) == 0 {
return commandStore, readingError
}
unmarshalError := json.Unmarshal(marshalledJSONData, &commandStore)
if unmarshalError != nil {
return commandStore, unmarshalError
}
return commandStore, nil
}
// never called directly from scheduling logic, only through add command and change command functions
// so the command store file will be already locked
func marshalAndWriteCommandStore(commandStore CommandStore) error {
return marshalAndWriteCommandStoreToFile(pathToCommandStoreFile, commandStore)
}
func marshalAndWriteCommandStoreToFile(pathToCommandStoreFile string, commandStore CommandStore) error {
// prefix new lines with nothing, indent with tabs
marshalledJSONData, marshalError := json.MarshalIndent(&commandStore, "", "\t")
if marshalError != nil {
return marshalError
}
// write to / overwrite configured data store file with provided data:
// file mode is 0 meaning regular file and 600 means only readable and writeable by own user
// and executable by no one
// umask gets applied afterwards and might change permissions of created file
var permissionsForNewFileBeforeUmask os.FileMode = 0600
// when overwriting file, permissions are not changed
writeError := ioutil.WriteFile(pathToCommandStoreFile, marshalledJSONData, permissionsForNewFileBeforeUmask)
if writeError != nil {
return writeError
}
return nil
}