update import.php in misc/testing folder. Remove scripts and classes not used in newznab-tmux.

This commit is contained in:
Darko
2015-09-16 12:37:58 +02:00
parent 5c1224309e
commit 2b23fd4194
9 changed files with 2 additions and 3905 deletions
-494
View File
@@ -1,494 +0,0 @@
<?php
//
// This script is allows you to perform post blacklist scanning
// Read about the flags below before using this script.
//
// The intent of this script is to try and eliminate garbage
// entries.
//
// by l2g
//
require_once dirname(__FILE__) . '/../../../www/config.php';
use newznab\db\Settings;
# If satisfied with what is matched, set this to true and have
# matched content removed.
# This is a DANGEROUS switch to change. Be sure your absolutely
# satisfied with the regular expressions being matched are bad.
# You'd ideally want to change this flag back to false when you
# resume testing.
$purgeMatched=false;
# When this flag is set to true, the database blacklist is applied
# to your existing nn db. If you set this to false, then the
# blacklist array below is applied instead
$use_NNRegexDB=false;
# By default it looks at all the headers in the database, but sometimes
# if your database is just too big and you want to expermeint with different
# regular expressions, you can set this flag to false, and instead start
# playing with the $res table below labled as Test DB data
$use_NNReleasesDB=true;
# Categorized content can be futher subjected to size restrictions 'IF'
# your database is configured for such.
# This allows you to filter out crap that makes no sense being
# idenified as the category it is based on the category sizing
# Note: This is expermental at this point and requires an update to the DB to
# work correctly.
#
# ALTER TABLE `category` ADD `minsize` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `status` ,
# ADD `maxsize` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `minsize` ;
#
# ;; Update tvshows.SD to set a min value of 30MB (in bytes)
# UPDATE `newznab`.`category` SET `minsize` = '31457280' WHERE `category`.`ID` =5030 LIMIT 1 ;
# ;; Update movies.SD to set a min value of 50MB (in bytes)
# UPDATE `newznab`.`category` SET `minsize` = '52428800' WHERE `category`.`ID` =2030 LIMIT 1 ;
#
# This also requires a few functions in the lib/category.php
# Better just wait before enabling this :)
$use_CategorySizeRestrictions=true;
# This flag is only used if $use_CategorySizeRestrictions (above) is set to true.
# setting this value to false, will have all category restrictions defined in the
# static table below take effect... otherwise, the script will query the database (it's assumed
# that all the proposed changes were accepted and pushed into the DB). Leave this value to
# false until it is certain the database changes have been pushed. When these chagnes are pushed
# this flag will be merged with the above.
$use_NNCategorySizeRestrictions=false;
# Completion Restriction allows you to remove content that is older then
# X hrs and is less then 'Y%' complete.
# Why keep content in the database that can't be put back together
$use_CompletionRestrictions=true;
# The precentage *must be less then (or equal to) 100* to accept 'up to'
$completionAllowable=90;
# The number of hours the release has aged and still hasn't met the allowable
# completion percentage (in seconds) 10800 is equal to 3 hrs
$completionSec=10800;
# check or the article is considered bogus. This greatly increasese
# the processing time against the database.
# Be warned that if your cataloging music, almost every band out there
# spells their name with something that does not coincide with a dictionary
# value.
$use_spellCheck=false;
# Identify the number of dictionaries you want to check the spelling
# against. International spell checking might include 'ge', '
$spellCheckLang=array('en');
# International (uncomment the below)
#$spellCheckLang=array('en', 'fr', 'it', 'ge', 'es', 'nl');
# Exceptions are bad spellings that exist in a filename but still
# mark it valid; This list is not case sensitive, so add any
# keywords that should not be considered while searching
$spellExceptions=array(
# Some good tokens
'xvid', 'DVD', 'DVDRip', 'Bluray', 'x264', '480p', '720p', '1080p',
'HDRIP',
# TV Episode Tags
'S[0-9]{1,2}([. -]*E[0-9]{1,2}$|$)',
# Tv Series release groups
"BARC0DE", "DIMENSION", "LOL", "WEB-DL", "SUBLiME", "2HD", "FMQ",
"BIA",
# MP3/FLAC Groups
"FiH", "SDR",
# Software Groups
"LAXiTY",
);
# Some default category restrictions to play with for those who haven't
# updated their database yet and would still like size restrictions
# in place.
$categoryRestrictions=array(
# TV Standard Def (SD) set a 30MB Min restriction
Category::CAT_TV_SD=>array('min'=>31457280, 'max'=>PHP_INT_MAX),
# TV High Def (HD) set a 40MB Min restriction
Category::CAT_TV_HD=>array('min'=>41943040, 'max'=>PHP_INT_MAX),
# Movies Standard Def (SD) set a 50MB Min restriction
Category::CAT_MOVIE_SD=>array('min'=>52428800, 'max'=>PHP_INT_MAX),
# Movies High Def (HD) set a 80MB Min restriction
Category::CAT_MOVIE_HD=>array('min'=>83886080, 'max'=>PHP_INT_MAX),
# Movies Other set a 30MB Min restriction
Category::CAT_MOVIE_OTHER=>array('min'=>31457280, 'max'=>PHP_INT_MAX),
# Bluray movies, 350MB Min restriction
Category::CAT_MOVIE_BLURAY=>array('min'=>367001600, 'max'=>PHP_INT_MAX),
);
# Throttle cleanup/searching by performing batches... the larger
# the number, the faster processing will go, but will demand more
# system memory...
$batch=500;
# Throttle
# The throttle allows a scan of your database without impacting performance
# to the public who may or may not be using your database. it also reduces
# the cpu demand of the cleanup. Set this value to 0 if your system is
# private and dedicated to yourself for the best speed. It identifies
# the number of seconds to sleep for between each batch
$throttle=0.5;
# This blacklist is an aray of regular expressions that you can use
# to test with (sometimes it's easier to update a table infront of
# you as a developer then going back and making a database change
# and then returning to run the script.
$blacklist = array(
# random string of crap
array("groupname"=>"alt.binaries.*", "regex"=>'^[a-z0-9]{1,80}([0-9-]+$|$)'),
# Missing blacklist catching of foreign content
array("groupname"=>"alt.binaries.*", "regex"=>'[-.](FR|DE|ITA)[-.]'),
# Common German Keywords
array("groupname"=>"alt.binaries.*", "regex"=>'(^|[.\/ \-]+)(ohne|das|der|und|fuer|ersten|leicht|meinem|zum|aus|dem|blitzlicht|alle|grosse|blitzen|ich|zed|sed)([.\/ \-]+|$)'),
# random string of crap in alt.binaries.sounds.mp3, and alt.binaries.multimedia
array("groupname"=>"alt.binaries.*", "regex"=>'^[a-z0-9]{5} [0-9]{8} [0-9]{3}$'),
# random crap like: acef826f-6c8b-405c-b7f2-c9c143d726a8
array("groupname"=>"alt.binaries.*", "regex"=>'^[a-z0-9]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}$'),
array("groupname"=>"alt.binaries.*", "regex"=>'^[a-z0-9.]+\.BDR$'),
array("groupname"=>"alt.binaries.*", "regex"=>'^\([0-9.]+\)$'),
# Password protected content that was marked by the poster
array("groupname"=>"alt.binaries.*", "regex"=>'\[password\]'),
);
# Test DB Data; if you plan on using this, you need to set the $use_NNReleasesDB to false
$test_headers = array(
# Set id's to 0 so id's are purged unessisarily when testing
array('ID'=>0, 'groupname'=>"alt.binaries.multimedia", 'name'=>"1204odayrtgh6j7app"),
array('ID'=>0, 'groupname'=>"alt.binaries.multimedia", 'name'=>"BB555"),
array('ID'=>0, 'groupname'=>"alt.binaries.multimedia", 'name'=>"bnedhe8utrh5tnbg9"),
array('ID'=>0, 'groupname'=>"alt.binaries.multimedia", 'name'=>"Uoaaqunio-396653289-201212201118"),
array('ID'=>0, 'groupname'=>"alt.binaries.multimedia", 'name'=>"acef826f-6c8b-405c-b7f2-c9c143d726a8"),
array('ID'=>0, 'groupname'=>"alt.binaries.multimedia", 'name'=>"ich26389"),
array('ID'=>0, 'groupname'=>"alt.binaries.multimedia", 'name'=>"GkTNPvg3"),
array('ID'=>0, 'groupname'=>"alt.binaries.multimedia", 'name'=>"RVL 25 12 2012 G O M S BDR"),
array('ID'=>0, 'groupname'=>"alt.binaries.multimedia", 'name'=>"25 12 2012 M S R BDR"),
array('ID'=>0, 'groupname'=>"alt.binaries.multimedia", 'name'=>"23YKC 20121212 013"),
);
function handleError($errno, $errstr, $errfile, $errline, array $errcontext)
{
// this function makes it easier to catch regular expression errors
// found
if (0 === error_reporting()) {
return false;
}
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler('handleError');
function get_block($offset, $batch)
{
global $use_NNReleasesDB;
static $retrieved=false;
if(!$use_NNReleasesDB){
// use a retrieved boolean for test data so content
// is only fetched once.
if(!$retrieved && $offset > 0){
return Null;
}
global $test_headers;
// toggle retrieved content on test data
$retrieved=true;
return $test_headers;
}
$catsql = "SELECT releases.*,groups.name as groupname FROM "
."releases LEFT JOIN groups on releases.groupID = groups.ID ";
$db = new Settings();
return $db->query($catsql.sprintf(' LIMIT %d,%d', $offset, $batch));
}
$db = new Settings();
$errcnt=0;
$total=0;
$offset=0;
# Dictionary
$pspelldict=[];
# Completion Refernece Time in past
$completionRef = strtotime("-$completionSec seconds");
# Scan header for spelling...; at least 1 word has to match the spelling
if($use_spellCheck && !function_exists('pspell_check')){
echo "Note: Disabling spell checking as it is not available in your PHP installation.\n";
echo " Consider the following to enable it:";
echo " ubuntu #> sudo apt-get install php5-pspell\n";
echo " redhat #> yum install php-pspell\n";
echo " other #> # re-compile php with --with-pspell flag\n";
$use_spellCheck=false;
}
$binaries = new Binaries();
if($use_NNRegexDB)
# Over-ride Blacklist up top and use database
$blacklist = $binaries->getBlacklist(true);
while($res=get_block($offset, $batch)){
$subtotal=count($res);
$total+=$subtotal;
$release = new Releases();
$category = new Category();
// Track error count changes
$_errcnt=$errcnt;
foreach ($res as $header)
{
// if a white list is detected we now are required to
// only accept the entry if it matches at least 1 whitelist
// while a blacklist will over-ride all
$whitelist = [];
$matches_whitelist = false;
foreach ($blacklist as $bl)
{
// opttype = 1 -> Blacklist (Default)
// opttype = 2 -> Whitelist
$opttype = Binaries::OPTYPE_BLACKLIST;
if (array_key_exists ('opttype' , $bl)){
$opttype = intval($bl['opttype']);
}
// Regular expression
$regex = $bl['regex'];
// Default msgcol is subject
$msgcol = Binaries::BLACKLIST_FIELD_SUBJECT;
$parse = $header['name'];
if (array_key_exists ('msgcol' , $bl)){
$msgcol = intval($bl['msgcol']);
}
switch($msgcol)
{
case Binaries::BLACKLIST_FIELD_FROM:
if(!array_key_exists ('fromname' , $bl)){
# we only process fromname if we found it... otherwise we keep going
continue;
}
$parse = $header['fromname'];
break;
case Binaries::BLACKLIST_FIELD_MESSAGEID:
# unsupported; you can't get this information from an already
# populated database
continue;
};
if ($opttype == Binaries::OPTYPE_WHITELIST){
// at least 1 whitelist found...
// this means we have to match it or be rejected
$whitelist[] = $regex;
}
try
{
$group_check = preg_match('/^'.$bl['groupname'].'$/i', $header['groupname']);
}
catch(Exception $e)
{
# Support letting the user know when there is a bad
# regular expression entry abort futher checks
$strerr=str_pad((int) $errcnt,2," ",STR_PAD_LEFT);
echo "$strerr. id=".$bl["id"].
", group=".$header["groupname"]."\n";
echo " regex='".$bl["groupname"]."'\n";
echo " error=".$e->getMessage()."\n\n";
exit(1);
}
if ($group_check)
{
try
{
$blacklist_check = preg_match('/'.$regex.'/i', $parse);
// White lists react in the invert manner... something that
// doesn't match causes us to keep going
if($opttype == Binaries::OPTYPE_WHITELIST && $blacklist_check){
// invert check since it's a good thing we matched
$blacklist_check=false;
// Flag that we matched the white list
$matches_whitelist = true;
}
}
catch(Exception $e)
{
# Support letting the user know when there is a bad
# regular expression entry abort futher checks
$strerr=str_pad((int) $errcnt,2," ",STR_PAD_LEFT);
if(isset($bl["id"]))
echo "$strerr. id=".$bl["id"].", ";
echo " name=".$header["name"]."\n";
echo " regex=$regex\n";
echo " error=".$e->getMessage()."\n\n";
exit(1);
}
if ($blacklist_check)
{
$errcnt++;
echo 'BL: /'.$bl['regex'].'/i matched '.$header['ID'].'/'.$header['name'].
" (".$header['groupname'].")\n";
if ($purgeMatched && isset($header['ID']) && $header['ID'] > 0){
// Support purging if enabled
$release->delete($header['ID']);
}
# ensure whitelist flags are off to avoid extra processing
# at the end of this loop (Below)
$whitelist = [];
break;
}
if($use_CategorySizeRestrictions == true && $header['categoryID'])
{
$sizes = null;
if($use_NNCategorySizeRestrictions){
$sizes = $category->getSizeRangeById($header['categoryID']);
}else if (array_key_exists ($header['categoryID'], $categoryRestrictions)){
$sizes = $categoryRestrictions[$header['categoryID']];
}
if(is_array($sizes)){
# Restrictions are in place
$totalSize=intval($header['size']);
if(!($totalSize >= $sizes['min'] && $totalSize < $sizes['max']))
{
$errcnt++;
echo 'BL: size restrictions '.$header['ID'].'/'.$header['name'].
" (".$header['groupname'].")\n";
if ($purgeMatched && isset($header['ID']) && $header['ID'] > 0){
// Support purging if enabled
$release->delete($header['ID']);
}
# ensure whitelist flags are off to avoid extra processing
# at the end of this loop (Below)
$whitelist = [];
break;
}
}
}
if($use_CompletionRestrictions == true){
$lastUpdate=strtotime($header['updatedate']);
$completion=intval($header['completion']);
if($completion < $completionAllowable && $lastUpdate < $completionRef)
{
$errcnt++;
echo 'BL: incomplete '.$header['ID'].'/'.$header['name'].
" (".$header['groupname'].")\n";
if ($purgeMatched && isset($header['ID']) && $header['ID'] > 0){
// Support purging if enabled
$release->delete($header['ID']);
}
# ensure whitelist flags are off to avoid extra processing
# at the end of this loop (Below)
$whitelist = [];
break;
}
}
if($use_spellCheck == true &&
$opttype == Binaries::OPTYPE_BLACKLIST &&
$msgcol == Binaries::BLACKLIST_FIELD_SUBJECT){
# Track matched
$matched=0;
# First extract the words
$keywords = preg_split('/[^a-z0-9]+/i', $parse);
foreach($spellCheckLang as $lang){
if (!array_key_exists ($lang , $pspelldict))
$pspelldict[$lang] = pspell_new($lang);
foreach($keywords as $kw){
foreach($spellExceptions as $exception){
if(preg_match('/'.$exception.'/i', $kw)){
$matched++;
break;
}
}
if($matched>0)break;
if(pspell_check($pspelldict[$lang], $kw)){
$matched++;
break;
}
}
# we only need to match once to presume the data
# is good
if($matched>0)break;
}
# how did we do for spelling matches
if(!$matched){
$errcnt++;
echo 'BL: due to bad spelling '.$header['ID'].'/'.$header['name'].
" (".$header['groupname'].")\n";
if ($purgeMatched && isset($header['ID']) && $header['ID'] > 0){
// Support purging if enabled
$release->delete($header['ID']);
}
# ensure whitelist flags are off to avoid extra processing
# at the end of this loop (Below)
$whitelist = [];
break;
}
}
}
}
# We parsed entire matching list entries at this point.. now we need
# to handle the whitelist (if it was enabled)
if(count($whitelist) > 0 && !$matches_whitelist)
{
$errcnt++;
$first_loop=true;
foreach ($whitelist as $entry){
if($first_loop)
{
$first_loop=false;
}
else
{
echo '\n';
}
echo 'WL: /'.$bl['regex'].'/i';
}
echo ' failed to match '.$header['ID'].'/'.$header['name'].
" (".$header['groupname'].")\n";
if ($purgeMatched && $header['ID'] > 0){
// Support purging if enabled
$release->delete($header['ID']);
}
}
}
if(!$purgeMatched || $_errcnt==$errcnt){
# No changes... update batch
$offset+=$batch;
}
if($subtotal == $batch){
// Presume there is more content to come
// throttle request
if($throttle > 0.0){
sleep($throttle);
}
}
}
echo "Scanned $total record(s), $errcnt match(es) found.\n";
if($errcnt >0){
if(!$purgeMatched){
echo "Note: This scan was performed in a safe read-only mode.\n";
echo " If you are satisfied with the output as being 'garbage/spam'\n".
" then consider editing this file and setting the \$purgeMatched flag to 'true'\n";
}else{
echo "Note: This scan was performed in a write mode; all content matched was removed.\n";
}
}
exit(($errcnt>0)?1:0);
-75
View File
@@ -1,75 +0,0 @@
<?php
//This script allows you to delete properly all releases which match some criteria
//The nzb, covers and all linked records will be deleted properly.
require_once dirname(__FILE__) . '/../../www/config.php';
use newznab\db\Settings;
$releases = new Releases();
$db = new Settings();
//
// [1] delete all releases for a group which only has x number of files
//
//$sql = "select * from releases where totalpart = 1 and groupID in (select ID from groups where name = 'alt.binaries.cd.image')";
//
// [2] delete all releases where the only file inside the rars is *.exe and they are not in the PC category
//
/*
$sql = "
select releasefiles.releaseID as ID
from releasefiles
inner join
( select releaseID, count(*) as totnum from releasefiles group by releaseID ) x on x.releaseID = releasefiles.releaseID and x.totnum = 1
inner join releases on releases.ID = releasefiles.releaseID
where releasefiles.name like '%.exe'
and releases.categoryID not in (4000,4010,4020,4030,4040,4050)
group by releasefiles.releaseID ";
*/
//
// [3] delete all releases which contain a file with password.url in its
//
//$sql = "select distinct releasefiles.releaseID from releasefiles where name = 'password.url'";
//
// [4] delete all releases for a poster
//
//$sql = "select ID from releases where fromname = 'PowerBUS@gmail.com (PowerBUS)'";
//
// [5] delete all under a certain amount of completion
//
//$sql = "select ID from releases where completion != 0 and completion != 100 and completion < 99";
//
// [6] all audio which contains a file with .exe or install.bin in
//
//$sql = "select distinct r.ID from releasefiles rf inner join releases r on r.id = rf.releaseID and r.categoryID like '3%' where rf.name like '%.exe' or rf.name = 'install.bin'";
//
// [7] delete all releases for a name
//
//$sql = "select ID from releases where searchname like '%Friday The 13th The Series S03 -enjoy-%'";
//
// [8] delete all releases for a regex
//
//$sql = "select ID from releases where regexID = 1307";
//
// [9] delete all releases under a certain size (100MB) for a category
//
//$sql = "select ID from releases where categoryID like '2%' and size < 104857600";
$rel = $db->query($sql);
echo "about to delete ".count($rel)." release(s)";
foreach ($rel as $r)
{
$releases->delete($r['ID']);
}
-10
View File
@@ -1,10 +0,0 @@
<?php
require_once dirname(__FILE__) . '/../../www/config.php';
$echoonly = false;
$limitedtotoday = true;
$verbose = true;
$p = new Parsing($echoonly, $limitedtotoday, $verbose);
$p->removeSpecial();
-9
View File
@@ -1,9 +0,0 @@
<?php
require_once dirname(__FILE__) . '/../../www/config.php';
$echoonly = true;
$limittotoday = false;
$verbose = true;
$p = new Parsing($echoonly, $limittotoday, $verbose);
$p->cleanup();
-9
View File
@@ -1,9 +0,0 @@
<?php
require_once dirname(__FILE__) . '/../../www/config.php';
$echoonly = false;
$limittotoday = false;
$verbose = true;
$p = new Parsing($echoonly, $limittotoday, $verbose);
$p->process();
+2 -1
View File
@@ -6,6 +6,7 @@ use newznab\db\Settings;
use newznab\utility\Utility;
$releases = new Releases();
$releaseImage = new ReleaseImage();
$db = new Settings();
$cat = new Categorize();
$releaseRegex = new ReleaseRegex();
@@ -116,7 +117,7 @@ foreach ($filestoprocess as $nzbFile) {
if (!file_exists($errorpath)) mkdir($errorpath);
if (!file_exists($errorpath . basename($nzbFile))) rename($nzbFile, $errorpath . basename($nzbFile));
}
$releases->delete($relid);
$releases->deleteSingle(['g' => $relguid, 'i' => $relid], $nzb, $releaseImage);
}
} else {
$numbins = 0;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-808
View File
@@ -1,808 +0,0 @@
<?php
declare(ticks = 1);
/**
* PowerProcess is an abstraction class for PHP's posix and pcntl extensions.
*
* It enables easy process forking or threading to allow use of parallel
* processes for completing complicated tasks that would otherwise be
* inefficient for normal serial and procedural processing
*
* If you like PowerProcess, please consider donating
* - BTC: 1K2tvdYzdDDd8w6vNHQgvbNQnhcHqLEadx
* - LTC: LfceD3QH2n1FqH8inqHdKxjBFV55QvuESv
*
* @package PowerProcess
*
* @author Don Bauer <lordgnu@me.com>
* @link https://github.com/lordgnu/PowerProcess
* @license MIT License
* @version 2.0
*
* @copyright
* Copyright (c) 2011 Don Bauer <lordgnu@me.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
class PowerProcess {
const CALLBACK_STOP_PROPOGATION = -1;
const CALLBACK_IGNORE = 0;
const CALLBACK_CONTINUE = 1;
const CALLBACK_SHUTDOWN = 2;
const CALLBACK_RESTART = 3;
/**
* Current PowerProcess version
*
* @var string
*/
public static $version = '2.1';
/**
* Data store for data that is to be passed to the child process which is to be spawned
*
* @var mixed
*/
public $threadData;
/**
* Boolean variable which determines whether or not to shutdown the control process (parent)
*
* @var boolean
*/
public $complete;
/**
* Callback array for setting callback functions based on signals that can be sent to the parent process
*
* @var array
*/
public $callbacks;
/**
* The name of the current thread. Used by WhoAmI()
*
* @var string
*/
public $currentThread;
/**
* Whether to log internal debug message
*
* @var boolean
*/
public $debugLogging;
/**
* The maximum number of concurrent threads that can be running at any given time.
*
* This setting has an impact on performance for PowerProcess so play
* with it on the system you are on to determine a good value.
* 10 is a good place to start
*
* @var integer
*/
public $maxThreads;
/**
* Array which stores the thread data for the control process (parent) to manage running child threads
*
* @var array
*/
public $myThreads;
/**
* Session id of parent session when process is daemonized
*
* @var integer
*/
public $parentSID;
/**
* The pid of the parent process
*
* Used after a process is forked to
* determine whether the new thread is to run the thread code
*
* @var integer
*/
public $parentPID;
/**
* Sleep timer in micro seconds for the parent process to sleep between status checks using Tick()
*
* @var integer
*/
public $tickCount = 100;
/**
* Whether to add a timestamp to log output
*
* @var boolean
*/
public $timeStampLogs = true;
/**
* The maximum number of seconds a thread will be allowed to run.
*
* Set to 0 to disable a time limit (use with caution)
*
* @var integer
*/
public $threadTimeLimit;
/**
* Location to log information messages to.
*
* Can be a file or php://stdout, php://stderr.
* Set to false to disable
*
* @var mixed
*/
public $logTo;
/**
* When logging is enabled, this points to the socket in which to write log messages.
*
* @var resource
*/
public $logSocket;
/**
* Signals to install for SignalDispatcher.
*
* You can use any signal constant PNCTL supports
* @link http://us3.php.net/manual/en/pcntl.constants.php
*
* @var array
*/
public $signalArray = array(
SIGUSR1, // User-Defined 1
SIGUSR2 // User-Defined 2
);
static public function SignalName($signal) {
switch ($signal) {
case SIGHUP:
return 'SIGHUP';
case SIGINT:
return 'SIGINT';
case SIGQUIT:
return 'SIGQUIT';
case SIGILL:
return 'SIGILL';
case SIGTRAP:
return 'SIGTRAP';
case SIGABRT:
return 'SIGABRT';
case SIGIOT:
return 'SIGIOT';
case SIGBUS:
return 'SIGBUS';
case SIGFPE:
return 'SIGFPE';
case SIGUSR1:
return 'SIGUSR1';
case SIGSEGV:
return 'SIGSEGV';
case SIGUSR2:
return 'SIGUSR2';
case SIGPIPE:
return 'SIGPIPE';
case SIGALRM:
return 'SIGALRM';
case SIGTERM:
return 'SIGTERM';
case SIGSTKFLT:
return 'SIGSTKFLT';
case SIGCLD:
return 'SIGCLD';
case SIGCHLD:
return 'SIGCHLD';
case SIGCONT:
return 'SIGCONT';
case SIGTSTP:
return 'SIGTSTP';
case SIGTTIN:
return 'SIGTTIN';
case SIGTTOU:
return 'SIGTTOU';
case SIGURG:
return 'SIGURG';
case SIGXCPU:
return 'SIGXCPU';
case SIGXFSZ:
return 'SIGXFSZ';
case SIGVTALRM:
return 'SIGVTALRM';
case SIGPROF:
return 'SIGPROF';
case SIGWINCH:
return 'SIGWINCH';
case SIGPOLL:
return 'SIGPOLL';
case SIGIO:
return 'SIGIO';
case SIGPWR:
return 'SIGPWR';
case SIGSYS:
return 'SIGSYS';
case SIGBABY:
return 'SIGBABY';
case SIG_BLOCK:
return 'SIG_BLOCK';
case SIG_UNBLOCK:
return 'SIG_UNBLOCK';
case SIG_SETMASK:
return 'SIG_SETMASK';
default:
return "Signal # {$signal}";
}
}
/**
* PowerProcess constructor.
*
* Returns an instanced PowerProcess object or dies on failure
*
* @param integer $maxThreads Max number of concurrent threads to allow at any given time
* @param integer $threadTimeLimit Maximum number of seconds a thread is allowed to live
* @param boolean $daemon Whether to start as a deamon or just a normal script
* @param string $logTo What stream to log output to
* @param boolean $debugLogging Whether to enable debug logging
*
* @return object Instanced PowerProcess object
*/
public function __construct($maxThreads = 10, $threadTimeLimit = 300, $daemon = false, $logTo = false, $debugLogging = false) {
if (function_exists('pcntl_fork') && function_exists('posix_getpid')) {
// Set the current thread name
$this->currentThread = 'CONTROL';
// Set the max threads setting
$this->SetMaxThreads($maxThreads);
// Set the thread time limit setting
$this->SetThreadTimeLimit($threadTimeLimit);
// Init the logger
$this->InitializeLogger($logTo, $debugLogging);
if ($daemon) {
// Attempt to daemonize
if (!$this->Daemonize()) {
die("Could not daemonize");
} else {
$this->Log("Daemonized successfully",true);
}
} else {
// Register control process PID
$this->parentPID = $this->GetPID();
$this->parentSID = false;
$this->Log("Parent PID detected as {$this->parentPID}",true);
}
// The the complete flag to false
$this->complete = false;
// Install the signal handler
$this->InstallSignalHandler();
// Init the Thread Queue
$this->myThreads = [];
// Log completion of startup
$this->Log("Startup process complete",true);
} else {
die("PowerProcess requires both the POSIX and PCNTL extensions to operate.\n");
}
}
/**
* Frees up memory
*/
public function __destruct() {
unset($this->callbacks);
unset($this->myThreads);
// Handle any remaining signals
pcntl_signal_dispatch();
$this->RemoveLogger();
}
/**
* Executes specified program in the current process space
*
* @param string $process Path to the binary process to execute
* @param array $args Array of argument strings to pass to the program
*/
public function Exec($process, $args = null) {
if ($args == null) {
pcntl_exec($process);
} else {
pcntl_exec($process, $args);
}
}
/**
* Returns the PID of the current process
*
* @return integer
*/
public function GetPID() {
return posix_getpid();
}
/**
* Returns the PID of the process that spawned this one
*
* @return integer
*/
public function GetControlPID() {
return posix_getppid();
}
/**
* Get the status of a running thread by name or PID
*
* @param string|integer $name The name or PID of the process for which you want status information
* @return array|boolean
*/
public function GetThreadStatus($name = false) {
if ($name === false) return false;
if (isset($this->myThreads[$name])) {
return $this->myThreads[$name];
} else {
return false;
}
}
/**
* Determine whether the control process is daemonized
*
* @return boolean
*/
public function IsDaemon() {
return $this->parentSID !== false;
}
/**
* Log a message
*
* @param string $msg The message to log
* @param boolean $internal Whether this is an internal debug logging message
*/
public function Log($msg, $internal = false) {
if ($this->logSocket !== false) {
if (!$internal || $this->debugLogging) {
if ($this->timeStampLogs) {
fwrite($this->logSocket, sprintf("[%s][%-22s] %s\n", date("Y-m-d H:i:s"), $this->WhoAmI(), $msg));
} else {
fwrite($this->logSocket, sprintf("[%-22s] %s\n", $this->WhoAmI(), $msg));
}
}
}
}
/**
* Restarts the control process
*/
public function Restart() {
// Build Path of Script
if (isset($_SERVER['_'])) {
$cmd = $_SERVER['_'];
$this->Log("Attempting to restart using {$cmd}",true);
} else {
$this->Log("Can not restart - Shutting down", true);
return $this->Shutdown();
}
// Wait for threads to complete
while ($this->ThreadCount()) {
$this->CheckThreads();
$this->Tick();
}
// Remove the first arg if this is a stand-alone
if ($cmd == $_SERVER['argv'][0]) unset($_SERVER['argv'][0]);
// Execute Restart
$this->Exec($cmd, $_SERVER['argv']);
$this->Shutdown(true);
return self::CALLBACK_IGNORE;
}
/**
* Registers a callback function for the signal dispatcher or for special signals used by PowerProcess
*
* Special signals are:
* - 'shutdown' : Triggered on completion of the Shutdown() method
* - 'threadotl' : Triggered on killing a thread due to exceeding time limit
*
* @param int|string $signal The signal to register a callback for
* @param callback $callback The callback function
*/
public function RegisterCallback($signal, $callback = false) {
if ($callback !== false) $this->callbacks[$signal][] = $callback;
// Register with PCNTL
if (is_int($signal)) {
$this->Log("Registering signal {$signal} with dispatcher",true);
pcntl_signal($signal, array(&$this, 'SignalDispatch'));
// Unblock the Signal
pcntl_sigprocmask(SIG_UNBLOCK,array($signal));
}
}
/**
* Determines whether we should be running the control code or the thread code
*
* @return boolean
*/
public function RunControlCode() {
$this->Tick();
if (!$this->complete) {
return $this->ControlCheck();
} else {
$this->SignalDispatch('shutdown');
return false;
}
}
/**
* Determines whether we should be running the child code
*
* @return boolean
*/
public function RunThreadCode() {
return !$this->ControlCheck();
}
/**
* Send a signal to a process
*
* @param integer $pid
* @param integer $signal
*/
public function SendSignal($pid = 0, $signal = 0) {
if ($signal > 0 && $pid > 0) {
return posix_kill($pid, $signal) && pcntl_signal_dispatch();
} else {
return false;
}
}
/**
* Set the max number of threads that can be running concurrently
*
* @param integer $maxThreads The max number of threads to run concurrently
*/
public function SetMaxThreads($maxThreads = 10) {
$this->maxThreads = $maxThreads;
}
/**
* Set the max number of seconds a thread can run before being terminated
*
* @param integer $threadTimeLimit The max number of seconds a thread can run
*/
public function SetThreadTimeLimit($threadTimeLimit = 300) {
$this->threadTimeLimit = $threadTimeLimit;
}
/**
* Initiates the shutdown procedure for PowerProcess
*
* @param boolean $exit When set to true, Shutdown causes the script to exit
*/
public function Shutdown($exit = false) {
$this->Log("Initiating shutdown",true);
while ($this->ThreadCount()) {
$this->CheckThreads();
$this->Tick();
}
$this->complete = true;
// Send custom shutdown signal
$this->SignalDispatch('shutdown');
$this->Log("Shutdown Complete");
if ($exit) exit;
return self::CALLBACK_IGNORE;
}
/**
* Determines if a new process can be spawned
*
* @return boolean
*/
public function SpawnReady() {
$this->Tick();
return ($this->ThreadCount() < $this->maxThreads);
}
/**
* Spawn a new thread
*
* @param string $name The name of the thread to be spawned
* @param boolean $returnPid Whether to return the pid instead of boolean
*
* @return boolean|integer
*/
public function SpawnThread($name = false, $returnPid = false) {
// Check to make sure we can spawn another thread
if (!$this->SpawnReady()) {
$this->Log("The maximum number of threads are already running",true);
$this->Tick();
return false;
}
if ($name !== false) {
// Check to make sure there is not already a named thread with this name
if ($this->GetThreadStatus($name) !== false) {
$this->Log("There is already a thread named '{$name}' running",true);
$this->Tick();
return false;
}
}
$pid = pcntl_fork();
if ($pid) {
// We are the control thread so log the child in a queue
$index = ($name === false) ? $pid : $name;
$name = ($name === false) ? "THREAD:{$pid}" : $name;
$this->myThreads[$index] = array(
'pid' => $pid,
'time' => time(),
'name' => $name
);
$this->Log("Spawned thread: {$name}",true);
$this->Tick();
return ($returnPid) ? $pid : true;
} else {
// We are the child thread so change the current thread var
$this->currentThread = ($name === false) ? "THREAD:".$this->GetPID() : $name;
return ($returnPid) ? $pid : true;
}
}
/**
* Get the count of running threads
*
* @return integer
*/
public function ThreadCount() {
return count($this->myThreads);
}
/**
* Process signals to be dispatched and sleep for a number of microseconds
*/
public function Tick() {
// Dispatch Pending Signals
pcntl_signal_dispatch();
// Check Running Threads
if ($this->parentPID == $this->GetPID()) $this->CheckThreads();
// Tick
usleep($this->tickCount);
}
/**
* Get the name of the current thread
*
* @return string The name of the current thread
*/
public function WhoAmI() {
return $this->currentThread;
}
// All Private Functions Below Here
/**
* Checks all running threads to make sure they are still running and their time limit has not been exceeded
*
* If a thread has exceeded it's time limit, this method will kill that process
* and dispatch the special signal 'threadotl'
*/
private function CheckThreads() {
foreach ($this->myThreads as $i => $thread) {
// Check to make sure the process is still running
if ($this->PIDDead($thread['pid']) != 0) {
// Thread is Dead
unset($this->myThreads[$i]);
} elseif ($this->threadTimeLimit > 0) {
if (time() - $thread['time'] > $this->threadTimeLimit) {
$this->KillThread($thread['pid']);
$this->Log("Thread {$thread['name']} has exceeded the thread time limit",true);
$this->SignalDispatch('threadotl');
unset($this->myThreads[$i]);
}
}
}
}
/**
* Check if the current process is the control process
*
* @return boolean
*/
private function ControlCheck() {
return $this->parentPID == $this->GetPID();
}
/**
* Attempts to daemonize the current process
*
* @return integer
*/
private function Daemonize() {
$this->Log("Attempting to Daemonize",true);
// First need to fork
$pid = pcntl_fork();
// Tick to catch signals
$this->Tick();
if ($pid < 0) exit; // Error
if ($pid) exit; // Parent
$this->parentSID = posix_setsid();
// Need to reset the parent PID
$this->parentPID = $this->GetPID();
$this->Log("Parent PID {$this->parentPID}",true);
$this->Log("Parent SID {$this->parentSID}",true);
return ($this->parentSID > 0);
}
/**
* Initialize the logging stream if enabled
*
* @param string|boolean $logTo The path or stream to log to or false to disable
*/
private function InitializeLogger($logTo, $debugLogging) {
if ($logTo !== false) {
$this->logSocket = @fopen($logTo, 'w');
$this->debugLogging = $debugLogging;
} else {
$this->logSocket = false;
$this->debugLogging = false;
}
}
/**
* Installs the default signal handlers
*/
private function InstallSignalHandler() {
// Register the callback for thread completion
$this->RegisterCallback(SIGCHLD, array($this,'CheckThreads'));
$this->Log("SIGCHLD callback registered",true);
// Register the callback for restart requests
$this->RegisterCallback(SIGHUP, array($this, 'Restart'));
$this->Log("SIGHUP callback registered",true);
// Register the callback for shutdown requests
$this->RegisterCallback(SIGTERM, array($this, 'Shutdown'));
$this->Log("SIGTERM callback registered",true);
// Install the signal handler
foreach ($this->signalArray as $signal) $this->RegisterCallback($signal);
$this->Log("Signal Dispatcher installed",true);
}
/**
* Kill a thread by PID
*
* @param integer $pid The PID of the thread to kill
*/
private function KillThread($pid = 0) {
$this->SendSignal($pid, SIGTERM);
}
/**
* Determine whether a child pid has exited
*
* Returns the PID of child which exited or 0
*
* @param integer $pid The PID to check
* @return integer
*/
private function PIDDead($pid = 0) {
if ($pid > 0) {
return pcntl_waitpid($pid, $status, WUNTRACED OR WNOHANG);
} else {
return 0;
}
}
/**
* Closes the logging stream
*/
private function RemoveLogger() {
if ($this->logSocket) {
@fclose($this->logSocket);
}
}
/**
* Handles dispatching of signals to user-defined callbacks
*
* @param integer|string $signal
*/
public function SignalDispatch($signal) {
// Log Dispatch
$signalName = self::SignalName($signal);
$this->Log("Received signal: {$signalName}",true);
// Check the callback array for this signal number
if (isset($this->callbacks[$signal])) {
// Execute the callback
$callStack = $this->callbacks[$signal];
// Run last added callbacks first (FILO)
$i = count($callStack);
while ($callback = array_pop($callStack)) {
$this->Log("Running Callback[{$i}] for signal: {$signalName}", true);
--$i;
$status = call_user_func($callback);
// Check if we should continue
if ($status == self::CALLBACK_STOP_PROPOGATION) {
// Break out of loop
$this->Log("Callback[{$i}] for signal '{$signalName}' has stopped propogation of further callbacks", true);
break;
}
// OK, now switch on the status
switch ($status) {
case self::CALLBACK_RESTART:
$this->Restart();
break;
case self::CALLBACK_SHUTDOWN:
$this->Shutdown();
break;
case self::CALLBACK_IGNORE:
case self::CALLBACK_CONTINUE:
// Do nothing :: Continue propogation
break;
default:
$this->Log("Callback[{$i}] for signal '{$signalName}' did not return a valid status");
}
// Continue the loop
}
} else {
// No callback registered
$this->Log("There is no callback registered for signal {$signalName}",true);
}
// Handle SIGTERM for threads
if ($signal == 15) exit(0);
}
}