#!/usr/bin/perl

# SLIMP3 Server Copyright (C) 2001 Sean Adams, Slim Devices Inc.
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License,
# version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#

require 5.006_000;
use strict;  # _NOPERL2EXE_
#use utf8;

# This package section is used for the windows service version of the application, 
# as built with ActiveState's PerlSvc

package PerlSvc;

our $Name = 'slimp3svc';
our $DisplayName = 'SLIMP3 Server';

sub Startup {

	# added to workaround a problem with 5.8 and perlsvc.
    $SIG{BREAK} = sub {} if RunningAsService();

	main::init();
	
	main::start();
	
   # here's where your startup code will go
    while (ContinueRun() && !main::idle()) {
    }
}

sub Install {
    # add your additional install messages or functions here
}

sub Remove {
    # add your additional remove messages or functions here
}

sub Help {
    # add your additional help messages or functions here
    $DisplayName . $Name;
}

package main;

use Config;
use Getopt::Long;
#use FindBin qw($Bin);
our $Bin="/usr/share/slimp3";
use File::Spec::Functions qw(:ALL);
use FileHandle;
use POSIX qw(:signal_h :errno_h :sys_wait_h);
use Socket qw(:DEFAULT :crlf);

#use lib (catdir($Bin,'lib'), $Bin, catdir($Bin,'lib','CPAN'));

use SliMP3::Animation;
use SliMP3::Buttons::Browse;
use SliMP3::Buttons::BrowseMenu;
use SliMP3::Buttons::Home;
use SliMP3::Buttons::Power;
use SliMP3::Buttons::ScreenSaver;
use SliMP3::Buttons::MoodWheel;
use SliMP3::Buttons::InstantMix;
use SliMP3::Client;
use SliMP3::Command;
use SliMP3::Control;
use SliMP3::CLI;
use SliMP3::Discovery;
use SliMP3::Display;
use SliMP3::HTTP;
use SliMP3::IR;
use SliMP3::Info;
use SliMP3::iTunes;
use SliMP3::MusicFolderScan;
use SliMP3::Misc;
use SliMP3::OSDetect;
use SliMP3::Playlist;
use SliMP3::Prefs;
use SliMP3::Protocol;
use SliMP3::Scheduler;
use SliMP3::Setup;
use SliMP3::Stdio;
use SliMP3::Strings qw(string);
use SliMP3::Timers;
use SliMP3::MoodLogic;


use vars qw($VERSION
		@AUTHORS);

@AUTHORS = (

	'Sean Adams',
	'Dean Blackketter',
	'Amos Hayes',
	'Mark Langston',
	'Eric Lyons',
	'Scott McIntyre',
	'Robert Moser',
	'Richard Smith',
	'Sam Saffron',
	'Daniel Sully',
);

$VERSION = '4.2.6';

# old preferences settings, only used by the .slimp3.conf configuration.
# real settings are stored in the new preferences file:  .slimp3.pref
use vars qw(
	    $mp3dir
	    $httpport
);

use vars qw(
		$d_cli
		$d_control
		$d_command
		$d_display
		$d_files
		$d_http
		$d_info
		$d_ir
		$d_itunes
		$d_moodlogic
		$d_mdns
		$d_os
		$d_perf
		$d_parse
		$d_paths
		$d_playlist
		$d_protocol
		$d_prefs
		$d_remotestream
		$d_scan
		$d_server
		$d_scheduler
		$d_stdio
		$d_stream
		$d_stream_v
		$d_sync
		$d_time
		$d_ui
		$d_usage

	    $user
	    $group
	    $cliaddr
	    $cliport
	    $daemon
	    $httpaddr
	    $lastlooptime
	    $logfile
	    $loopcount
	    $loopsecond
	    $localClientNetAddr
	    $localStreamAddr
		$newVersion
	    $pidfile
	    $prefsfile
	    $prefsfile2
	    $quiet
	    $nosetup
	    $stdio
	    $stop
	);

# these IO::Select objects contain references to every socket we might be listening
# or connected to, so we can wait in select for network events
use vars qw(
	    $selRead
		$selWrite
	);
	

