Newer
Older
backup-commander / html / svc / main.php
<?php

/*
 * This is the backup process CLI application.
 * 
 * Rather than use 2 threads, I've decided to poll. This is because the threading in PHP is no longer supported.
 * Instead we should use parallel:
 * 	https://www.php.net/manual/en/parallel.run.php
 * which I don't like. Basically, it doesn't look like you can share code (by way of a reference)?!!
 * 
 * So we are polling the RPC layer every 0.1secs. In practice, this will be plenty for the backup app.
 * 
 */
include __DIR__."/../lib/sysVcom.php";

//create the comms file
touch('BU-commander');

//remove all queues at startup
clear_IPC( '../svc/BU-commander' );

rpc_setGlobals( 'procName', '../svc/BU-commander' );
rpc_setGlobals( 'max_chan', 8182 );

// problem is that the paths are not correct wrt this location, so we need to adjust them
include __DIR__."/../inc/paths.php";
$g_database_path = '../' . $g_database_path;

include_once "db-reads.php";
include_once "db-writes.php";

// get the latest data and cache it
$g_bu_states = null;
$g_bu_table = array();
$g_bu_colIdxs = array();
getbu_list_all( $g_bu_table, $g_bu_colIdxs, 'order by BU_DATE desc, BU_TIME desc' );

//print_r($g_bu_table);
//print_r( $g_bu_table[18][ $g_bu_colIdxs['Dir_Dest'] ]);


while(true){

	$bDidWork = false;
	
	$bDidWork |= check_rpc_clients();
	$bDidWork |= check_bu_sched();
	
	if(!$bDidWork){
		time_nanosleep( 0, 100000000);
	}
		
}

/**
 * Clears all cached data. This is called after any DB updates. It can be called at any time since loading is lazy.
 */
function clear_cached_data(){
	
	global $g_bu_states;
	global $g_bu_table;
	
	$g_bu_states = null;
	$g_bu_table = array();
}

/**
 * Return true if the backup was kicked off
 */
function check_bu_sched(){
	
	global $g_bu_table;
	global $g_bu_colIdxs;

	//get the first non null entry which is not running
	$keys = array_keys( $g_bu_table );
	$idfound = 0;
	
	foreach ($keys as $key) {		
		if( $g_bu_table[$key][ $g_bu_colIdxs['BU_DATE'] ] == '' ) continue;		//skip anything without a date
		if( $g_bu_table[$key][ $g_bu_colIdxs['BuRunning'] ] == 1 ) continue;		//skip any already running
		if( $g_bu_table[$key][ $g_bu_colIdxs['BuError'] ] != null ) continue;	//skip entries with problems
		$idfound = $g_bu_table[$key][ $g_bu_colIdxs['BUID'] ];
	}
	
	if($idfound==0) return;
	
	// is it time...?
	$str_bu_dt = $g_bu_table[$idfound][ $g_bu_colIdxs['BU_DATE']] . ' ' . $g_bu_table[$idfound][ $g_bu_colIdxs['BU_TIME']] . ':00';
	$schedTime = new DateTime( $str_bu_dt, new DateTimeZone("UTC") );
	
	if( $schedTime > new DateTime('now', new DateTimeZone("UTC") ) ){
		//...no it's not
		return;
	}
	
	kickoff_backup( $idfound );

	print( "check after forked: running = ". $g_bu_table[$idfound][ $g_bu_colIdxs['BuRunning'] ] . "\n");

	return true;
}

/**
 * Kicks off a back up. Returns immediately. Deligates the work to a client fork.
 * 
 * $scheduled:
 * 	true if it is run according to the $schedule info in the db
 * 	false if it has been manually initiated
 */
function kickoff_backup( $buid, $scheduled = true, $bu_test = false ){

	global $g_bu_table;
	global $g_bu_colIdxs;

	$isRunning = $g_bu_table[$buid][ $g_bu_colIdxs['BuRunning'] ];
	if( !$bu_test && $isRunning==1) return array("e:Error", "Backup already running" );
	
	$i=0;
	$bu_row[$i++] = $g_bu_table[$buid][ $g_bu_colIdxs['BUName'] ];
	$bu_row[$i++] = $g_bu_table[$buid][ $g_bu_colIdxs['Dir_Src'] ];
	$bu_row[$i++] = $g_bu_table[$buid][ $g_bu_colIdxs['Dir_Dest'] ];
	$bu_row[$i++] = $g_bu_table[$buid][ $g_bu_colIdxs['Files_Ex'] ];
	$bu_row[$i++] = $g_bu_table[$buid][ $g_bu_colIdxs['BuType'] ];
	$bu_row[$i++] = $isRunning;
	
	// we should be able to use pcntl_fork()
	$pid = pcntl_fork();
	if ( $pid == -1 ) {        
		print("fork failed");
		return array("e:Error", "fork failed" );
	}

	if ( $pid ) {
		// parent process
		print( "parentpid = ". getmypid() . "\n" );
		$g_bu_table[$buid][ $g_bu_colIdxs['BuRunning'] ] = 1;
		return array("ok:", "running" );
		 
	} else {
		
		// child process
				
		$g_bu_table = array();	//data here is unique to the parent
		$pid = getmypid();
		print( "childpid = $pid\n" );
				
		print( "kickoff_backup for $buid\n" );
		include_once __DIR__."/../inc/bu-common.php";
		$bu_result = '';
		global $g_data_dir;
		$g_data_dir = __DIR__."/../data";
		
		$bu_result = run_backup_block( $buid, $bu_row, $bu_test );
	
		print( "backup ended for $buid, result = $bu_result\n" );
		$obj = rpc( 'm:fork-complete', $pid, "backup-complete", $buid, $scheduled );	//we use our rpc mechanism to notify parent
		//print_r( $obj );
		//print( "\n" );
		exit(0);
	}
	
}



