From 54d90dbc803bc8a9a108481aa0f8b10ef7e71554 Mon Sep 17 00:00:00 2001
From: Benjamin Renard <benjamin.renard@akka.eu>
Date: Tue, 29 Jun 2021 13:32:12 +0200
Subject: [PATCH] Add concurrent access to statistics files

---
 php/classes/AmdaStats.php | 285 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------
 1 file changed, 159 insertions(+), 126 deletions(-)

diff --git a/php/classes/AmdaStats.php b/php/classes/AmdaStats.php
index 08a0a79..2e753f0 100644
--- a/php/classes/AmdaStats.php
+++ b/php/classes/AmdaStats.php
@@ -6,8 +6,8 @@
  
 class AmdaStats {
 
-	public $statXml;
-	public $tasks = array('plot', 'mining', 'print', 'statistic');
+        public $statsFilePath;
+	public $tasks = array('plot', 'mining', 'print', 'statistics');
 	public $tasksWs = array('ws_print', 'ws_plot');
 	public $tasksAdd = array('ttoper', 'samp', 'upload', 'create', 'images');
 	public $usersToExclude = array('bouchemit');
@@ -17,92 +17,139 @@ class AmdaStats {
 				'getparameter'=>'ws_print', 'getdataset' => 'ws_print', 'getorbites' => 'ws_print', 'getplot' => 'ws_plot');
 	
 	public function __construct($user) {
-		
-		$this->statXml = new DomDocument('1.0','UTF-8');
-		$this->statXml->preserveWhiteSpace = false;
-		$this->statXml->formatOutput = true;
-	
-		if (!defined("StatsXml")){
-			$thisYear = date("Y");
-			if (!$user){  
-			// general - to read
-			// define('StatsXml',DATAPATH.'Statistics/Stats.xml');
-				define("StatsXml",DATAPATH."Statistics/Stats$thisYear.xml");
-			// if (file_exists(StatsXml)) unlink(StatsXml);
-			}
-			else {
-			// individual - to write
-			// define("StatsXml", USERPATH."/".$user."/Stats.xml");
-				define("StatsXml", USERPATH.$user."/Stats$thisYear.xml"); 
-				$this->user = $user;
-			}
+		if (!is_dir(DATAPATH.'Statistics')) {
+			if (!mkdir(DATAPATH.'Statistics', 0775)) $this->success = false; //return -1;
+			if (!chgrp(DATAPATH.'Statistics', APACHE_USER)) $this->success = false; // return -1;
 		}
 
-		if (!file_exists(StatsXml)) {
-			if (!is_dir(DATAPATH.'Statistics')) {
-					if (!mkdir(DATAPATH.'Statistics', 0775)) $this->success = false; //return -1;
-					if (!chgrp(DATAPATH.'Statistics', APACHE_USER)) $this->success = false; // return -1;                 
-			}
+		$this->statsFilePath = $this->getStatsFilePath($user);
+	}
 
-			$status = $this->generateXml(); 
-			if (!$status) {
-				error_log('Cannot create Stats.xml: Fatal Error '.$user,1,email); 
-				$this->success = false;
-			}
+	public function addTask($user, $task, $vars) {
+		$this->concurrentAccessGuestFile(array($this,'_addTask'),array('user' => $user, 'task' => $task, 'vars' => $vars));
+	}
+
+	public function getModulesStat($start, $stop, $update) {
+		return $this->concurrentAccessGuestFile(array($this,'_getModulesStat'), array('start' => $start, 'stop' => $stop, 'update' => $update));
+	}
+
+	public function mergeXml($year) {
+		return $this->concurrentAccessGuestFile(array($this,'_mergeXml'), array('year' => $year));
+	}
+
+	public function getDataStat($index, $start, $stop, $update) {
+		return $this->concurrentAccessGuestFile(array($this,'_getDataStat'), array('index' => $index, 'start', $start, 'stop' => $stop, 'update' => $update));
+	}
+
+	public function mergeStats($inXml) {
+		return $this->concurrentAccessGuestFile(array($this,'_mergeStats'), array('inXml' => $inXml));
+	}
+
+	private function getStatsFilePath($user) {
+		$thisYear = date("Y");
+		if (empty($user)){		
+			return DATAPATH."Statistics/Stats$thisYear.xml";
 		}
-		else {
-			$status = $this->statXml->load(StatsXml); 
-			if (!$status) {
-				$status = $this->generateXml(); 
-				$msg = $status ? 'Cannot load Stats.xml. New Stats.xml was created' : 
-						'Cannot load Stats.xml. Cannot create Stats.xml: Fatal Error ';
-						
-				error_log($msg.$user,1,email);
-				
-				if (!$status) 
-					$this->success = false;
+		return USERPATH.$user."/Stats$thisYear.xml";
+	}
+
+	private function concurrentAccessGuestFile($callback, $options) {
+		$lockFile =  $this->statsFilePath.".lockfile";
+
+		$fp = fopen($lockFile, "w+");
+
+		if ($fp === false) {
+			return false;
+		}
+
+		$res = true;
+
+		if (flock($fp, LOCK_EX))
+		{
+			$newFile = FALSE;
+			if (!file_exists($this->statsFilePath)) {
+				$res = $this->_generateXml();
 			}
-			else {
-				$allTasks = array_merge($this->tasks, $this->tasksAdd, $this->tasksWs);
-				$newTask = FALSE;
-				foreach ($allTasks as $task) {
-					$items = $this->statXml->getElementsByTagName($task);
-					if ($items->length == 0) {
-						//add missing task
-						$element = $this->statXml->createElement("$task");
-						$this->statXml->documentElement->appendChild($element);
-						$newTask = TRUE;
+
+			if ($res) {
+				$dom = new DomDocument("1.0","UTF-8");
+				$dom->preserveWhiteSpace = false;
+				$dom->formatOutput = true;
+				$res = $dom->load($this->statsFilePath);
+				if (!$newFile) {
+					// add missing tasks if needed
+					$allTasks = array_merge($this->tasks, $this->tasksAdd, $this->tasksWs);
+					$newTask = FALSE;
+					foreach ($allTasks as $task) {
+						$items = $dom->getElementsByTagName($task);
+						if ($items->length == 0) {
+							//add missing task
+							$element = $dom->createElement("$task");
+							$dom->documentElement->appendChild($element);
+							$newTask = TRUE;
+						}
 					}
+					if ($newTask) {
+						$dom->save($this->statsFilePath);
+					}					
 				}
-               			if ($newTask) {
-					$this->statXml->save(StatsXml);
+				if ($res) {
+					$func_res = call_user_func($callback,$dom, $options);
 				}
 			}
-		}         
+		}
+		else
+			$res = false;
+
+		fclose($fp);
+
+		if ($res)
+			return $func_res;
+
+		return false;
+	}
+
+        private function _generateXml() {
+		$dom = new DomDocument("1.0","UTF-8");
+		$dom->preserveWhiteSpace = false;
+		$dom->formatOutput = true;
+
+		$rootElement = $dom->createElement('stats');
+
+		$allTasks = array_merge($this->tasks, $this->tasksAdd, $this->tasksWs);
+
+		foreach ($allTasks as $task) {
+			$element = $dom->createElement("$task");
+			$rootElement->appendChild($element);
+		}
+
+		$dom->appendChild($rootElement);
+
+		return $dom->save($this->statsFilePath);
 	}
 
 /*
 *  Merge individual User Stats.xml into one generique Stats.xml
 */
-	public function mergeXml($year) {
+	private function _mergeXml($dom, $options) {
 		// long procedure
 		ini_set('max_execution_time', 600); 
 		
 		$allTasks = array_merge($this->tasks, $this->tasksAdd, $this->tasksWs);
 
 		$userDoc = new DomDocument("1.0");
-		if ($year == null) $year = date("Y");
+		if ($options['year'] == null) $options['year'] = date("Y");
 
 		$users=glob(USERPATH."*");
 		foreach ($users as $user) {        
-			 $userXmlPath = $user."/Stats$year.xml";
+			$userXmlPath = $user."/Stats".$options['year'].".xml";
 
 			if (!file_exists($userXmlPath)) continue;
 
 			$userDoc->load($userXmlPath);
 
 			foreach ($allTasks as $task) {
-				$globalTaskItems = $this->statXml->getElementsByTagName($task);
+				$globalTaskItems = $dom->getElementsByTagName($task);
 				if ($globalTaskItems->length == 0)
 					continue;
 				$globalTaskItem = $globalTaskItems->item(0);
@@ -113,7 +160,7 @@ class AmdaStats {
 				$userItems = $userTaskItem->getElementsByTagName("item");
 				if ($userItems->length > 0) {
 					foreach ($userItems as $userItem) {
-						$globalItem = $this->statXml->importNode($userItem, true);
+						$globalItem = $dom->importNode($userItem, true);
 						$globalTaskItem->appendChild($globalItem);
 					}
 				}          
@@ -121,98 +168,79 @@ class AmdaStats {
 		}
 
 		// write task statistics as json
-		$this->getModulesStat(null,null,true);
+		$this->_getModulesStat($dom, array('start' => null, 'stop' => null, 'update' => TRUE));
 		// write data statistics as json
-		$this->getDataStat(0,null,null,true);
+		$this->_getDataStat($dom, array('index' => 0, 'start', null, 'stop' => null, 'update' => true));
 	
-		return  $this->statXml->save(StatsXml);
+		return  $dom->save($this->statsFilePath);
 	}
 
-	private function generateXml() {
-
-		$rootElement = $this->statXml->createElement('stats');
-
-		$allTasks = array_merge($this->tasks, $this->tasksAdd, $this->tasksWs);
-		
-		foreach ($allTasks as $task) {            
-			$element = $this->statXml->createElement("$task");        
-			$rootElement->appendChild($element);        
-		}
-
-		$this->statXml->appendChild($rootElement);
-
-		return $this->statXml->save(StatsXml);
-	}
- 
-	public function addTask($user, $task, $vars) {
-
-// 		if (!$this->user) {
-// 			error_log('User is null', 1, email);
-// 			return;
-// 		}
-		if ($task == 'killplot')
-			return;
+	private function _addTask($dom, $options) {
+		if ($options['task'] == 'killplot')
+			return FALSE;
 			
-		if ($vars)
-			$realTask = $this->task[$task];
+		if ($options['vars'])
+			$realTask = $this->task[$options['task']];
 		else
-			$realTask = $task;
+			$realTask = $options['task'];
 
-		if (!in_array($user, $this->usersToExclude)) {
-			$taskElementNode = $this->statXml->getElementsByTagName("$realTask");
+		if (!in_array($options['user'], $this->usersToExclude)) {
+			$taskElementNode = $dom->getElementsByTagName("$realTask");
 
 			if ($taskElementNode->length < 1) 
 					return; 
 					
 			$taskElement = $taskElementNode->item(0);
 			if (is_object($taskElement)) {
-				$newTask = $this->statXml->createElement('item');
+				$newTask = $dom->createElement('item');
 				$newTask->setAttribute('date', date('Y-m-d'));
-				$newTask->setAttribute('user', $user);
+				$newTask->setAttribute('user', $options['user']);
            
-				if ($vars) { 
+				if ($options['vars']) { 
 					$ID = array();
 
-					foreach ($vars as $var) {
+					foreach ($options['vars'] as $var) {
 						$ID[] = $var;
 					}
 						
 					$ID = array_unique($ID);
 
 					foreach ($ID as $id) {
-						$datasetElement = $this->statXml->createElement('dataset', $id);
+						$datasetElement = $dom->createElement('dataset', $id);
 						$newTask->appendChild($datasetElement);  
 					}         
 				}
 				
 				$taskElement->appendChild($newTask);
-				$this->statXml->save(StatsXml);  
+				return $dom->save($this->statsFilePath);  
 			}
 			else 
-				error_log('Check Stats.xml - no task element '.$task, 1, email);
+				error_log('Check Stats.xml - no task element '.$options['task'], 1, email);
 		}
+
+		return FALSE;
 	}
  
 /*
 *     Show Statistics
 */
-	public function getModulesStat($start, $stop, $update){
+	private function _getModulesStat($dom, $options){
 
-		if (!$update && file_exists(DATAPATH.'Statistics/tasks.json')) {
-					return  file_get_contents(DATAPATH.'Statistics/tasks.json');
+		if (!$options['update'] && file_exists(DATAPATH.'Statistics/tasks.json')) {
+			return  file_get_contents(DATAPATH.'Statistics/tasks.json');
 		}
 		
 		$taskArray = array();
 
 		foreach (array_merge($this->tasks,$this->tasksAdd, $this->tasksWs) as $task) {
-			$taskItems = $this->statXml->getElementsByTagName($task);
+			$taskItems = $dom->getElementsByTagName($task);
 			if ($taskItems->length < 1)
 				return;
 			$theTask = $taskItems->item(0);
 			$items = $theTask->getElementsByTagName('item');
 			$hints = $items->length;
 
-			$startStop = $this->getStartStop($items, $start, $stop);
+			$startStop = $this->getStartStop($items, $options['start'], $options['stop']);
 			
 			$taskArray[] = array('task' => $task, 'number' => $hints,
 				'start' => $startStop[0], 'stop' => $startStop[1]); 
@@ -228,9 +256,9 @@ class AmdaStats {
 /*
 *     Show Statistics
 */
-	public function getDataStat($index, $start, $stop, $update){
+	private function _getDataStat($dom, $options){
 
-		if (!$update && file_exists(DATAPATH.'Statistics/data.json')) {
+		if (!$options['update'] && file_exists(DATAPATH.'Statistics/data.json')) {
 			$GENERALarray = json_decode(file_get_contents(DATAPATH.'Statistics/data.json'));
 		}
 		else {
@@ -243,7 +271,7 @@ class AmdaStats {
 			$dataTasks = array_merge($this->tasks, $this->tasksWs);
 
 			foreach ($dataTasks as $task) {
-				$taskItems = $this->statXml->getElementsByTagName($task);
+				$taskItems = $dom->getElementsByTagName($task);
 				if ($taskItems->length < 1)
 					continue;
 				$theTask = $taskItems->item(0);
@@ -258,10 +286,15 @@ class AmdaStats {
 					foreach ($VIs as $VI) {
 						$id = $VI->nodeValue;
 						if ($id) {
-
+							if (!in_array($id, $usersArray)) {
+								$usersArray[$id] = array();
+							}
+							if (!in_array($user, $usersArray[$id])) {
+								$usersArray[$id][$user] = 0;
+							}
 							$usersArray[$id][$user]++;
 
-							if ($TASKarray[$id]) {
+							if (array_key_exists($id, $TASKarray)) {
 								$TASKarray[$id]++;
 								$TOTALarray[$id]++;
 								if ($STARTarray[$id] > $time) 
@@ -270,7 +303,7 @@ class AmdaStats {
 											$STOParray[$id] = $time;
 							}
 							else {
-								if (!$TOTALarray[$id]) { 
+								if (!array_key_exists($id,$TOTALarray)) { 
 									$STARTarray[$id] = $time;
 									$STOParray[$id] = $time;
 									$TOTALarray[$id] = 1;
@@ -298,17 +331,20 @@ class AmdaStats {
 			foreach ($TOTALarray as $key => $value) {
 				$viStart = $STARTarray[$key];
 				$viStop = $STOParray[$key];
-				$plot = $VIarray['plot'][$key];
-				$mining = $VIarray['mining'][$key];
-				$print = $VIarray['print'][$key];
-				$stat = $VIarray['statistics'][$key];
+				$plot = (empty($VIarray['plot']) || empty($VIarray['plot'][$key])) ? 0 : $VIarray['plot'][$key];
+				$mining = (empty($VIarray['mining']) || empty($VIarray['mining'][$key])) ? 0 : $VIarray['mining'][$key];
+				$print = (empty($VIarray['print']) || empty($VIarray['print'][$key])) ? 0 : $VIarray['print'][$key];
+				$stat = (empty($VIarray['statistics']) || empty($VIarray['statistics'][$key])) ? 0 : $VIarray['statistics'][$key];
+				$ws_print = (empty($VIarray['ws_print']) || empty($VIarray['ws_print'][$key])) ? 0 : $VIarray['ws_print'][$key];
+				$ws_plot = (empty($VIarray['ws_plot']) || empty($VIarray['ws_plot'][$key])) ? 0 : $VIarray['ws_plot'][$key];
 				$uniqueUsers = count($usersArray[$key]);
 				
 				if ($key != 'undefined') 
 				{
 					$GENERALarray[] = array('id' => $key, 'number' => $value, 'percent' => $value,
 												'plot' => $plot, 'mining' => $mining, 
-												'print' => $print,'statistics' => $stat, 
+												'print' => $print,'statistics' => $stat,
+												'ws_print' => $ws_print, 'ws_plot' => $ws_plot,
 												'start' => $viStart, 'stop' => $viStop, 'unique' => $uniqueUsers);
 												
 					$Ntotal += $value;							
@@ -325,8 +361,8 @@ class AmdaStats {
  
 		$Nmax = count($GENERALarray);
  
-		$length = $index + 20 > $Nmax ? $Nmax - $index + 1 : 20;
-		$objToReturn = array('stats' => array_reverse(array_slice($GENERALarray, $index, $length)));
+		$length = $options['index'] + 20 > $Nmax ? $Nmax - $options['index'] + 1 : 20;
+		$objToReturn = array('stats' => array_reverse(array_slice($GENERALarray, $options['index'], $length)));
 		
 		file_put_contents(DATAPATH.'Statistics/data.json',json_encode($GENERALarray)); 
 		
@@ -355,32 +391,29 @@ class AmdaStats {
 		return array(min($date), max($date));
 	}
 
-	public function mergeStats($inXml) {
+	private function _mergeStats($dom, $options) {
 
-		if (!file_exists(StatsXml)) return 0;
-		if (!file_exists($inXml)) return 0;
+		if (!file_exists($options['inXml'])) return 0;
 
 		$tags = array_merge($this->tasks,$this->tasksAdd, $this->tasksWs);
 
-		$doc1 = new DomDocument("1.0");
 		$doc2 = new DomDocument("1.0");
 
-		if (!$doc1->load(StatsXml)) return 0;
-		if (!$doc2->load($inXml)) return 0;
+		if (!$doc2->load($options['inXml'])) return 0;
 
 		foreach ($tags as $tag){
-			$tag1 = $doc1->getElementsByTagName($tag)->item(0);
+			$tag1 = $dom->getElementsByTagName($tag)->item(0);
 			$tag2 = $doc2->getElementsByTagName($tag)->item(0);
 			$items2 = $tag2->getElementsByTagName("item");
 			if ($items2->length > 0) {
 				foreach ($items2 as $item2) {
-					$item1 = $doc1->importNode($item2, true);
+					$item1 = $dom->importNode($item2, true);
 					$tag1->appendChild($item1);
 				}
 			}
 		}
 
-		return $doc1->save(StatsXml);
+		return $dom->save($this->statsFilePath);
 	} 
 }
 ?>
--
libgit2 0.21.2