sub init {
	srand();

	$selRead = IO::Select->new();
	$selWrite = IO::Select->new();

	autoflush STDERR;
	autoflush STDOUT;

	$::d_server && msg("SLIMP3 Server OSDetect init...\n");
	SliMP3::OSDetect::init();

	$::d_server && msg("SLIMP3 Server Strings init...\n");
	SliMP3::Strings::init();


	$::d_server && msg("SLIMP3 Server OS Specific init...\n");
	if (SliMP3::OSDetect::OS() ne 'mac') {
		$SIG{CHLD} = 'IGNORE';
		$SIG{PIPE} = 'IGNORE';
		if (SliMP3::OSDetect::OS() ne 'win') {
			$SIG{INT} = \&sigint;
			$SIG{HUP} = \&initSettings;
		}		
		$SIG{TERM} = \&sigint;
		$SIG{QUIT} = \&sigint;
	}
	
	# we have some special directories under OSX.
	if ($^O eq 'darwin') {
		mkdir $ENV{'HOME'} . "/Library/SlimDevices";
		mkdir $ENV{'HOME'} . "/Library/SlimDevices/Plugins";
		mkdir $ENV{'HOME'} . "/Library/SlimDevices/html";
		mkdir $ENV{'HOME'} . "/Library/SlimDevices/IR";
		mkdir $ENV{'HOME'} . "/Library/SlimDevices/bin";
		
		unshift @INC, $ENV{'HOME'} . "/Library/SlimDevices";
		unshift @INC, "/Library/SlimDevices/";
	}

	$::d_server && msg("SLIMP3 Server settings init...\n");
	initSettings();

	$::d_server && msg("SLIMP3 Server setting language...\n");
	SliMP3::Strings::setLanguage(SliMP3::Prefs::get("language"));

	$::d_server && msg("SLIMP3 Server IR init...\n");
	SliMP3::IR::init();
	
	$::d_server && msg("SLIMP3 Server Buttons init...\n");
	SliMP3::Buttons::Common::init();
}

sub start {

	$::d_server && msg("SLIMP3 Server starting up...\n");

	$::d_server && msg("SLIMP3 Server daemonizing...\n");
	# background if requested
	if (SliMP3::OSDetect::OS() ne 'win' && $daemon) {
		daemonize();
	} else {
		save_pid_file();
		
		if (defined $logfile) {
			if ($stdio) {
				if (!open STDERR, ">>$logfile") { die "Can't write to $logfile: $!";}
			} else {
				if (!open STDOUT, ">>$logfile") { die "Can't write to $logfile: $!";}
				if (!open STDERR, '>&STDOUT') { die "Can't dup stdout: $!"; }
			}
		}
	};
	
	$::d_server && msg("SLIMP3 Server Stdio init...\n");
	if ($stdio) {
		SliMP3::Stdio::init(\*STDIN, \*STDOUT);
	}

	$::d_server && msg("SLIMP3 Server Protocol init...\n");
	SliMP3::Protocol::init();
	$::d_server && msg("SLIMP3 Server Info init...\n");
	SliMP3::Info::init();
	$::d_server && msg("SLIMP3 Server HTTP init...\n");
	SliMP3::HTTP::init();
	$::d_server && msg("SLIMP3 Server CLI init...\n");
	SliMP3::CLI::init();
	$::d_server && msg("SLIMP3 Server History load...\n");
	SliMP3::History::load();

	
	$::d_server && msg("SLIMP3 Server persist playlists...\n");
	if (SliMP3::Prefs::get('persistPlaylists')) {
		SliMP3::Command::setExecuteCallback(\&SliMP3::Playlist::modifyPlaylistCallback);
	}
	
	$::d_server && msg("SLIMP3 Server iTunes init...\n");

# start scanning based on a timer...
# Currently, it's set to one item (directory or song) scanned per second.
	if (SliMP3::iTunes::useiTunesLibrary()) {
		SliMP3::iTunes::startScan();
	} elsif (SliMP3::MoodLogic::useMoodLogic()) {
		SliMP3::MoodLogic::startScan();
	} else {
		SliMP3::MusicFolderScan::startScan(1);
	}
	
	$lastlooptime = Time::HiRes::time();
	$loopcount = 0;
	$loopsecond = int($lastlooptime);
	
	checkVersion();
	
	$::d_server && msg("SLIMP3 Server done start...\n");
}

sub main {
	initOptions();

	init();
	start();
	
	while (!idle()) {}
	
	stopServer();
}