$msgArr = null;

/**
 * Return true if a request was found and handled
 */
function check_rpc_clients(){
	
	$caller_id = 0;
	
	$obj_in = rpc_listen( $caller_id, $msgArr, false );
	$obj_out = null;

	if($caller_id==0){
		//nothing to do
		return false;
	}
	
	try{
		$obj_out = handle_rpc_req( $obj_in );
		
	}catch( Exception $e ){
		print( "Exception: " . $e->getMessage() ."\n" );
		$obj_out = array( 'e:Exception', $e );
	}
	
	//send msg back
	rpc_reply( $caller_id, $obj_out );

	//done with the message
	unset( $msgArr[$caller_id] );
	
	if( $obj_in[0] == 'm:fork-complete' ){
		if( count($obj_in)<5){
			print("m:fork-complete only has". count($obj_in) . " params, should be 5, skipping client cleanup\n");
			print_r( $obj_in );
			return true;
		}
		print("now wait for child pid = {$obj_in[1]} \n");
		// this is required to clean up the child process
		$status;
		pcntl_waitpid( $obj_in[1], $status );

		if( $obj_in[2]="backup-complete"){
			backupComplete( $obj_in[3], $obj_in[4] );			
		}
	}
	
	return true;
	
}

/**
 * Called by the parent after a client notified that a backup completed.
 */
function backupComplete( $buid, $scheduled ){
	
	global $g_bu_table;	
	global $g_bu_colIdxs;
		
	//reload the table data - client updates the error field (might move this)
	clear_cached_data();
	$nothanks = null;
	getbu_list_all( $g_bu_table, $nothanks, 'order by BU_DATE desc, BU_TIME desc' );

	include_once __DIR__."/../inc/bu-common.php";
	
	if($scheduled){
		//if DB error, do nothing
		$err = $g_bu_table[$buid][ $g_bu_colIdxs['BuError'] ];
		if($err==''){
			//update date and time according to the repeat field.
			$date = date_create( $g_bu_table[$buid][ $g_bu_colIdxs['BU_DATE'] ] );
			$dt_new = null;
			
			switch( $g_bu_table[$buid][ $g_bu_colIdxs['BU_REPEAT'] ] ){
			case 'M':
				//monthly
				date_add( $date, new DateInterval('P1M') );
				$dt_new = date_format( $date, "Y-m-d");
				break;
				
			case 'W':
				//weekly
				date_add( $date, new DateInterval('P1W') );
				$dt_new = date_format( $date, "Y-m-d");
				break;
				
			case 'D':
				//daily
				date_add( $date, new DateInterval('P1D') );
				$dt_new = date_format( $date, "Y-m-d");
				break;
				
			}			
			$g_bu_table[$buid][ $g_bu_colIdxs['BU_DATE'] ] = $dt_new;

			set_sched_date( $buid, $dt_new );
			
		}
	}//if manually kicked off - nothing to do

	//TODO: remove the outputfiles
	
	$dt = gmdate("Y-m-d H:i:s");
	set_last_run_date( $buid, $dt );
	$g_bu_table[$buid][ $g_bu_colIdxs['LastRunDt'] ] = $dt;

}


/**
 * Carries out the task requested from any RPC client.
 * That client may be from the webserver or from a forked child or any other process.
 */
function handle_rpc_req( $obj ){
	
	// The idea will be that the indicator before the colon will be handled generically in cases
	// where it is a simple function call, i.e. 'f:' (provided it is a trusted source and/or has been previously checked)
	
	print_r( $obj );
	
	switch( $obj[0] ){
	case 'm:fork-complete':
		//send the same message back to the caller
		return $obj;
		
	case 'f:getbu_list_all':
		//($table, $colIdxs) = rpc( 'f:getbu_list_all', "order by BUID" );
		$table = array();
		$colIdxs = array();
		getbu_list_all( $table, $colIdxs, $obj[1] );
		return array( $table, $colIdxs );
		
	case 'f:getbu_list_state':
		global $g_bu_states;
		if($g_bu_states==null){
			$g_bu_states = getbu_list_state();
		}
		return $g_bu_states;
		
	case 'f:save_bu_schedule':
		$err = save_bu_schedule( $obj[1] );
		clear_cached_data();
		return array($err);
		
	case 'f:save_bu_item':
		//[$err] = rpc( 'f:save_bu_item', $postVars, $bu_id );
		$err = save_bu_item( $obj[1], $obj[2] );
		clear_cached_data();
		return array($err);
		
	case 'f:set_bu_error':
		print("setting backup error\n");
		$err = set_bu_error_db( $obj[1], $obj[2] );
		clear_cached_data();
		return array($err);
			
	case 'f:set_bu_running':
		$err = set_bu_running_db( $obj[1], $obj[2] );
		clear_cached_data();
		return array($err);	
				
	case 'f:kickoff-backup':
		$buid = $obj[1];
		$isTest = $obj[2]==1;
		//caller expects param1 to be one of error, done or running
		//we've changed this slightly e:Error, ok:, done is not working yet
		return kickoff_backup( $buid, false, $isTest );

	}
		
	array_unshift( $obj, "Unknown Command" );
	print_r( $obj );
	return $obj;
	
}

?>