github.com/quantumghost/awgo@v0.15.0/workflow.go (about) 1 // 2 // Copyright (c) 2016 Dean Jackson <deanishe@deanishe.net> 3 // 4 // MIT Licence. See http://opensource.org/licenses/MIT 5 // 6 7 package aw 8 9 import ( 10 "fmt" 11 "io" 12 "log" 13 "os" 14 "path/filepath" 15 "runtime/debug" 16 "sync" 17 "time" 18 19 "github.com/deanishe/awgo/fuzzy" 20 "github.com/deanishe/awgo/util" 21 ) 22 23 // AwGoVersion is the semantic version number of this library. 24 const AwGoVersion = "0.14.0" 25 26 // Default Workflow settings. Can be changed with the corresponding Options. 27 // 28 // See the Options and Workflow documentation for more information. 29 const ( 30 DefaultLogPrefix = "\U0001F37A" // Beer mug 31 DefaultMaxLogSize = 1048576 // 1 MiB 32 DefaultMaxResults = 0 // No limit, i.e. send all results to Alfred 33 DefaultSessionName = "AW_SESSION_ID" // Workflow variable session ID is stored in 34 DefaultMagicPrefix = "workflow:" // Prefix to call "magic" actions 35 ) 36 37 var ( 38 startTime time.Time // Time execution started 39 40 // The workflow object operated on by top-level functions. 41 // wf *Workflow 42 43 // Flag, as we only want to set up logging once 44 // TODO: Better, more pluggable logging 45 logInitialized bool 46 ) 47 48 // init creates the default Workflow. 49 func init() { 50 startTime = time.Now() 51 } 52 53 // Workflow provides a consolidated API for building Script Filters. 54 // 55 // As a rule, you should create a Workflow in init or main and call your main 56 // entry-point via Workflow.Run(), which catches panics, and logs & shows the 57 // error in Alfred. 58 // 59 // Script Filter 60 // 61 // To generate feedback for a Script Filter, use Workflow.NewItem() to create 62 // new Items and Workflow.SendFeedback() to send the results to Alfred. 63 // 64 // Run Script 65 // 66 // Use the TextErrors option, so any rescued panics are printed as text, 67 // not as JSON. 68 // 69 // Use ArgVars to set workflow variables, not Workflow/Feedback. 70 // 71 // See the _examples/ subdirectory for some full examples of workflows. 72 type Workflow struct { 73 sync.WaitGroup 74 // Interface to workflow's settings. 75 // Reads workflow variables by type and saves new values to info.plist. 76 Config *Config 77 78 // Call Alfred's AppleScript functions. 79 Alfred *Alfred 80 81 // Cache is a Cache pointing to the workflow's cache directory. 82 Cache *Cache 83 // Data is a Cache pointing to the workflow's data directory. 84 Data *Cache 85 // Session is a cache that stores session-scoped data. These data 86 // persist until the user closes Alfred or runs a different workflow. 87 Session *Session 88 89 // The response that will be sent to Alfred. Workflow provides 90 // convenience wrapper methods, so you don't normally have to 91 // interact with this directly. 92 Feedback *Feedback 93 94 // Updater fetches updates for the workflow. 95 Updater Updater 96 97 // MagicActions contains the magic actions registered for this workflow. 98 // It is set to DefaultMagicActions by default. 99 MagicActions *MagicActions 100 101 logPrefix string // Written to debugger to force a newline 102 maxLogSize int // Maximum size of log file in bytes 103 magicPrefix string // Overrides DefaultMagicPrefix for magic actions. 104 maxResults int // max. results to send to Alfred. 0 means send all. 105 sortOptions []fuzzy.Option // Options for fuzzy filtering 106 textErrors bool // Show errors as plaintext, not Alfred JSON 107 helpURL string // URL to help page (shown if there's an error) 108 dir string // Directory workflow is in 109 cacheDir string // Workflow's cache directory 110 dataDir string // Workflow's data directory 111 sessionName string // Name of the variable sessionID is stored in 112 sessionID string // Random session ID 113 } 114 115 // New creates and initialises a new Workflow, passing any Options to 116 // Workflow.Configure(). 117 // 118 // For available options, see the documentation for the Option type and the 119 // following functions. 120 // 121 // IMPORTANT: In order to be able to initialise the Workflow correctly, 122 // New must be run within a valid Alfred environment; specifically 123 // *at least* the following environment variables must be set: 124 // 125 // alfred_workflow_bundleid 126 // alfred_workflow_cache 127 // alfred_workflow_data 128 // 129 // If you aren't running from Alfred, or would like to specify a 130 // custom environment, use NewFromEnv(). 131 func New(opts ...Option) *Workflow { return NewFromEnv(nil, opts...) } 132 133 // NewFromEnv creates a new Workflows from a custom Env, instead of 134 // reading its configuration from Alfred's environment variables. 135 func NewFromEnv(env Env, opts ...Option) *Workflow { 136 137 if env == nil { 138 env = sysEnv{} 139 } 140 141 if err := validateEnv(env); err != nil { 142 panic(err) 143 } 144 145 wf := &Workflow{ 146 Config: NewConfig(env), 147 Alfred: NewAlfred(env), 148 Feedback: &Feedback{}, 149 logPrefix: DefaultLogPrefix, 150 maxLogSize: DefaultMaxLogSize, 151 maxResults: DefaultMaxResults, 152 sessionName: DefaultSessionName, 153 sortOptions: []fuzzy.Option{}, 154 } 155 156 wf.MagicActions = defaultMagicActions(wf) 157 158 wf.Configure(opts...) 159 160 wf.Cache = NewCache(wf.CacheDir()) 161 wf.Data = NewCache(wf.DataDir()) 162 wf.Session = NewSession(wf.CacheDir(), wf.SessionID()) 163 wf.initializeLogging() 164 return wf 165 } 166 167 // -------------------------------------------------------------------- 168 // Initialisation methods 169 170 // Configure applies one or more Options to Workflow. The returned Option reverts 171 // all Options passed to Configure. 172 func (wf *Workflow) Configure(opts ...Option) (previous Option) { 173 prev := make(options, len(opts)) 174 for i, opt := range opts { 175 prev[i] = opt(wf) 176 } 177 return prev.apply 178 } 179 180 // initializeLogging ensures future log messages are written to 181 // workflow's log file. 182 func (wf *Workflow) initializeLogging() { 183 184 if logInitialized { // All Workflows use the same global logger 185 return 186 } 187 188 // Rotate log file if larger than MaxLogSize 189 fi, err := os.Stat(wf.LogFile()) 190 if err == nil { 191 192 if fi.Size() >= int64(wf.maxLogSize) { 193 194 new := wf.LogFile() + ".1" 195 if err := os.Rename(wf.LogFile(), new); err != nil { 196 fmt.Fprintf(os.Stderr, "Error rotating log: %v\n", err) 197 } 198 199 fmt.Fprintln(os.Stderr, "Rotated log") 200 } 201 } 202 203 // Open log file 204 file, err := os.OpenFile(wf.LogFile(), 205 os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) 206 207 if err != nil { 208 wf.Fatal(fmt.Sprintf("Couldn't open log file %s : %v", 209 wf.LogFile(), err)) 210 } 211 212 // Attach logger to file 213 multi := io.MultiWriter(file, os.Stderr) 214 log.SetOutput(multi) 215 216 // Show filenames and line numbers if Alfred's debugger is open 217 if wf.Debug() { 218 log.SetFlags(log.Ltime | log.Lshortfile) 219 } else { 220 log.SetFlags(log.Ltime) 221 } 222 223 logInitialized = true 224 } 225 226 // -------------------------------------------------------------------- 227 // API methods 228 229 // BundleID returns the workflow's bundle ID. This library will not 230 // work without a bundle ID, which is set in the workflow's main 231 // setup sheet in Alfred Preferences. 232 func (wf *Workflow) BundleID() string { 233 234 s := wf.Config.Get(EnvVarBundleID) 235 if s == "" { 236 wf.Fatal("No bundle ID set. You *must* set a bundle ID to use AwGo.") 237 } 238 return s 239 } 240 241 // Name returns the workflow's name as specified in the workflow's main 242 // setup sheet in Alfred Preferences. 243 func (wf *Workflow) Name() string { return wf.Config.Get(EnvVarName) } 244 245 // Version returns the workflow's version set in the workflow's configuration 246 // sheet in Alfred Preferences. 247 func (wf *Workflow) Version() string { return wf.Config.Get(EnvVarVersion) } 248 249 // SessionID returns the session ID for this run of the workflow. 250 // This is used internally for session-scoped caching. 251 // 252 // The session ID is persisted as a workflow variable. It and the session 253 // persist as long as the user is using the workflow in Alfred. That 254 // means that the session expires as soon as Alfred closes or the user 255 // runs a different workflow. 256 func (wf *Workflow) SessionID() string { 257 258 if wf.sessionID == "" { 259 260 ev := os.Getenv(wf.sessionName) 261 262 if ev != "" { 263 wf.sessionID = ev 264 } else { 265 wf.sessionID = NewSessionID() 266 } 267 } 268 269 return wf.sessionID 270 } 271 272 // Debug returns true if Alfred's debugger is open. 273 func (wf *Workflow) Debug() bool { return wf.Config.GetBool(EnvVarDebug) } 274 275 // Args returns command-line arguments passed to the program. 276 // It intercepts "magic args" and runs the corresponding actions, terminating 277 // the workflow. See MagicAction for full documentation. 278 func (wf *Workflow) Args() []string { 279 prefix := DefaultMagicPrefix 280 if wf.magicPrefix != "" { 281 prefix = wf.magicPrefix 282 } 283 return wf.MagicActions.Args(os.Args[1:], prefix) 284 } 285 286 // Run runs your workflow function, catching any errors. 287 // If the workflow panics, Run rescues and displays an error message in Alfred. 288 func (wf *Workflow) Run(fn func()) { 289 290 vstr := wf.Name() 291 292 if wf.Version() != "" { 293 vstr += "/" + wf.Version() 294 } 295 296 vstr = fmt.Sprintf(" %s (AwGo/%v) ", vstr, AwGoVersion) 297 298 // Print right after Alfred's introductory blurb in the debugger. 299 // Alfred strips whitespace. 300 if wf.logPrefix != "" { 301 fmt.Fprintln(os.Stderr, wf.logPrefix) 302 } 303 304 log.Println(util.Pad(vstr, "-", 50)) 305 306 // Clear expired session data 307 wf.Add(1) 308 go func() { 309 defer wf.Done() 310 wf.Session.Clear(false) 311 }() 312 313 // Catch any `panic` and display an error in Alfred. 314 // Fatal(msg) will terminate the process (via log.Fatal). 315 defer func() { 316 317 if r := recover(); r != nil { 318 319 log.Println(util.Pad(" FATAL ERROR ", "-", 50)) 320 log.Printf("%s : %s", r, debug.Stack()) 321 log.Println(util.Pad(" END STACK TRACE ", "-", 50)) 322 323 // log.Printf("Recovered : %x", r) 324 err, ok := r.(error) 325 if ok { 326 wf.outputErrorMsg(err.Error()) 327 } 328 329 wf.outputErrorMsg(fmt.Sprintf("%v", r)) 330 } 331 }() 332 333 // Call the workflow's main function. 334 fn() 335 336 wf.Wait() 337 finishLog(false) 338 } 339 340 // -------------------------------------------------------------------- 341 // Helper methods 342 343 // outputErrorMsg prints and logs error, then exits process. 344 func (wf *Workflow) outputErrorMsg(msg string) { 345 if wf.textErrors { 346 fmt.Print(msg) 347 } else { 348 wf.Feedback.Clear() 349 wf.NewItem(msg).Icon(IconError) 350 wf.SendFeedback() 351 } 352 log.Printf("[ERROR] %s", msg) 353 // Show help URL or website URL 354 if wf.helpURL != "" { 355 log.Printf("Get help at %s", wf.helpURL) 356 } 357 finishLog(true) 358 } 359 360 // awDataDir is the directory for AwGo's own data. 361 func (wf *Workflow) awDataDir() string { 362 return util.MustExist(filepath.Join(wf.DataDir(), "_aw")) 363 } 364 365 // awCacheDir is the directory for AwGo's own cache. 366 func (wf *Workflow) awCacheDir() string { 367 return util.MustExist(filepath.Join(wf.CacheDir(), "_aw")) 368 } 369 370 // -------------------------------------------------------------------- 371 // Package-level only 372 373 // finishLog outputs the workflow duration 374 func finishLog(fatal bool) { 375 376 elapsed := time.Now().Sub(startTime) 377 s := util.Pad(fmt.Sprintf(" %v ", elapsed), "-", 50) 378 379 if fatal { 380 log.Fatalln(s) 381 } else { 382 log.Println(s) 383 } 384 }