sub idle {

	my $select_time;

	my $now = Time::HiRes::time();
	my $to;

	if ($::d_perf) {
		if (int($now) == $loopsecond) {
			$loopcount++;
		} else {
			msg("Idle loop speed: $loopcount iterations per second\n");
			$loopcount = 0;
			$loopsecond = int($now);
		}
		$to = watchDog();
	}
	
	# check for time travel (i.e. If time skips backwards for DST or clock drift adjustments)
	if ($now < $lastlooptime) {
		SliMP3::Timers::adjustAllTimers($now - $lastlooptime);
		$::d_time && msg("finished adjustalltimers: " . Time::HiRes::time() . "\n");
	} 
	$lastlooptime = $now;

	# check the timers for any new tasks		
	SliMP3::Timers::checkTimers();	
	if ($::d_perf) { $to = watchDog($to, "checkTimers"); }

	# handle SliMP3 client protocol activity
	SliMP3::Protocol::idle();
	if ($::d_perf) { $to = watchDog($to, "Protocol::idle"); }
	
	# handle queued IR activity
	SliMP3::IR::idle();
	if ($::d_perf) { $to = watchDog($to, "IR::idle"); }
	
	# check the timers for any new tasks		
	SliMP3::Timers::checkTimers();	
	if ($::d_perf) { $to = watchDog($to, "checkTimers"); }

	my $tasks = SliMP3::Scheduler::run_tasks();
	if ($::d_perf) { $to = watchDog($to, "run_tasks"); } 
	
	# if background tasks are running, don't wait in select.
	if (!$tasks) {
		# undefined if there are no timers, 0 if overdue, otherwise delta to next timer
		$select_time = SliMP3::Timers::nextTimer();
		
		# loop through once a second, at a minimum
		if (!defined($select_time) || $select_time > 1) { $select_time = 1 };
		
		$::d_time && msg("select_time: ". (defined($select_time) ? $select_time : "UNDEF")."\n");
		
		# wait in select until we get something.  
		$::d_time && msg("select counts: " . $selRead->count . " " . $selWrite->count . "\n");

		my ($r, $w, $e) = IO::Select->select($selRead,$selWrite,undef,$select_time);
		if ($::d_perf) { $to = watchDog($to, "select"); }
		
		if ($stdio) {
			foreach my $fh (@$r) {
				if ($fh == $SliMP3::Stdio::stdin) {
				  SliMP3::Stdio::processRequest($fh);
				}
			}
		}
	}
	
	# handle HTTP and command line interface activity, including:
	#   opening sockets, 
	#   reopening sockets if the port has changed, 
	#   and handling HTTP traffic
	SliMP3::HTTP::idle();
	if ($::d_perf) { $to = watchDog($to, "http::idle"); }

	SliMP3::CLI::idle();
	if ($::d_perf) { $to = watchDog($to, "cli::idle"); }

	return $::stop;
}

sub showUsage {
	print <<EOF;
Usage: $0 [--mp3dir <dir>] [--daemon] [--stdio] [--logfile <logfilepath>]
          [--user <username>]
          [--group <groupname>]
          [--httpport <portnumber> [--httpaddr <listenip>]]
          [--cliport <portnumber> [--cliaddr <listenip>]]
          [--prefsfile <prefsfilepath> [--prefsfile2 <prefsfilepath>]]
          [--pidfile <pidfilepath>]
          [--d_various]

    --help           => Show this usage information.
    --mp3dir         => The path to a directory of your MP3 files.
    --logfile        => Specify a file for error logging.
    --daemon         => Run the server in the background.
                        This may only work on Unix-like systems.
    --stdio          => Use standard in and out as a command line interface 
                        to the server
    --user           => Specify the user that server should run as.
                        Only usable if server is started as root.
                        This may only work on Unix-like systems.
    --group          => Specify the group that server should run as.
                        Only usable if server is started as root.
                        This may only work on Unix-like systems.
    --httpport       => Activate the web interface on the specified port.
                        Set to 0 in order disable the web server.
    --httpaddr       => Activate the web interface on the specified IP address.
    --cliport        => Activate the command line interface TCP/IP interface
                        on the specified port. Set to 0 in order disable the 
                        command line interface server.
    --cliaddr        => Activate the command line interface TCP/IP 
                        interface on the specified IP address.
    --prefsfile      => Specify where the preferences file should be stored
    --prefsfile2     => If set, this file will override prefsfile values and
                        should not be writtable. For example, use
                        --prefsfile=/var/lib/slimp3/slimp3.conf
                        --prefsfile2=/etc/slimp3/slimp3.conf
    --pidfile        => Specify where a process ID file should be stored
    --quiet          => Minimize the amount of text output
    --playeraddr     => Specify the _server's_ IP address to use to connect 
                        to SLIMP3 players
    --streamaddr     => Specify the _server's_ IP address to use to connect
                        to streaming audio sources
    --nosetup        => Disable setup via http.

The following are debugging flags which will print various information 
to the console via stderr:

    --d_cli          => Display debugging information for the 
                        command line interface interface
    --d_command      => Display internal command execution
    --d_control      => Low level player control information
    --d_display      => Show what (should be) on the slimp3's display 
    --d_files        => Files, paths, opening and closing
    --d_http         => HTTP activity
    --d_info         => MP3/ID3 track information
    --d_ir           => Infrared activity
    --d_itunes       => iTunes synchronization information
    --d_moodlogic    => MoodLogic synchronization information
    --d_mdns         => Multicast DNS aka Zeroconf aka Rendezvous information
    --d_os           => Operating system detection information
    --d_paths        => File path processing information
    --d_perf         => Performance information
    --d_parse        => Playlist parsing information
    --d_playlist     => High level playlist and control information
    --d_protocol     => Client protocol information
    --d_prefs        => Preferences file information
    --d_remotestream => Information about remote HTTP streams and playlists
    --d_scan         => Information about scanning directories and filelists
    --d_server       => Basic server functionality
    --d_scheduler    => Internal scheduler information
    --d_stdio        => Standard I/O command debugging
    --d_stream       => Information about player streaming protocol 
    --d_stream_v     => Verbose information about player streaming protocol 
    --d_sync         => Information about multi player synchronization
    --d_time         => Internal timer information
    --d_ui           => Player user interface information
    --d_usage        => Display buffer usage codes on the SLIMP3 display
    
Commands may be sent to the server through standard in and will be echoed via
standard out.  See complete documentation for details on the command syntax.
EOF

}

