// fsManager handles fsWalker processes, and communicates back and forth with the manager goroutine func fsManager(mediaFolder string, fsKillChan chan struct{}) { log.Println("fs: starting...") // Initialize a queue to cancel filesystem tasks cancelQueue := make(chan chan struct{}, 10) // Set up the data source (typically filesystem, unless in test mode) fsSource = fsFileSource{} if env.IsTest() { // Mock file source fsSource = memFileSource{} } // Track the number of filesystem events fired fsTaskCount := 0 // Initialize filesystem watcher when ready watcherChan := make(chan struct{}) // Queue an initial, verbose orphan scan o := new(fsOrphanScan) o.SetFolders(mediaFolder, "") o.Verbose(true) fsQueue <- o // Queue a media scan m := new(fsMediaScan) m.SetFolders(mediaFolder, "") m.Verbose(true) fsQueue <- m // Invoke task queue via goroutine, so it can be halted via the manager go func() { for { select { // Trigger a fsTask from queue case task := <-fsQueue: // Create a channel to halt the scan cancelChan := make(chan struct{}) cancelQueue <- cancelChan // Retrieve the folders to use with scan baseFolder, subFolder := task.Folders() // Start the scan changes, err := task.Scan(baseFolder, subFolder, cancelChan) if err != nil { log.Println(err) } // If changes occurred, update the scan time if changes > 0 { common.UpdateScanTime() } // On completion, close the cancel channel cancelChan = <-cancelQueue close(cancelChan) fsTaskCount++ // After both initial scans complete, start the filesystem watcher if fsTaskCount == 2 { close(watcherChan) } } } }() // Create a filesystem watcher, which is triggered after initial scans go func() { // Block until triggered <-watcherChan // Initialize the watcher watcher, err := fsmonitor.NewWatcher() if err != nil { log.Println(err) return } // Wait for events on goroutine go func() { // Recently modified/renamed files sets, used as rate-limiters to prevent modify // events from flooding the select statement. The filesystem watcher may fire an // excessive number of events, so these will block the extras for a couple seconds. recentModifySet := set.New() recentRenameSet := set.New() for { select { // Event occurred case ev := <-watcher.Event: switch { // On modify, trigger a media scan case ev.IsModify(): // Add file to set, stopping it from propogating if the event was recently triggered if !recentModifySet.Add(ev.Name) { break } // Remove file from rate-limiting set after a couple seconds go func() { <-time.After(2 * time.Second) recentModifySet.Remove(ev.Name) }() fallthrough // On create, trigger a media scan case ev.IsCreate(): // Invoke a slight delay to enable file creation <-time.After(250 * time.Millisecond) // Scan item as the "base folder", so it just adds this item m := new(fsMediaScan) m.SetFolders(ev.Name, "") m.Verbose(false) fsQueue <- m // On rename, trigger an orphan scan case ev.IsRename(): // Add file to set, stopping it from propogating if the event was recently triggered if !recentRenameSet.Add(ev.Name) { break } // Remove file from rate-limiting set after a couple seconds go func() { <-time.After(2 * time.Second) recentRenameSet.Remove(ev.Name) }() fallthrough // On delete, trigger an orphan scan case ev.IsDelete(): // Invoke a slight delay to enable file deletion <-time.After(250 * time.Millisecond) // Scan item as the "subfolder", so it just removes this item o := new(fsOrphanScan) o.SetFolders("", ev.Name) o.Verbose(false) fsQueue <- o } // Watcher errors case err := <-watcher.Error: log.Println(err) return } } }() // Watch media folder if err := watcher.Watch(mediaFolder); err != nil { log.Println(err) return } log.Println("fs: watching folder:", mediaFolder) }() // Trigger manager events via channel for { select { // Stop filesystem manager case <-fsKillChan: // Halt any in-progress tasks log.Println("fs: halting tasks") for i := 0; i < len(cancelQueue); i++ { // Receive a channel f := <-cancelQueue if f == nil { continue } // Send termination f <- struct{}{} log.Println("fs: task halted") } // Inform manager that shutdown is complete log.Println("fs: stopped!") fsKillChan <- struct{}{} return } } }
// apiRouter sets up the instance of negroni func apiRouter(apiKillChan chan struct{}) { log.Println("api: starting...") // Initialize negroni n := negroni.New() // Set up render r := render.New(render.Options{ // Output human-readable JSON/XML. GZIP will essentially negate the size increase, and this // makes the API much more developer-friendly IndentJSON: true, IndentXML: true, }) // GZIP all responses n.Use(gzip.Gzip(gzip.DefaultCompression)) // Initial API setup n.Use(negroni.HandlerFunc(func(res http.ResponseWriter, req *http.Request, next http.HandlerFunc) { // Send a Server header with all responses res.Header().Set("Server", fmt.Sprintf("%s/%s (%s_%s)", App, Version, runtime.GOOS, runtime.GOARCH)) // Store render in context for all API calls context.Set(req, api.CtxRender, r) // Wrap HTTP request and response with metrics instrumentation req.Body = httpRMetricsLogger{req.Body} metricsRes := httpWMetricsLogger{res} // On debug, log everything if env.IsDebug() { log.Println(req.Header) log.Println(req.URL) // Wrap response in debug logging next(httpWDebugLogger{metricsRes}, req) return } // Delegate to next middleware next(metricsRes, req) return })) // Authenticate all API calls n.Use(negroni.HandlerFunc(func(res http.ResponseWriter, req *http.Request, next http.HandlerFunc) { // Use factory to determine and invoke the proper authentication method for this path user, session, clientErr, serverErr := auth.Factory(req.URL.Path).Authenticate(req) // Check for client error if clientErr != nil { // Check for a Subsonic error, since these are rendered as XML if subErr, ok := clientErr.(*subsonic.Container); ok { r.XML(res, 200, subErr) return } // If debug mode, and no username or password, send a WWW-Authenticate header to prompt request // This allows for manual exploration of the API if needed if env.IsDebug() && (clientErr == auth.ErrNoUsername || clientErr == auth.ErrNoPassword) { res.Header().Set("WWW-Authenticate", "Basic") } r.JSON(res, 401, api.ErrorResponse{ Error: &api.Error{ Code: 401, Message: "authentication failed: " + clientErr.Error(), }, }) return } // Check for server error if serverErr != nil { log.Println(serverErr) // Check for a Subsonic error, since these are rendered as XML if subErr, ok := serverErr.(*subsonic.Container); ok { r.XML(res, 200, subErr) return } r.JSON(res, 500, api.ErrorResponse{ Error: &api.Error{ Code: 500, Message: "server error", }, }) return } // Successful login, map session user and session to gorilla context for this request context.Set(req, api.CtxUser, user) context.Set(req, api.CtxSession, session) // Print information about this API call log.Printf("api: [%s] %s %s?%s", req.RemoteAddr, req.Method, req.URL.Path, req.URL.Query().Encode()) // Perform API call next(res, req) })) // Wait for graceful to signal termination gracefulChan := make(chan struct{}, 0) // Use gorilla mux with negroni, start server n.UseHandler(newRouter()) go func() { // Load config conf, err := config.C.Load() if err != nil { log.Println(err) return } // Check for empty host if conf.Host == "" { log.Fatalf("api: no host specified in configuration") } // Start server, allowing up to 10 seconds after shutdown for clients to complete log.Println("api: binding to host", conf.Host) if err := graceful.ListenAndServe(&http.Server{Addr: conf.Host, Handler: n}, 10*time.Second); err != nil { // Check if address in use if strings.Contains(err.Error(), "address already in use") { log.Fatalf("api: cannot bind to %s, is wavepipe already running?", conf.Host) } // Ignore error on closing if !strings.Contains(err.Error(), "use of closed network connection") { // Log other errors log.Println(err) } } // Shutdown complete close(gracefulChan) }() // Trigger events via channel for { select { // Stop API case <-apiKillChan: // If testing, don't wait for graceful shutdown if !env.IsTest() { // Block and wait for graceful shutdown log.Println("api: waiting for remaining connections to close...") <-gracefulChan } // Inform manager that shutdown is complete log.Println("api: stopped!") apiKillChan <- struct{}{} return } } }
// Manager is responsible for coordinating the application func Manager(killChan chan struct{}, exitChan chan int) { // Check if a commit hash was injected if Revision == "" { log.Println("manager: empty git revision, please rebuild using 'make'") } else { log.Printf("manager: initializing %s %s [revision: %s]...", App, Version, Revision) } // Gather information about the operating system stat := common.OSInfo() log.Printf("manager: %s - %s_%s (%d CPU) [pid: %d]", stat.Hostname, stat.Platform, stat.Architecture, stat.NumCPU, stat.PID) // Set configuration source, load configuration config.C = new(config.CLIConfig) conf, err := config.C.Load() if err != nil { log.Fatalf("manager: could not load config: %s", err.Error()) } // Check valid media folder, unless in test mode folder := conf.Media() if !env.IsTest() { // Check empty folder, provide help information if not set if folder == "" { log.Fatal("manager: no media folder set in config: ", config.C.Help()) } else if _, err := os.Stat(folder); err != nil { // Check file existence log.Fatalf("manager: invalid media folder set in config: %s", err.Error()) } } // Launch database manager to handle database/ORM connections dbLaunchChan := make(chan struct{}) dbKillChan := make(chan struct{}) go dbManager(*conf, dbLaunchChan, dbKillChan) // Wait for database to be fully ready before other operations start <-dbLaunchChan // Launch cron manager to handle timed events cronKillChan := make(chan struct{}) go cronManager(cronKillChan) // Launch filesystem manager to handle file scanning fsKillChan := make(chan struct{}) go fsManager(folder, fsKillChan) // Launch HTTP API server apiKillChan := make(chan struct{}) go apiRouter(apiKillChan) // Launch transcode manager to handle ffmpeg and file transcoding transcodeKillChan := make(chan struct{}) go transcodeManager(transcodeKillChan) // Wait for termination signal for { select { // Trigger a graceful shutdown case <-killChan: log.Println("manager: triggering graceful shutdown, press Ctrl+C again to force halt") // Stop transcodes, wait for confirmation transcodeKillChan <- struct{}{} <-transcodeKillChan close(transcodeKillChan) // Stop API, wait for confirmation apiKillChan <- struct{}{} <-apiKillChan close(apiKillChan) // Stop filesystem, wait for confirmation fsKillChan <- struct{}{} <-fsKillChan close(fsKillChan) // Stop database, wait for confirmation dbKillChan <- struct{}{} <-dbKillChan close(dbKillChan) // Stop cron, wait for confirmation cronKillChan <- struct{}{} <-cronKillChan close(cronKillChan) // Exit gracefully log.Println("manager: stopped!") exitChan <- 0 } } }