#!/usr/bin/php -q
<?php
if (PHP_VERSION_ID < 60000) {
	mb_internal_encoding("UTF-8");
	set_time_limit(0);
	ob_implicit_flush();
	declare(ticks = 10);
	date_default_timezone_set("Europe/Paris");
}
error_reporting(-1);
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('display_errors', true);
ini_set('display_startup_errors', true);
ini_set('log_errors', true);
ini_set('error_log', '/var/log/syslog');
//------ INIT ------------------------------------------------------------------
openlog ( "USB-AUTO" , LOG_PID , LOG_SYSLOG );
$config=array();
$config['usb_port']=array();
$config['vm']=array();
$config['dev_connected']=array();
$config['vm_list']=array();
$config['debug']=FALSE;
$proc=FALSE;
//----FUNCTION------------------------------------------------------------------
//-----------------------------------------------------------------------------
class Thread {
    const FUNCTION_NOT_CALLABLE     = 10;
    const COULD_NOT_FORK            = 15;
    private $errors = array(
        Thread::FUNCTION_NOT_CALLABLE   => 'ERROR : No valid function given to THREAD function...',
        Thread::COULD_NOT_FORK          => 'ERROR : Fork process failed...',
    );
    protected $runnable;
    private $pid;
	//--------------------------------------------
    public static function available() {
        $required_functions = array(
            'pcntl_fork',
        );
        
        foreach( $required_functions as $function ) {
            if ( !function_exists( $function ) ) {
                return false;
            }
        }
        
        return true;
    }
	//-------------------------------------------------
    public function __construct( $_runnable = null ) {
    	if( $_runnable !== null ) {
        	$this->setRunnable( $_runnable );
    	}
    }
    //-------------------------------------------------
    public function setRunnable( $_runnable ) {
        if( self::runnableOk( $_runnable ) ) {
            $this->runnable = $_runnable;
        }
        else {
            throw new Exception( $this->getError( Thread::FUNCTION_NOT_CALLABLE ), Thread::FUNCTION_NOT_CALLABLE );
        }
    }
	//-------------------------------------------------
    public function getRunnable() {
        return $this->runnable;
    }
	//--------------------------------------------------
    public static function runnableOk( $_runnable ) {
        return ( function_exists( $_runnable ) && is_callable( $_runnable ) );
    }
	//--------------------------------------------------
    public function getPid() {
        return $this->pid;
    }
	//---------------------------------------------------
    public function isAlive() {
        $pid = pcntl_waitpid( $this->pid, $status, WNOHANG );
        return ( $pid === 0 );
        
    }
	//---------------------------------------------------------
    public function start() {
        $pid = @ pcntl_fork();
        if( $pid == -1 ) {
            throw new Exception( $this->getError( Thread::COULD_NOT_FORK ), Thread::COULD_NOT_FORK );
        }
        if( $pid ) {
            $this->pid = $pid;
        }
        else {
            pcntl_signal( SIGTERM, array( $this, 'signalHandler' ) );
            $arguments = func_get_args();
            if ( !empty( $arguments ) ) {
                call_user_func_array( $this->runnable, $arguments );
            }
            else {
                call_user_func( $this->runnable );
            }
            
            exit( 0 );
        }
    }
	//------------------------------------------------------------------------
    public function stop( $_signal = SIGTERM, $_wait = false ) {
        if( $this->isAlive() ) {
            posix_kill( $this->pid, $_signal );
            if( $_wait ) {
                pcntl_waitpid( $this->pid, $status = 0 );
            }
        }
    }
	//------------------------------------------------------------------------
    public function kill( $_signal = SIGKILL, $_wait = false ) {
        posix_kill( $this->pid, $_signal );
    }
    //------------------------------------------------------------------------
    public function getError( $_code ) {
        if ( isset( $this->errors[$_code] ) ) {
            return $this->errors[$_code];
        }
        else {
            return "error " . $_code;
        }
    }
	//--------------------------------------------------------------------------
    protected function signalHandler( $_signal ) {
        switch( $_signal ) {
            case SIGTERM:
                echo "SIGTERM via TREAD";
				exit( 0 );
            break;
        }
    }
	//---------------------------------------------------------------------------
}
//------------------------------------------------------------------------------
function myexec($commande) {
	global $config;
	if ($config['debug']) logme("MYEXEC -> ".$commande);
	exec ($commande, $result, $return);
	if ($return > 0) logme ("EXEC : ".$commande." -> ".$return);
	return $result;
}
//------------------------------------------------------------------------------
function logme ($data) {
	syslog ( LOG_INFO , $data );
}
//------------------------------------------------------------------------------
function become_daemon() { 

	$pid = pcntl_fork(); 
	if ($pid == -1) { 
		exit(); 
    }
	elseif ($pid) { 
		exit(); 
    }
	else { 
		posix_setsid(); 
		chdir('/'); 
		umask(0); 
		return posix_getpid(); 
    } 
}
//------------------------------------------------------------------------------
function sig_handler($sig) { 
	global $proc;
	global $sock;
    switch($sig) { 
        case SIGTERM:
			exit(0);
		break;
        case SIGINT:
			exit(0);
        break; 		
    } 
}
//------------------------------------------------------------------------------
function sig_handler_thread($sig) { 
	global $proc;
    switch($sig) { 
        case SIGTERM:
			logme ("thread received SIGTERM");
			if (is_resource($proc)) {
				proc_terminate($proc, 15);
			}
			exit(0);
		break;
        case SIGINT:
			logme ("thread received SIGINT");
			if (is_resource($proc)) {
				proc_terminate($proc, 15);
			}
			exit(0);
        break; 
    } 
}
//------------------------------------------------------------------------------
function thread_check_syslog() {
	global $config;
	global $proc;
	pcntl_signal(SIGTERM, 'sig_handler_thread'); 
	pcntl_signal(SIGINT, 'sig_handler_thread');
	pcntl_signal(SIGCHLD, SIG_IGN);
	logme("check_syslog start ...");
	$descriptorspec = array(
       0 => array("pipe", "r"),   // stdin
       1 => array("pipe", "w"),  // stdout
       2 => array("pipe", "w")   // stderr
	);
	if ($proc = proc_open("exec tail -f /var/log/syslog -n 0", $descriptorspec, $pipes, null, null)) {
		while ($proc) {
			$proc_status = proc_get_status ( $proc );
			if (!$proc_status['running']) break;
			$read   = array($pipes[1]);
			$write  = NULL;
			$except = NULL;
			if (@stream_select($read, $write, $except, 0, 500000) > 0) { 
				$line = fgets($pipes[1]);
				preg_match("/^.*\susb\s(?P<port>[0-9\-\.]+)\:\s.*\sidVendor\=(?P<vendor>[a-z0-9]+)\,\sidProduct\=(?P<product>[a-z0-9]+)$/", $line, $match);
				if (isset($match['port']) AND isset($match['vendor']) AND isset($match['product'])) {
					$is_usb3=0;
					if (substr($match['port'],0,1)=="2") $is_usb3=1;
					logme("DETECT ".$match['port']." -> ".$match['vendor'].":".$match['product']." USB=".$is_usb3);
					$t_port=explode(".",$match['port']);
					$t_port=$t_port[0];
					if (isset($config['usb_port'][$t_port])) {
						logme($match['port']." is assign to VM ".$config['usb_port'][$t_port]. "(".$t_port.")");
						assign_to_vm($match['vendor'], $match['product'], $config['usb_port'][$t_port],$is_usb3, $match['port']);
					}
					else {
						logme($match['port']." is not assigned to VM");
					}
				}
				preg_match("/^.*\susb\s(?P<port>[0-9\-\.]+)\:\sUSB\sdisconnect.*$/", $line, $match);
				if (isset($match['port'])) {
					logme ("Disconnect ".$match['port']);
					unbind_from_vm($match['port']);
				}
			}
		}
	}
	logme("check_syslog stopped !");
	proc_terminate($proc, 15);
	sleep (1);
}
//------------------------------------------------------------------------------
function assign_to_vm($vendor, $product, $vm, $is_usb3, $port) {
	global $config;
	$config['vm'][$port]=array();
	$config['vm'][$port]['vm']=$vm;
	$config['vm'][$port]['vendor']=$vendor;
	$config['vm'][$port]['product']=$product;
	$config['vm'][$port]['is_usb3']=$is_usb3;
	if ($is_usb3) {
		passthru("pvesh create /nodes/localhost/qemu/".$vm."/monitor --command 'device_add usb-host,bus=xhci.0,vendorid=0x".$vendor.",productid=0x".$product.",id=auto".$port."'");
		logme("ASSIGN ".$vendor.":".$product." to VM ".$vm." ID : auto".$port."  USB 3");
		$config['vm_list'][$vm]=$config['vm_list'][$vm]." -device usb-host,bus=xhci.0,vendorid=0x".$vendor.",productid=0x".$product.",id=auto".$port;
		passthru("qm set ".$vm." -args \"".$config['vm_list'][$vm]."\"");
	}
	else {
		passthru("pvesh create /nodes/localhost/qemu/".$vm."/monitor --command 'device_add usb-host,vendorid=0x".$vendor.",productid=0x".$product.",id=auto".$port."'");
		logme("ASSIGN ".$vendor.":".$product." to VM ".$vm." ID : auto".$port."  USB1/2");
		$config['vm_list'][$vm]=$config['vm_list'][$vm]." -device usb-host,vendorid=0x".$vendor.",productid=0x".$product.",id=auto".$port;
		passthru("qm set ".$vm." -args \"".$config['vm_list'][$vm]."\"");
	}
	
}
//------------------------------------------------------------------------------
function unbind_from_vm($port) {
	global $config;
	if (isset($config['vm'][$port])) {
		$vm=$config['vm'][$port]['vm'];
		logme ("UNBIND ".$port." from VM ".$vm);
		passthru("pvesh create /nodes/localhost/qemu/".$vm."/monitor --command 'device_del auto".$port."'");
		if ($config['vm'][$port]['is_usb3']) {
			$new=str_replace("-device usb-host,bus=xhci.0,vendorid=0x".$config['vm'][$port]['vendor'].",productid=0x".$config['vm'][$port]['product'].",id=auto".$port, "", $config['vm_list'][$vm]);
		}
		else {
			$new=str_replace("-device usb-host,vendorid=0x".$config['vm'][$port]['vendor'].",productid=0x".$config['vm'][$port]['product'].",id=auto".$port, "", $config['vm_list'][$vm]);
		}
		$config['vm_list'][$vm]=$new;
		passthru("qm set ".$config['vm'][$port]['vm']." -args \"".$new."\"");
		unset ($config['vm'][$port]);
	}
}
//------------------------------------------------------------------------------
function load_usb_list_from_pve() {
	global $config;
	foreach ($config['vm_list'] AS $k=>$v) {
		$args="";
		$t=myexec("qm config ".$k." | grep args:");
		if (isset($t[0])) {
			$tt=explode(" -",$t[0]);
			if ($tt[0]=="args:") {
				foreach($tt AS $a=>$b) {
					$find=FALSE;
					$must_unbind=FALSE;
					preg_match("/^device\susb\-host\,bus\=xhci\.0\,vendorid\=0x(?P<vendor>[a-z0-9]+)\,productid\=0x(?P<product>[a-z0-9]+)\,id\=auto(?P<port>[a-z0-9\-\.]+)$/", $b, $match);
					if (isset($match['vendor']) && isset($match['product']) && isset($match['port'])) {
						$port=$match['port'];
						$config['vm'][$port]=array();
						$config['vm'][$port]['vm']=$k;
						$config['vm'][$port]['vendor']=$match['vendor'];
						$config['vm'][$port]['product']=$match['product'];
						$config['vm'][$port]['is_usb3']=1;
						$find=TRUE;
						$id=$match['vendor'].":".$match['product'];
						if (!isset($config['dev_connected'][$id])) {
							$must_unbind=TRUE;
						}
					}
					preg_match("/^device\susb\-host\,vendorid\=0x(?P<vendor>[a-z0-9]+)\,productid\=0x(?P<product>[a-z0-9]+)\,id\=auto(?P<port>[a-z0-9\-\.]+)$/", $b, $match);
					if (isset($match['vendor']) && isset($match['product']) && isset($match['port'])) {
						$port=$match['port'];
						$config['vm'][$port]=array();
						$config['vm'][$port]['vm']=$k;
						$config['vm'][$port]['vendor']=$match['vendor'];
						$config['vm'][$port]['product']=$match['product'];
						$config['vm'][$port]['is_usb3']=0;
						$find=TRUE;
						$id=$match['vendor'].":".$match['product'];
						if (!isset($config['dev_connected'][$id])) {
							$must_unbind=TRUE;
						}
					}
					if ((!$find) && ($a!=0)) {
						$args=$args." -".$b;
					}
				}
				$config['vm_list'][$k]=$args;
				if ($must_unbind) {
					unbind_from_vm($port);
				}
			}
		}
	}
}
//------------------------------------------------------------------------------
function get_config() {
	global $config;
	$t=file_get_contents("/etc/usb-auto.conf");
	$t=explode("\n", $t);
	foreach($t AS $k=>$v) {
		if (substr($v,0,1)!="#") {
			$a=explode ("|",$v);
			if (isset($a[0]) AND isset($a[1])) {
				if (!isset($config['vm_list'][$a[1]])) {
					$config['vm_list'][$a[1]]=array();
				}
				$config['usb_port'][$a[0]]=$a[1];
				logme ("GET conf : find : ".$a[0]." -> VM ".$a[1]);
			}
		}
	}
	$b=myexec("lsusb");
	foreach ($b AS $c=>$d) {
		preg_match("/^Bus\s.*ID\s(?P<vendor>[a-z0-9]+)\:(?P<product>[a-z0-9]+)\s.*$/", $d, $match);
		if (isset($match['vendor']) AND isset($match['product'])) {
			$id=$match['vendor'].":".$match['product'];
			$config['dev_connected'][$id]=TRUE;
		}
	}
}
//------------------------------------------------------------------------------
function add_to_vm_conf() {
	global $config;
}
//------------------------------------------------------------------------------
function del_from_vm_conf() {
	global $config;
}
//-----BECOME DAEMON------------------------------------------------------------
logme("MAIN start ...");
if (!file_exists("/etc/usb-auto.conf")) {
	logme("ERROR -> config file not exist");
	exit(0);
}
if (function_exists ( "apc_store" )) {
	apc_store("test", "test");
	$t=apc_fetch("test");
	if ($t!=="test") {
		echo("ERROR -> PHP APC n\'est pas disponible sur ce système -> exit\n");
		logme("ERROR -> PHP APC n\'est pas disponible sur ce système -> exit");
		exit();
	}
}
else {
	echo("ERROR -> PHP APC n\'est pas disponible sur ce système -> exit\n");
	logme("ERROR -> PHP APC n\'est pas disponible sur ce système -> exit");
	exit();
}
//---------------INIT CONFIG---------------
get_config();
load_usb_list_from_pve();
//----------BECOME A DAEMON-------------------------------
$__parent_pid = become_daemon();
passthru("echo '".getmypid()."' > /var/run/usb-auto");
pcntl_signal(SIGTERM, 'sig_handler'); 
pcntl_signal(SIGINT, 'sig_handler');
pcntl_signal(SIGCHLD, SIG_IGN);
//---MAIN PROGRAM---------------------------------------------------------------
//---START THREAD---------------------------------------------------------------------
$a=0;
$thread_task_arg = array();
$a++;$thread_task[$a] = new Thread( 'thread_check_syslog' );$thread_task[$a]->start();apc_store("thread_check_syslog", $thread_task[$a]->getPid());
//---MAIN BECOME WATCHDOG-------------------------------------------------------
proc_nice(19);
while (TRUE) {
	sleep (5);
	foreach ($thread_task AS $key => $value) {
		if (!$thread_task[$key]->isAlive()) {
			if (isset($thread_task_arg[$key])) {
				$thread_task[$key]->start($thread_task_arg[$key]);
			}
			else {
				$thread_task[$key]->start();
			}
		}
		sleep(1);
	}
}
?>