sub initOptions {
	if (!GetOptions(
		'user=s'   			=> \$user,
		'group=s'   		=> \$group,
		'cliaddr=s'   		=> \$cliaddr,
		'cliport=s'   		=> \$cliport,
		'daemon'   			=> \$daemon,
		'httpaddr=s'   		=> \$httpaddr,
		'httpport=s'   		=> \$httpport,
		'logfile=s'   		=> \$logfile,
		'mp3dir=s' 			=> \$mp3dir,
		'pidfile=s' 		=> \$pidfile,
		'playeraddr=s'		=> \$localClientNetAddr,
		'stdio'				=> \$stdio,
		'streamaddr=s'		=> \$localStreamAddr,
		'prefsfile=s' 		=> \$prefsfile,
		'prefsfile2=s' 		=> \$prefsfile2,
		'quiet'   			=> \$quiet,
		'nosetup'			=> \$nosetup,
		'd_cli'				=> \$d_cli,
		'd_command'			=> \$d_command,
		'd_control'			=> \$d_control,
		'd_display'			=> \$d_display,
		'd_files'			=> \$d_files,
		'd_http'			=> \$d_http,
		'd_info'			=> \$d_info,
		'd_ir'				=> \$d_ir,
		'd_itunes'			=> \$d_itunes,
		'd_moodlogic'			=> \$d_moodlogic,
		'd_mdns'			=> \$d_mdns,
		'd_os'				=> \$d_os,
		'd_paths'			=> \$d_paths,
		'd_perf'			=> \$d_perf,
		'd_parse'			=> \$d_parse,
		'd_playlist'		=> \$d_playlist,
		'd_protocol'		=> \$d_protocol,
		'd_prefs'			=> \$d_prefs,
		'd_remotestream'	=> \$d_remotestream,
		'd_scan'			=> \$d_scan,
		'd_server'			=> \$d_server,
		'd_scheduler'		=> \$d_scheduler,
		'd_stdio'			=> \$d_stdio,
		'd_stream'			=> \$d_stream,
		'd_stream_v'		=> \$d_stream_v,
		'd_sync'			=> \$d_sync,
		'd_time'			=> \$d_time,
		'd_ui'				=> \$d_ui,
		'd_usage'			=> \$d_usage,
	)) {
		showUsage();
		exit(1);
	};
}

sub initSettings {	
	if(defined($prefsfile2)) {
		SliMP3::Prefs::load($prefsfile2, 1);
	}
	SliMP3::Prefs::load($prefsfile, $nosetup);
	SliMP3::Prefs::checkServerPrefs();
	SliMP3::Buttons::Home::updateMenu();
	SliMP3::Setup::initSetup();
	
	#options override existing preferences
	if (defined($mp3dir)) {
		SliMP3::Prefs::set("mp3dir", $mp3dir);
	}
	
	if (defined($httpport)) {
		SliMP3::Prefs::set("httpport", $httpport);
	}

	if (defined($cliport)) {
		SliMP3::Prefs::set("cliport", $cliport);
	}

	# warn if there's no mp3dir preference
	# FIXME put the strings in strings.txt
	if (!(defined SliMP3::Prefs::get("mp3dir") && 
				-d SliMP3::Prefs::get("mp3dir")) && 
				!$quiet && 
				!SliMP3::iTunes::useiTunesLibrary()) {
		msg("Your MP3 directory needs to be configured. Please open your web browser,\n");
		msg("go to the following URL, and click on the \"Server Settings\" link.\n\n");
		msg(string('SETUP_URL_WILL_BE') . "\n\t" . SliMP3::HTTP::HomeURL() . "\n");
	} else {
		if (SliMP3::Prefs::get("mp3dir") =~ m|[/\\]$|) {
			$mp3dir = SliMP3::Prefs::get("mp3dir");
			$mp3dir =~ s|[/\\]$||;
			SliMP3::Prefs::set("mp3dir",$mp3dir);
		}
	}
}

