Example #1
0
// 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
		}
	}
}
Example #2
0
// 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
		}
	}
}
Example #3
0
// 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
		}
	}
}