-
Notifications
You must be signed in to change notification settings - Fork 2
/
plugin.go
482 lines (405 loc) · 12.6 KB
/
plugin.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
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
package plugin
import (
"fmt"
"github.com/gocms-io/gcm/config"
"github.com/gocms-io/gcm/models"
"github.com/gocms-io/gcm/utility"
"github.com/gocms-io/gocms/utility/errors"
"github.com/urfave/cli"
"net/url"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"runtime"
"strings"
"syscall"
"time"
)
const flag_hard = "delete"
const flag_hard_short = "d"
const flag_watch = "watch"
const flag_watch_short = "w"
const flag_entry = "entry"
const flag_entry_short = "e"
const flag_dir_file_to_copy = "copy"
const flag_dir_file_to_copy_short = "c"
const flag_run_gocms = "run"
const flag_run_gocms_short = "r"
const flag_gocms_dev_mode = "gocms"
const flag_gocms_dev_mode_short = "g"
const flag_ignore_files = "ignore"
const flag_ignore_files_short = "i"
type pluginContext struct {
hardCopy bool
watch bool
buildEntry string
filesToCopy []string
run bool
devMode bool
pluginPath string
srcDir string
destDir string
verbose bool
manifest *models.PluginManifest
goCMSDoneChan chan bool
watcherDoneChan chan bool
systemDoneChan chan bool
ignorePath []string
goBuildExec *exec.Cmd
watcherFileContext *utility.WatchFileContext
skipRunDueToFailedComplile bool
}
var CMD_PLUGIN = cli.Command{
Name: "plugin",
Usage: "copy plugin files from development directory into the gocms plugin directory",
ArgsUsage: "<source> <gocms installation>",
Action: cmd_copy_plugin,
Flags: []cli.Flag{
cli.BoolFlag{
Name: flag_hard + ", " + flag_hard_short,
Usage: "Delete the existing destination and replace with the contents of the source.",
},
cli.BoolFlag{
Name: flag_watch + ", " + flag_watch_short,
Usage: "Watch for file changes in source and copy to destination on change.",
},
cli.StringFlag{
Name: flag_entry + ", " + flag_entry_short,
Usage: "Build the plugin using the following entry point. Defaults to 'main.go'.",
},
cli.StringSliceFlag{
Name: flag_dir_file_to_copy + ", " + flag_dir_file_to_copy_short,
Usage: "Directory or file to copy with plugin. Accepts multiple instances of the flag.",
},
cli.BoolFlag{
Name: flag_run_gocms + ", " + flag_run_gocms_short,
Usage: "Run gocms after plugin is compiled and copied.",
},
cli.BoolFlag{
Name: flag_gocms_dev_mode + ", " + flag_gocms_dev_mode_short,
Usage: "This option is intended for use only during gocms development. It is used to compile and run gocms from the specified destination directory.",
},
cli.StringSliceFlag{
Name: flag_ignore_files + ", " + flag_ignore_files_short,
Usage: "Files to ignore while watching. Multiple ignore flags can be given to ignore multiple files. Ignore files are regex capable. ex: .git*",
},
},
}
func cmd_copy_plugin(c *cli.Context) error {
// get command context from cli
pctx, err := buildContextFromFlags(c)
if err != nil {
return err
}
// build binary
err = pctx.getBinaryBuildCommand()
if err != nil {
return err
}
fmt.Printf("Starting Build and Copy - %v\n", time.Now().Format("03:04:05"))
// to try run go generate or fail nice and continue
err = pctx.goGenerate()
if err != nil {
fmt.Println("Error running go generate. Continue anyway.")
}
// build binary
err = pctx.runBinaryBuildCommand()
if err != nil {
return err
}
// copy files
pctx.copyPluginFiles()
fmt.Printf("Build and Copy Complete - %v\n", time.Now().Format("03:04:05"))
if pctx.run || pctx.watch {
pctx.systemDoneChan = make(chan bool)
if pctx.run {
// setup gracful close to prevent port leaks
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
fmt.Printf("Quiting...\n")
close(pctx.goCMSDoneChan)
time.Sleep(time.Second * 2)
close(pctx.systemDoneChan)
}()
pctx.goCMSDoneChan = make(chan bool)
pctx.runGoCMS()
}
// if watch create watcher context and run
if pctx.watch {
pctx.watcherDoneChan = make(chan bool)
pctx.startFileWatcher()
}
// listen
<-pctx.systemDoneChan
}
return nil
}
func (pctx *pluginContext) startFileWatcher() {
pctx.watcherFileContext = &utility.WatchFileContext{
Verbose: pctx.verbose,
SourceBase: pctx.srcDir,
IgnorePaths: pctx.ignorePath,
DoneChan: pctx.watcherDoneChan,
ChangeTimeoutMap: make(map[string]time.Time),
Chmod: utility.IgnoreDestination,
Removed: pctx.onFileChangeHandler,
Create: pctx.onFileChangeHandler,
Rename: utility.IgnoreDestination,
Write: pctx.onFileChangeHandler,
}
if pctx.devMode {
fmt.Printf("Dev mode enabled. Waiting 5 seconds before watching files for change.\n")
time.Sleep(time.Second * 5)
}
go pctx.watcherFileContext.Watch()
}
func (pctx *pluginContext) onFileChangeHandler(c *utility.WatchFileContext, eventPath string) {
// ignore changes to "."
if eventPath == "." || eventPath == "/" || eventPath == "./" || eventPath == "" {
return
}
// ignore paths as specified
for _, ignorePath := range c.IgnorePaths {
ignorePathRegex, _ := regexp.Compile(ignorePath)
if ignorePathRegex.MatchString(filepath.Clean(eventPath)) {
return
}
}
// check if file change is to soon and skip if needed
currentTime := time.Now()
fileChangedLastTime := c.ChangeTimeoutMap[eventPath]
if currentTime.Sub(fileChangedLastTime) < (time.Second * 5) {
return
}
// set file change time
c.ChangeTimeoutMap[eventPath] = currentTime
fmt.Printf("Changes Dectected in '%v'\n", eventPath)
if pctx.run {
if !pctx.skipRunDueToFailedComplile {
fmt.Printf("Stopping GoCMS\n")
close(pctx.goCMSDoneChan)
}
}
fmt.Printf("Start Rebuild & Copy - %v\n", time.Now().Format("03:04:05"))
// get binary build command
err := pctx.getBinaryBuildCommand()
if err != nil {
fmt.Printf("Error getting new binary build command: %v\n", err.Error())
}
// run go generate
err = pctx.goGenerate()
if err != nil {
fmt.Printf("Error running go generate: %v\n", err.Error())
}
// run binary build command
err = pctx.runBinaryBuildCommand()
if err != nil {
fmt.Printf("Error running new binary build command: %v\n", err.Error())
pctx.skipRunDueToFailedComplile = true
return
}
err = pctx.copyPluginFiles()
if err != nil {
fmt.Printf("Error copying files: %v\n", err.Error())
}
// if we are suppose to run the new binary within gocms
pctx.skipRunDueToFailedComplile = false
if pctx.run {
newGoCMSDonChan := make(chan bool)
pctx.goCMSDoneChan = newGoCMSDonChan
pctx.runGoCMS()
} else {
fmt.Printf("Rebuild & Copy Complete - %v\n", time.Now().Format("03:04:05"))
}
}
func (pctx *pluginContext) copyPluginFiles() error {
// copy files to plugin
for _, file := range pctx.filesToCopy {
// if file doesn't exist
if _, err := os.Stat(file); os.IsNotExist(err) {
fmt.Printf("Can't copy file/directory:'%v'... Doesn't exist!\n", file)
return err
}
destFile := file
// strip leading path if it isn't just a file
if filepath.Base(file) != file && pctx.srcDir != "." {
destFile = strings.Replace(file, pctx.srcDir, "", 1)
if pctx.verbose {
fmt.Printf("compaired %v and replaced %v, with %v\n", pctx.srcDir, file, destFile)
}
}
destFilePath := filepath.Join(pctx.pluginPath, destFile)
err := utility.Copy(file, destFilePath, true, pctx.verbose)
if err != nil {
fmt.Printf("Error copying %v: %v\n", file, err.Error())
return err
}
}
return nil
}
func (pctx *pluginContext) runBinaryBuildCommand() error {
fullBinPath := filepath.Join(pctx.pluginPath, pctx.manifest.Services.Bin)
if runtime.GOOS == "windows" {
fullBinPath = fmt.Sprintf("%v.exe", fullBinPath)
}
err := pctx.goBuildExec.Run()
if err != nil {
fmt.Printf("Error running 'go build -o %v %v': %v\n", fullBinPath, filepath.Join(pctx.srcDir, pctx.buildEntry), err.Error())
return err
}
// set permissions to run
err = os.Chmod(fullBinPath, os.FileMode(0755))
if err != nil {
fmt.Printf("Error setting plugin to executable: %v\n", err.Error())
return err
}
return nil
}
func (pctx *pluginContext) runGoCMS() {
fmt.Printf("Running GoCMS\n")
go utility.StartGoCMS(pctx.destDir, pctx.devMode, pctx.goCMSDoneChan)
}
func (pctx *pluginContext) getBinaryBuildCommand() error {
// build go binary exce
pctx.pluginPath = filepath.Join(pctx.destDir, config.CONTENT_DIR, config.PLUGINS_DIR, pctx.manifest.Id)
buildPath := filepath.Join(pctx.pluginPath, pctx.manifest.Services.Bin)
if runtime.GOOS == "windows" {
buildPath = fmt.Sprintf("%v.exe", buildPath)
}
pctx.goBuildExec = exec.Command("go", "build", "-o", buildPath, filepath.Join(pctx.srcDir, pctx.buildEntry))
//if pctx.verbose {
pctx.goBuildExec.Stdout = os.Stdout
//}
pctx.goBuildExec.Stderr = os.Stderr
return nil
}
func (pctx *pluginContext) goGenerate() error {
// run go generate
goGenerate := exec.Command("go", "generate", filepath.Join(pctx.srcDir, pctx.buildEntry))
if pctx.verbose {
goGenerate.Stdout = os.Stdout
}
goGenerate.Stderr = os.Stderr
err := goGenerate.Run()
if err != nil {
errStr := fmt.Sprintf("Error running 'go generate %v': %v\n", filepath.Join(pctx.srcDir, pctx.buildEntry), err.Error())
fmt.Println(errStr)
return errors.New(errStr)
}
return nil
}
func buildContextFromFlags(c *cli.Context) (*pluginContext, error) {
// get src dir and dest dri
srcDir := c.Args().Get(0)
destDir := c.Args().Get(1)
// verify src and dest exist
if srcDir == "" || destDir == "" {
errStr := "A source and destination directory must be specified."
fmt.Println(errStr)
return nil, errors.New(errStr)
}
// clean dirs
srcDir = filepath.Clean(srcDir)
destDir = filepath.Clean(destDir)
// parse manifest file
manifestPath := filepath.Join(srcDir, "manifest.json")
manifest, err := utility.ParseManifest(manifestPath)
if err != nil {
fmt.Printf("Error parsing manifest file %v: %v\n", manifestPath, err.Error())
return nil, err
}
pctx := pluginContext{
buildEntry: "main.go",
manifest: manifest,
devMode: false,
run: false,
watch: false,
srcDir: srcDir,
destDir: destDir,
}
// entry
if c.String(flag_entry) != "" {
pctx.buildEntry = c.String(flag_entry)
}
// dev mode
if c.Bool(flag_gocms_dev_mode) {
pctx.devMode = true
}
// run
if c.Bool(flag_run_gocms) {
pctx.run = true
}
// watch
if c.Bool(flag_watch) {
pctx.watch = true
}
// verbose
if c.GlobalBool(config.FLAG_VERBOSE) {
pctx.verbose = true
}
// add default files
pctx.filesToCopy = append(pctx.filesToCopy, filepath.Join(pctx.srcDir, config.PLUGIN_MANIFEST))
// add docs if they exist
if pctx.manifest.Services.Docs != "" {
pctx.filesToCopy = append(pctx.filesToCopy, filepath.Join(pctx.srcDir, pctx.manifest.Services.Docs))
}
// add additional files
if c.StringSlice(flag_dir_file_to_copy) != nil {
pctx.filesToCopy = append(pctx.filesToCopy, c.StringSlice(flag_dir_file_to_copy)...)
}
// add interface files as needed
pctx.load_interface_for_plugin()
// add default ignore files
pctx.ignorePath = append(pctx.ignorePath, []string{"vendor", ".git", "docs", ".idea", "___*", "node_modules"}...)
// add files to ignore
if c.StringSlice(flag_ignore_files) != nil {
pctx.ignorePath = append(pctx.ignorePath, c.StringSlice(flag_ignore_files)...)
}
return &pctx, nil
}
func (pctx *pluginContext) load_interface_for_plugin() {
// public
if pctx.manifest.Interface.Public != "" {
pctx.loadFileOrUrl(pctx.manifest.Interface.Public)
}
// public vendor
if pctx.manifest.Interface.PublicVendor != "" {
pctx.loadFileOrUrl(pctx.manifest.Interface.PublicVendor)
}
// public style
if pctx.manifest.Interface.PublicStyle != "" {
pctx.loadFileOrUrl(pctx.manifest.Interface.PublicStyle)
}
// admin
if pctx.manifest.Interface.Admin != "" {
pctx.loadFileOrUrl(pctx.manifest.Interface.Admin)
}
// admin vendor
if pctx.manifest.Interface.AdminVendor != "" {
pctx.loadFileOrUrl(pctx.manifest.Interface.AdminVendor)
}
// admin style
if pctx.manifest.Interface.AdminStyle != "" {
pctx.loadFileOrUrl(pctx.manifest.Interface.AdminStyle)
}
// admin goes here
}
func (pctx *pluginContext) loadFileOrUrl(path string) {
// if file rather than request
_, err := url.ParseRequestURI(path)
if err != nil {
if pctx.verbose {
fmt.Printf("public vendor interface is a file: %v. Add it for copy.\n", path)
}
// add file
pctx.filesToCopy = append(pctx.filesToCopy, filepath.Join(pctx.srcDir, config.CONTENT_DIR, path))
} else { // skip url
if pctx.verbose {
fmt.Printf("public vendor interface is a url: %v. Don't copy as a file.\n", path)
}
}
}