sub daemonize {
	my $pid ;
	my $uname ;
	my $uid ; 
	my @grp ; 
	use POSIX 'setsid';
	my $log;
	
	if ($logfile) { $log = $logfile } else { $log = '/dev/null' };
	
	if (!open STDIN, '/dev/null') { die "Can't read /dev/null: $!";}
	if (!open STDOUT, ">>$log") { die "Can't write to $log: $!";}
	if (!defined($pid = fork)) { die "Can't fork: $!"; }
	
	if ($pid) {
		save_pid_file($pid);
		# don't clean up the pidfile!
		$pidfile = undef;
		exit;
	}
	
	# Do we want to change the effective user or group?
	if (defined($user) || defined($group)) {
		# Can only change effective UID/GID if root
		if ($> != 0) {
			$uname = getpwuid($>) ;
			print STDERR "Current user is ", $uname, "\n" ;
			print STDERR "Must run as root to change effective user or group.\n" ;
			die "Aborting" ;
		}

		# Change effective group ID if necessary
		# Need to do this while still root, so do group first
		if (defined($group)) {
			@grp = getgrnam($group);
			if (!defined ($grp[0])) {
				print STDERR "Group ", $group, " not found.\n" ;
				die "Aborting" ;
			}

			@) = @grp ;
			if ($)[0] ne $group) {
				print STDERR "Unable to set effective group(s)",
					" to ", $group, " (", @grp, ")!\n" ;
				die "Aborting" ;
			}
		}

		# Change effective user ID if necessary
		if (defined($user)) {
			$uid = getpwnam($user);
			if (!defined ($uid)) {
				print STDERR "User ", $user, " not found.\n" ;
				die "Aborting" ;
			}

			$> = $uid ;
			if ($> != $uid) {
				print STDERR "Unable to set effective user to ",
					$user, " (", $uid, ")!\n" ;
				die "Aborting" ;
			}
		}
	}

	#$0 = "slimp3d";
	
	if (!setsid) { die "Can't start a new session: $!"; }
	if (!open STDERR, '>&STDOUT') { die "Can't dup stdout: $!"; }
}

sub checkVersion {
	if (SliMP3::Prefs::get("checkVersion")) {
		my $url = "http://update.slimdevices.com/update/?version=$VERSION&lang=" . SliMP3::Strings::getLanguage();
		my $sock = SliMP3::RemoteStream::openRemoteStream($url);
		if ($sock) {
			my $content;
			my $line;
			while ($line = <$sock>) {
				$content .= $line;
			}
			$::newVersion = $content;
		}
		if ($sock) {
			$sock->close();
		}
		SliMP3::Timers::setTimer(0, time() + 60*60*24, \&checkVersion);
	} else {
		$::newVersion = undef;
	}
}

#------------------------------------------
#
# Clean up resources and exit.
#
sub stopServer {
	$::d_server && msg("SLIMP3 Server shutting down.\n");
	cleanup();
	exit();
}

sub sigint {
	cleanup();
	exit();
}

sub cleanup {
	$::d_server && msg("SLIMP3 Server cleaning up.\n");

	if (SliMP3::Prefs::get('usetagdatabase')) {
		SliMP3::Info::stopCache();
	}
	remove_pid_file();
	SliMP3::mDNS::stopAdvertise();
}

sub save_pid_file {
	 my $process_id = shift || $$;


	$::d_server && msg("SLIMP3 Server saving pid file.\n");
	 if (defined $pidfile && -e $pidfile) {
	 	die "Process ID file: $pidfile already exists";
	 }
	 
	 if (defined $pidfile and open PIDFILE, ">$pidfile") {
		print PIDFILE "$process_id\n";
		close PIDFILE;
	 }
}
 
sub remove_pid_file {
	 if (defined $pidfile) {
	 	unlink $pidfile;
	 }
}
 
sub END {
	sigint();
}

# start up the server if we're not running as a service.	
if (!defined($PerlSvc::VERSION)) { 
	main()
};



__END__
