Programmatically reading messages from systemd's journal - php

Update, 2013-09-12:
I've dug a bit deeper into systemd and it's journal, and, I've stumbled upon this, that states:
systemd-journald will forward all received log messages to the AF_UNIX SOCK_DGRAM socket /run/systemd/journal/syslog, if it exists, which may be used by Unix syslog daemons to process the data further.
As per manpage, I did set up my environment to also have syslog underneath, I've tweaked my code accordingly:
define('NL', "\n\r");
$log = function ()
{
if (func_num_args() >= 1)
{
$message = call_user_func_array('sprintf', func_get_args());
echo '[' . date('r') . '] ' . $message . NL;
}
};
$syslog = '/var/run/systemd/journal/syslog';
$sock = socket_create(AF_UNIX, SOCK_DGRAM, 0);
$connection = socket_connect($sock, $syslog);
if (!$connection)
{
$log('Couldn\'t connect to ' . $syslog);
}
else
{
$log('Connected to ' . $syslog);
$readables = array($sock);
socket_set_nonblock($sock);
while (true)
{
$read = $readables;
$write = $readables;
$except = $readables;
$select = socket_select($read, $write, $except, 0);
$log('Changes: %d.', $select);
$log('-------');
$log('Read: %d.', count($read));
$log('Write: %d.', count($write));
$log('Except: %d.', count($except));
if ($select > 0)
{
if ($read)
{
foreach ($read as $readable)
{
$data = socket_read($readable, 4096, PHP_BINARY_READ);
if ($data === false)
{
$log(socket_last_error() . ': ' . socket_strerror(socket_last_error()));
}
else if (!empty($data))
{
$log($data);
}
else
{
$log('Read empty.');
}
}
}
if ($write)
{
foreach ($write as $writable)
{
$data = socket_read($writable, 4096, PHP_BINARY_READ);
if ($data === false)
{
$log(socket_last_error() . ': ' . socket_strerror(socket_last_error()));
}
else if (!empty($data))
{
$log($data);
}
else
{
$log('Write empty.');
}
}
}
}
}
}
This apparently, only sees (selects) changes on write sockets. Well, might be that something here is wrong so I attempted to read from them, no luck (nor there should be):
[Thu, 12 Sep 2013 14:45:15 +0300] Changes: 1.
[Thu, 12 Sep 2013 14:45:15 +0300] -------
[Thu, 12 Sep 2013 14:45:15 +0300] Read: 0.
[Thu, 12 Sep 2013 14:45:15 +0300] Write: 1.
[Thu, 12 Sep 2013 14:45:15 +0300] Except: 0.
[Thu, 12 Sep 2013 14:45:15 +0300] 11: Resource temporarily unavailable
Now, this drives me nuts a little. syslog documentation says this should be possible. What is wrong with the code?
Original:
I had a working prototype, by simply:
while(true)
{
exec('journalctl -r -n 1 | more', $result, $exit);
// do stuff
}
But this feels wrong, and consumes too much system resources, then I found out about journald having sockets.
I have attempted to connect and read from:
AF_UNIX, SOCK_DGRAM : /var/run/systemd/journal/socket
AF_UNIX, SOCK_STREAM : /var/run/systemd/journal/stdout
the given sockets.
With /var/run/systemd/journal/socket, socket_select sees 0 changes. With /var/run/systemd/journal/stdout I always (every loop) get 1 change, with 0 byte data.
This is my "reader":
<?php
define('NL', "\n\r");
$journal = '/var/run/systemd/journal/socket';
$jSTDOUT = '/var/run/systemd/journal/stdout';
$journal = $jSTDOUT;
$sock = socket_create(AF_UNIX, SOCK_STREAM, 0);
$connection = #socket_connect($sock, $journal);
$log = function ($message)
{
echo '[' . date('r') . '] ' . $message . NL;
};
if (!$connection)
{
$log('Couldn\'t connect to ' . $journal);
}
else
{
$log('Connected to ' . $journal);
$readables = array($sock);
while (true)
{
$read = $readables;
if (socket_select($read, $write = NULL, $except = NULL, 0) < 1)
{
continue;
}
foreach ($read as $read_socket)
{
$data = #socket_read($read_socket, 1024, PHP_BINARY_READ);
if ($data === false)
{
$log('Couldn\'t read.');
socket_shutdown($read_socket, 2);
socket_close($read_socket);
$log('Server terminated.');
break 2;
}
$data = trim($data);
if (!empty($data))
{
$log($data);
}
}
}
$log('Exiting.');
}
Having no data in read socket(s), I assume I'm doing something wrong.
Question, idea:
My goal is to read the messages and upon some of them, execute a callback.
Could anyone point me into the right direction of how to programmatically read journal messages?

The sockets under /run/systemd/journal/ won't work for this – …/socket and …/stdout are actually write-only (i.e. used for feeding data into the journal) while the …/syslog socket is not supposed to be used by anything else than a real syslogd, not to mention journald does not send any metadata over it. (In fact, the …/syslog socket doesn't even exist by default – syslogd must actually listen on it, and journald connects to it.)
The official method is to read directly from the journal files, and use inotify to watch for changes (which is the same thing journalctl --follow and even tail -f /var/log/syslog use in place of polling). In a C program, you can use the functions from libsystemd-journal, which will do the necessary parsing and even filtering for you.
In other languages, you have three choices: call the C library; parse the journal files yourself (the format is documented); or fork journalctl --follow which can be told to output JSON-formatted entries (or the more verbose journal export format). The third option actually works very well, since it only forks a single process for the entire stream; I have written a PHP wrapper for it (see below).
Recent systemd versions (v193) also come with systemd-journal-gatewayd, which is essentially a HTTP-based version of journalctl; that is, you can get a JSON or journal-export stream at http://localhost:19531/entries. (Both gatewayd and journalctl even support server-sent events for accessing the stream from HTML 5 webpages.) However, due to obvious security issues, gatewayd is disabled by default.
Attachment: PHP wrapper for journalctl --follow
<?php
/* © 2013 Mantas Mikulėnas <grawity#gmail.com>
* Released under the MIT Expat License <https://opensource.org/licenses/MIT>
*/
/* Iterator extends Traversable {
void rewind()
boolean valid()
void next()
mixed current()
scalar key()
}
calls: rewind, valid==true, current, key
next, valid==true, current, key
next, valid==false
*/
class Journal implements Iterator {
private $filter;
private $startpos;
private $proc;
private $stdout;
private $entry;
static function _join_argv($argv) {
return implode(" ",
array_map(function($a) {
return strlen($a) ? escapeshellarg($a) : "''";
}, $argv));
}
function __construct($filter=[], $cursor=null) {
$this->filter = $filter;
$this->startpos = $cursor;
}
function _close_journal() {
if ($this->stdout) {
fclose($this->stdout);
$this->stdout = null;
}
if ($this->proc) {
proc_close($this->proc);
$this->proc = null;
}
$this->entry = null;
}
function _open_journal($filter=[], $cursor=null) {
if ($this->proc)
$this->_close_journal();
$this->filter = $filter;
$this->startpos = $cursor;
$cmd = ["journalctl", "-f", "-o", "json"];
if ($cursor) {
$cmd[] = "-c";
$cmd[] = $cursor;
}
$cmd = array_merge($cmd, $filter);
$cmd = self::_join_argv($cmd);
$fdspec = [
0 => ["file", "/dev/null", "r"],
1 => ["pipe", "w"],
2 => ["file", "/dev/null", "w"],
];
$this->proc = proc_open($cmd, $fdspec, $fds);
if (!$this->proc)
return false;
$this->stdout = $fds[1];
}
function seek($cursor) {
$this->_open_journal($this->filter, $cursor);
}
function rewind() {
$this->seek($this->startpos);
}
function next() {
$line = fgets($this->stdout);
if ($line === false)
$this->entry = false;
else
$this->entry = json_decode($line);
}
function valid() {
return ($this->entry !== false);
/* null is valid, it just means next() hasn't been called yet */
}
function current() {
if (!$this->entry)
$this->next();
return $this->entry;
}
function key() {
if (!$this->entry)
$this->next();
return $this->entry->__CURSOR;
}
}
$a = new Journal();
foreach ($a as $cursor => $item) {
echo "================\n";
var_dump($cursor);
//print_r($item);
if ($item)
var_dump($item->MESSAGE);
}

Related

PHP OOP Script using Multithreading, terminates unexpectedly

Hi I'm having a problem trying to get threads to perform properly. The problem is that my php script will terminate unexpectedly at 1 of 2 specific points, during execution, and works the rest of the time.
I've created a test scenario that produces the same result:
This script creates 5 threads. Each thread adds up, each iteration, of a random number ranging from 10 to 20.
Secondly, Summary_Thread checks to see when all threads are completed before printing the summary... and there in lies the issue.
The script terminates during Summary_Thread::run and Stack_Item_Container_Stack::Compile_Summary, which is indirectly called at the end of Summary_Thread::run.
#!/usr/bin/php
<?php
ini_set('max_execution_time', 0);
ini_set('display_errors', 1);
error_reporting(E_ALL ^ E_NOTICE);
ignore_user_abort();
class Summary_Thread_Container
{
public $stack_item_container_stack;
private $summary_thread;
function __construct($thread_count, Stack_Item_Container_Stack $stack_item_container_stack)
{
$this->stack_item_container_stack = $stack_item_container_stack;
$this->summary_thread = new Summary_Thread($thread_count, $this);
$this->summary_thread->start();
}
public function Compile_Summary(){ $this->stack_item_container_stack->Compile_Summary(); }
}
class Summary_Thread extends Worker
{
private $summary_thread_container;
private $thread_count;
function __construct($thread_count, Summary_Thread_Container $summary_thread_container)
{
$this->summary_thread_container = $summary_thread_container;
$this->thread_count = $thread_count;
}
public function run()
{
$thread_count = 0;
echo "\n************************************** Stack Thread Count: {$this->thread_count} \n";
echo "*** START.\n";
if($this->thread_count == $thread_count)
echo "*** THREAD COUNTS MATCH.\n";
else
echo "*** THREAD COUNTS DO NOT MATCH.\n";
while($this->thread_count != $thread_count)
{
$temp_SIC = $this->summary_thread_container->stack_item_container_stack->first_stack_item_container;
$thread_count = 0;
while($temp_SIC)
{
$thread_count++;
echo "************************************** Thread Count: {$thread_count} \n";
$temp_SIC = $temp_SIC->Get_Next_Stack_Item_Container();
}
echo "*** END.\n";
if($this->thread_count == $thread_count)
echo "*** THREAD COUNTS MATCH.\n";
else
echo "*** THREAD COUNTS DO NOT MATCH.\n";
}
$this->Compile_Summary();
}
public function Compile_Summary(){ $this->summary_thread_container->Compile_Summary(); }
}
class Stack_Item_Container_Stack
{
public $first_stack_item_container;
private $thread_count;
private $summary_thread_container;
function __construct()
{
$this->first_stack_item_container = null;
$this->thread_count = 0;
for($i = 0; $i < 5; $i++)
{
echo " * Creating Stack Item Container: {$i}\n";
$this->thread_count++;
$this->Add_Stack_Item_Container(new Stack_Item_Container(rand(10, 20), $i, $this));
}
$this->summary_thread_container = new Summary_Thread_Container($this->thread_count, $this);
}
public function Add_Stack_Item_Container(Stack_Item_Container $stack_item_container)
{
echo " * Adding Stack Item Container *\n";
if($this->first_stack_item_container)
{
$temp_stack_item_container = $this->first_stack_item_container;
while($temp_stack_item_container->Get_Next_Stack_Item_Container())
$temp_stack_item_container = $temp_stack_item_container->Get_Next_Stack_Item_Container();
$temp_stack_item_container->Set_Next_Stack_Item_Container($stack_item_container);
}
else $this->first_stack_item_container = $stack_item_container;
}
public function Compile_Summary()
{
echo "\n";
echo "===============\n";
echo "=== Summary ===\n";
echo "===============\n";
echo "\n";
$temp_SIC = $this->first_stack_item_container;
while($temp_SIC)
{
echo " Thread ID {$temp_SIC->member_variables[0]} ({$temp_SIC->member_variables[4]}) has a Total of {$temp_SIC->member_variables[2]}";
echo "\n";
$temp_SIC = $temp_SIC->Get_Next_Stack_Item_Container();
}
echo "\n";
$this->Kill();
}
private function Kill()
{
while($this->first_stack_item_container)
{
$temp_SIC = $this->first_stack_item_container;
$this->first_stack_item_container = $this->first_stack_item_container->Get_Next_Stack_Item_Container();
$temp_SIC->Kill();
}
unset($this->summary_thread_container);
unset($this);
}
}
class Stack_Item_Container
{
private $stack_item_container_stack;
private $next_stack_item_container;
public $member_variables;
public $stack_item_thread;
function __construct($time, $index, Stack_Item_Container_Stack $stack_item_container_stack)
{
$this->stack_item_container_stack = $stack_item_container_stack;
$this->next_stack_item_container = null;
$this->member_variables = new Stackable();
$this->member_variables[] = -1;
$this->member_variables[] = $time;
$this->member_variables[] = 0;
$this->member_variables[] = false;
$this->member_variables[] = $index;
$this->stack_item_thread = new Stack_Item_Thread($this->member_variables, $this);
$this->stack_item_thread->start();
}
public function Get_Stack_Item_Container_Stack(){ return $this->stack_item_container_stack; }
public function Get_Next_Stack_Item_Container(){ return $this->next_stack_item_container; }
public function Set_Next_Stack_Item_Container(Stack_Item_Container $next_SIC){ $this->next_stack_item_container = $next_SIC; }
public function Kill()
{
$this->stack_item_thread->kill();
unset($this->member_variables);
unset($this);
}
}
class Stack_Item_Thread extends Worker
{
private $stack_item_container;
private $member_variables;
function __construct($member_variables, Stack_Item_Container $stack_item_container)
{
$this->member_variables = $member_variables;
$this->stack_item_container = $stack_item_container;
}
public function run()
{
$this->member_variables[0] = $this->getThreadId();
$total = 0;
echo "\n";
for($i = 0; $i < $this->member_variables[1]; $i++)
{
$total += $i;
$val = $i + 1;
echo "Thread ID ({$this->member_variables[4]}): {$this->member_variables[0]}:";
echo " Count: {$val} of {$this->member_variables[1]}";
echo "\n";
}
echo "\n";
$this->member_variables[2] = $total;
$this->member_variables[3] = true;
}
}
$stack_item_container_stack = new Stack_Item_Container_Stack();
OUTPUT 1 (When it does work):
************************************** Stack Thread Count: 5
*** START.
*** THREAD COUNTS DO NOT MATCH.
************************************** Thread Count: 1
************************************** Thread Count: 2
************************************** Thread Count: 3
************************************** Thread Count: 4
************************************** Thread Count: 5
*** END.
*** THREAD COUNTS MATCH.
===============
=== Summary ===
===============
Thread ID 139975400195840 (0) has a Total of 105
Thread ID 139975389705984 (1) has a Total of 153
Thread ID 139975378360064 (2) has a Total of 153
Thread ID 139975367014144 (3) has a Total of 55
Thread ID 139975130801920 (4) has a Total of 153
OUTPUT 2: (the first point that it will terminate):
************************************** Stack Thread Count: 5
*** START.
*** THREAD COUNTS DO NOT MATCH.
OUTPUT 3: (the second point that it will terminate)
************************************** Stack Thread Count: 5
*** START.
*** THREAD COUNTS DO NOT MATCH.
************************************** Thread Count: 1
************************************** Thread Count: 2
************************************** Thread Count: 3
************************************** Thread Count: 4
************************************** Thread Count: 5
*** END.
*** THREAD COUNTS MATCH.
Just to give you as much information as i can:
(might not be relevant but just in case)
Changes made to config:
File: /etc/sysctl.conf, Changes Made: net.ipv4.tcp_fin_timeout=10
File: php.ini, Changes Made: extension=php_pthreads.dll
Server:
Linux 2.6.32-504.8.1.el6.x86_64
PHP 5.5.13
Apache/2.2.15 (CentOS)
Max Requests Per Child: 4000 - Keep Alive: off - Max Per Connection: 100
Timeouts Connection: 60 - Keep-Alive: 15
Virtual Server No
Please help :) and questions would help ... anything i'm not seeing?
Thanks in advance
I cant test it myself, but if PHP exits normaly without errors its propably caused by the main thread exiting before any other thread could finish. Try Thread::join them (http://php.net/manual/en/thread.join.php) so the parent thread waits until they are finished.

Gearman jobs are passed to more than one worker (PHP)

I have the problem that in a PHP application Gearman jobs sometimes are passed to more than one worker. I could reduce a code to reproduce it into one file. Now I am not sure if this is a bug in Gearman or a bug in the pecl library or maybe in my code.
Here is the code to reproduce the error:
#!/usr/bin/php
<?php
// Try 'standard', 'exception' or 'exception-sleep'.
$sWorker = 'exception';
// Detect run mode "client" or "worker".
if (!isset($argv[1]))
$sMode = 'client';
else
$sMode = 'worker-' . $sWorker;
$sLogFilePath = __DIR__ . '/log.txt';
switch ($sMode) {
case 'client':
// Remove all queued test jobs and quit if there are test workers running.
prepare();
// Init the greaman client.
$Client= new GearmanClient;
$Client->addServer();
// Empty the log file.
file_put_contents($sLogFilePath, '');
// Start some worker processes.
$aPids = array();
for ($i = 0; $i < 100; $i++)
$aPids[] = exec('php ' . __FILE__ . ' worker > /dev/null 2>&1 & echo $!');
// Start some jobs. Also try doHigh(), doBackground() and
// doBackgroundHigh();
for ($i = 0; $i < 50; $i++)
$Client->doNormal('test', $i);
// Wait a second (when running jobs in background).
// sleep(1);
// Prepare the log file entries.
$aJobs = array();
$aLines = file($sLogFilePath);
foreach ($aLines as $sLine) {
list($sTime, $sPid, $sHandle, $sWorkload) = $aAttributes = explode("\t", $sLine);
$sWorkload = trim($sWorkload);
if (!isset($aJobs[$sWorkload]))
$aJobs[$sWorkload] = array();
$aJobs[$sWorkload][] = $aAttributes;
}
// Remove all jobs that have been passed to only one worker as expected.
foreach ($aJobs as $sWorkload => $aJob) {
if (count($aJob) === 1)
unset($aJobs[$sWorkload]);
}
echo "\n\n";
if (empty($aJobs))
echo "No job has been passed to more than one worker.";
else {
echo "Those jobs has been passed more than one times to a worker:\n";
foreach ($aJobs as $sWorload => $aJob) {
echo "\nJob #" . $sWorload . ":\n";
foreach ($aJob as $aAttributes)
echo " $aAttributes[2] (Worker PID: $aAttributes[1])\n";
}
}
echo "\n\n";
// Kill all started workers.
foreach ($aPids as $sPid)
exec('kill ' . $sPid . ' > /dev/null 2>&1');
break;
case 'worker-standard':
$Worker = new GearmanWorker;
$Worker->addServer();
$Worker->addFunction('test', 'logJob');
$bShutdown = false;
while ($Worker->work())
if ($bShutdown)
continue;
break;
case 'worker-exception':
try {
$Worker = new GearmanWorker;
$Worker->addServer();
$Worker->addFunction('test', 'logJob');
$bShutdown = false;
while ($Worker->work())
if ($bShutdown)
throw new \Exception;
} catch (\Exception $E) {
}
break;
case 'worker-exception-sleep':
try {
$Worker = new GearmanWorker;
$Worker->addServer();
$Worker->addFunction('test', 'logJob');
$bShutdown = false;
while ($Worker->work())
{
if ($bShutdown) {
sleep(1);
throw new \Exception;
}
}
} catch (\Exception $E) {
}
break;
}
function logJob(\GearmanJob $Job)
{
global $bShutdown, $sLogFilePath;
$sLine = microtime(true) . "\t" . getmypid() . "\t" . $Job->handle() . "\t" . $Job->workload() . "\n";
file_put_contents($sLogFilePath, $sLine, FILE_APPEND);
$bShutdown = true;
}
function prepare()
{
$rGearman = fsockopen('127.0.0.1', '4730', $iErrno, $sErrstr, 3);
$aBuffer = array();
fputs ($rGearman, 'STATUS' . PHP_EOL);
stream_set_timeout($rGearman, 1);
while (!feof($rGearman))
if ('.' . PHP_EOL !== $sLine = fgets($rGearman, 128))
$aBuffer[] = $sLine;
else
break;
fclose($rGearman);
$bJobsInQueue = false;
$bWorkersRunning = false;
foreach ($aBuffer as $sFunctionLine) {
list($sFunctionName, $iQueuedJobs, $iRunningJobs, $iWorkers) = explode("\t", $sFunctionLine);
if ('test' === $sFunctionName) {
if (0 != $iQueuedJobs)
$bJobsInQueue = true;
if (0 != $iWorkers)
$bWorkersRunning = true;;
}
}
// Exit if there are workers running.
if ($bWorkersRunning)
die("There are some Gearman workers running that have registered a 'test' function. Please stop these workers and run again.\n\n");
// If there are test jobs in the queue start a worker that eat up the jobs.
if ($bJobsInQueue) {
$sPid = exec('gearman -n -w -f test > /dev/null 2>&1 & echo $!');
sleep(1);
exec ("kill $sPid > /dev/null 2>&1");
// Repeat this method to make sure all jobs are removed.
prepare();
}
}
When you run this code on the command line it should output "No job
has been passed to more than one worker." but insted it alway outputs a list of some jobs that have been passed to more than one worker. The error doesn't appear if you set $sWorker = 'standard'; or 'exception-sleep'.
It would help me a lot if you could run the code and tell me if you are able to reproduce the error of if I have a bug in the code.
Had exactly the same issue with Gearman 0.24, PECL lib 1.0.2. Was able to reproduce the error with your script every time.
An older version of Gearman (0.14 I think) used to work fine.
Upgrading Gearman to 0.33 fixed the issue.

PHP and Timer events

I would like to use a Timer in php, sending messages after a certain period of time (no player activity)? This is for a multi-player game through sockets.
I have googled and stackoverflew and found the following
could use a cron (will need to
synronize my php classes (socket
based) with the cron... ), No I
guess.
could use sleep ... if it could wait
for less than one second, I will have
given a try, No again, I guess
could use register_tick_function ...
not applicable I think
Any ideas?
Thanks!
This is what I wrote to deal with my timing needs. It doesn't magically interrupt your code, but it provides a very flexible way to schedule timers. All you have to do, is make sure you run $events->CheckTimers() every now and then.
The libevent module does a lot more, but that's a bit more effort.
$events = new Events();
function callback($text) {
printf("I'm a timer callback %s\n", $text);
}
$events->AddTimer("5s", "callback", "every 5 seconds", EVENTS::EVENT_REPEAT);
$events->AddTimer("2s..15s", "callback", "random time 2 to 15 seconds", EVENTS::EVENT_REPEAT);
$events->AddTimer("1m", create_function('', "echo 'Im a LAMBDA function that runs every minute\n';"), false, EVENTS::EVENT_REPEAT);
$events->AddTimer("1h..2h", "callback", "I will run once, in between 1 and 2 hours");
# This is just an example, in reality, you would make sure to call CheckTimer() regulary (ideally not more than once a second)
while (true) {
$events->CheckTimers();
printf("Next timer in %s seconds...\n", $ne = $events->GetNextTimer(), gettype($ne));
sleep(1);
}
class Events {
const EVENT_REPEAT = 0x0001;
const EVENT_SEQUENCE = 0x0002;
var $events;
var $timers;
function __construct() {
$this->events = array();
$this->timers = array();
}
function AddTimer($when, $action, $args = false, $flags = 0) {
if (preg_match('#([0-9a-zA-Z]+)..([0-9a-zA-Z]+)#', $when, $a)) {
$time = time(NULL) + rand($this->time2seconds($a[1]), $this->time2seconds($a[2]));
} else {
$time = time(NULL) + $this->time2seconds($when);
}
if ($flags & self::EVENT_SEQUENCE) {
while ($this->IsArrayCount($this->timers[$time])) {
$time ++;
}
}
$this->timers[$time][] = array("when" => $when, "action" => $action, "args" => $args, "flags" => $flags);
ksort($this->timers);
}
function GetNextTimer() {
if (!$this->IsArrayCount($this->timers)) {
return false;
}
reset($this->timers);
$firstevent = each($this->timers);
if ($firstevent === false) {
return false;
}
$time = $firstevent["key"];
$nextEvent = $time - time(NULL);
if ($nextEvent < 1) {
return 1;
}
return $nextEvent;
}
function CheckTimers() {
$rv = false;
$now = time(NULL);
foreach ($this->timers as $time => $events) {
if ($time > $now) {
break;
}
foreach ($events as $key => $event) {
# debug("Event::CheckTimer: {$event["action"]}");
# ircPubMsg("Event::CheckTimer: {$event["action"]}", "#bots");
if (!$event["args"]) {
call_user_func($event["action"]);
} else {
$rv = call_user_func_array($event["action"], is_array($event["args"]) ? $event["args"] : array($event["args"]));
}
unset($this->timers[$time][$key]);
if ($event["flags"] & self::EVENT_REPEAT) {
$this->AddTimer($event["when"], $event["action"], $event["args"], $event["flags"]);
}
if ($rv) {
# break;
}
}
if (!$this->IsArrayCount($this->timers[$time])) {
unset($this->timers[$time]);
}
if (0 && $rv) {
break;
}
}
if ($rv) {
return $rv;
}
}
function time2seconds($timeString) {
$end = substr($timeString, strlen($timeString) - 1);
$seconds = intval($timeString); // = preg_replace("#[^0-9]#", "", $a);
if (is_numeric($end)) {
return $seconds;
}
$unim = array("s","m","h","d", "w", "m", "y");
$divs = array(1, 60, 60, 24, 7, 28, 12, false);
$found = false;
while (!$found) {
$u = array_shift($unim);
$d = array_shift($divs);
$seconds *= $d;
if ($end === $u) {
return $seconds;
}
}
return intval($timeString);
}
function IsArrayCount($possibleArray) {
return (isset($possibleArray) && is_array($possibleArray)) ? count($possibleArray) : false;
}
}
Does PHP's time_nanosleep() work for you? Or usleep()?
Maybe a daemon?
http://kevin.vanzonneveld.net/techblog/article/create_daemons_in_php/
here are a few concepts how to do it :
1. common concept is to create script for sending messages, then add it to server Cron application and execute the script with specified time.
2. write python script that will control your php application file or section, in pythonic terms you can fork process on server start that will do your tasks every amount of time (with infinity loop I guess ... while 1: ..)
3. write shell .sh file that will invoke php or python script and add controls of this script it to /etc/init.d to be more controllable.

How to read FoxPro Memo with PHP?

I have to convert .DBF and .FPT files from Visual FoxPro to MySQL. Right now my script works for .DBF files, it opens and reads them with dbase_open() and dbase_get_record_with_names() and then executes the MySQL INSERT commands.
However, some fields of these .DBF files are of type MEMO and therefore stored in a separate files ending in .FPT. How do I read this file?
I have found the specifications of this filetype in MSDN, but I don't know how I can read this file byte-wise with PHP (also, I would really prefer a simplier solution).
Any ideas?
Alright, I have carefully studied the MSDN specifications of DBF and FPT file structures and the outcome is a beautiful PHP class which can open a DBF and (optional) an FPT memo file at the same time. This class will give you record after record and thereby fetch any memos from the memo file - if opened.
class Prodigy_DBF {
private $Filename, $DB_Type, $DB_Update, $DB_Records, $DB_FirstData, $DB_RecordLength, $DB_Flags, $DB_CodePageMark, $DB_Fields, $FileHandle, $FileOpened;
private $Memo_Handle, $Memo_Opened, $Memo_BlockSize;
private function Initialize() {
if($this->FileOpened) {
fclose($this->FileHandle);
}
if($this->Memo_Opened) {
fclose($this->Memo_Handle);
}
$this->FileOpened = false;
$this->FileHandle = NULL;
$this->Filename = NULL;
$this->DB_Type = NULL;
$this->DB_Update = NULL;
$this->DB_Records = NULL;
$this->DB_FirstData = NULL;
$this->DB_RecordLength = NULL;
$this->DB_CodePageMark = NULL;
$this->DB_Flags = NULL;
$this->DB_Fields = array();
$this->Memo_Handle = NULL;
$this->Memo_Opened = false;
$this->Memo_BlockSize = NULL;
}
public function __construct($Filename, $MemoFilename = NULL) {
$this->Prodigy_DBF($Filename, $MemoFilename);
}
public function Prodigy_DBF($Filename, $MemoFilename = NULL) {
$this->Initialize();
$this->OpenDatabase($Filename, $MemoFilename);
}
public function OpenDatabase($Filename, $MemoFilename = NULL) {
$Return = false;
$this->Initialize();
$this->FileHandle = fopen($Filename, "r");
if($this->FileHandle) {
// DB Open, reading headers
$this->DB_Type = dechex(ord(fread($this->FileHandle, 1)));
$LUPD = fread($this->FileHandle, 3);
$this->DB_Update = ord($LUPD[0])."/".ord($LUPD[1])."/".ord($LUPD[2]);
$Rec = unpack("V", fread($this->FileHandle, 4));
$this->DB_Records = $Rec[1];
$Pos = fread($this->FileHandle, 2);
$this->DB_FirstData = (ord($Pos[0]) + ord($Pos[1]) * 256);
$Len = fread($this->FileHandle, 2);
$this->DB_RecordLength = (ord($Len[0]) + ord($Len[1]) * 256);
fseek($this->FileHandle, 28); // Ignoring "reserved" bytes, jumping to table flags
$this->DB_Flags = dechex(ord(fread($this->FileHandle, 1)));
$this->DB_CodePageMark = ord(fread($this->FileHandle, 1));
fseek($this->FileHandle, 2, SEEK_CUR); // Ignoring next 2 "reserved" bytes
// Now reading field captions and attributes
while(!feof($this->FileHandle)) {
// Checking for end of header
if(ord(fread($this->FileHandle, 1)) == 13) {
break; // End of header!
} else {
// Go back
fseek($this->FileHandle, -1, SEEK_CUR);
}
$Field["Name"] = trim(fread($this->FileHandle, 11));
$Field["Type"] = fread($this->FileHandle, 1);
fseek($this->FileHandle, 4, SEEK_CUR); // Skipping attribute "displacement"
$Field["Size"] = ord(fread($this->FileHandle, 1));
fseek($this->FileHandle, 15, SEEK_CUR); // Skipping any remaining attributes
$this->DB_Fields[] = $Field;
}
// Setting file pointer to the first record
fseek($this->FileHandle, $this->DB_FirstData);
$this->FileOpened = true;
// Open memo file, if exists
if(!empty($MemoFilename) and file_exists($MemoFilename) and preg_match("%^(.+).fpt$%i", $MemoFilename)) {
$this->Memo_Handle = fopen($MemoFilename, "r");
if($this->Memo_Handle) {
$this->Memo_Opened = true;
// Getting block size
fseek($this->Memo_Handle, 6);
$Data = unpack("n", fread($this->Memo_Handle, 2));
$this->Memo_BlockSize = $Data[1];
}
}
}
return $Return;
}
public function GetNextRecord($FieldCaptions = false) {
$Return = NULL;
$Record = array();
if(!$this->FileOpened) {
$Return = false;
} elseif(feof($this->FileHandle)) {
$Return = NULL;
} else {
// File open and not EOF
fseek($this->FileHandle, 1, SEEK_CUR); // Ignoring DELETE flag
foreach($this->DB_Fields as $Field) {
$RawData = fread($this->FileHandle, $Field["Size"]);
// Checking for memo reference
if($Field["Type"] == "M" and $Field["Size"] == 4 and !empty($RawData)) {
// Binary Memo reference
$Memo_BO = unpack("V", $RawData);
if($this->Memo_Opened and $Memo_BO != 0) {
fseek($this->Memo_Handle, $Memo_BO[1] * $this->Memo_BlockSize);
$Type = unpack("N", fread($this->Memo_Handle, 4));
if($Type[1] == "1") {
$Len = unpack("N", fread($this->Memo_Handle, 4));
$Value = trim(fread($this->Memo_Handle, $Len[1]));
} else {
// Pictures will not be shown
$Value = "{BINARY_PICTURE}";
}
} else {
$Value = "{NO_MEMO_FILE_OPEN}";
}
} else {
$Value = trim($RawData);
}
if($FieldCaptions) {
$Record[$Field["Name"]] = $Value;
} else {
$Record[] = $Value;
}
}
$Return = $Record;
}
return $Return;
}
function __destruct() {
// Cleanly close any open files before destruction
$this->Initialize();
}
}
The class can be used like this:
$Test = new Prodigy_DBF("customer.DBF", "customer.FPT");
while(($Record = $Test->GetNextRecord(true)) and !empty($Record)) {
print_r($Record);
}
It might not be an almighty perfect class, but it works for me. Feel free to use this code, but note that the class is VERY tolerant - it doesn't care if fread() and fseek() return true or anything else - so you might want to improve it a bit before using.
Also note that there are many private variables like number of records, recordsize etc. which are not used at the moment.
I replaced this code:
fseek($this->Memo_Handle, (trim($RawData) * $this->Memo_BlockSize)+8);
$Value = trim(fread($this->Memo_Handle, $this->Memo_BlockSize));
with this code:
fseek($this->Memo_Handle, (trim($RawData) * $this->Memo_BlockSize)+4);
$Len = unpack("N", fread($this->Memo_Handle, 4));
$Value = trim(fread($this->Memo_Handle, $Len[1] ));
this helped for me
Although not PHP, VFP is 1-based references and I think PHP is zero-based references so you'll have to decypher and adjust accordingly, but this works and hopefully you'll be able
to post your version of this portion when finished.
FILETOSTR() in VFP will open a file, and read the entire content into
a single memory variable as a character string -- all escape keys, high byte characters, etc, intact. You'll probably need to rely on an FOPEN(), FSEEK(), FCLOSE(), etc.
MemoTest.FPT was my sample memo table/file
fpt1 = FILETOSTR( "MEMOTEST.FPT" )
First, you'll have to detect the MEMO BLOCK SIZE used when the file was created. Typically this would be 64 BYTES, but per the link you had in your post.
The header positions 6-7 identify the size (VFP, positions 7 and 8). The first byte is the high-order
nBlockSize = ASC( SUBSTR( fpt1, 7, 1 )) * 256 + ASC( SUBSTR( fpt1, 8, 1 ))
Now, at your individual records. Wherever in your DBF structure has the memo FIELD (and you can have many per single record structure) there will be 4 bytes. In THE RECORD field, it identifies the "block" in the memo file where the content is stored.
MemoBytes = 4 bytes at your identified field location. These will be stored as ASCII from 0-255. This field is stored with the FIRST byte as low-order and the 4th byte as 256^3 = 16777216. The first "Block" ever used will be starting in position offset of 512 per the memo .fpt file spec that the header takes up positions 0-511.
So, if your first memo field has a content of "8000" where the 8 is the actual 0x08, not number "8" which is 0x38, and the zeros are 0x00.
YourMemoField = "8000" (actually use ascii, but for readability showing hex expected value)
First Byte is ASCII value * 1 ( 256 ^ 0 )
Second Byte is ASCII value * 256 (256 ^ 1)
Third Byte is ASCII value * 65536 (256 ^ 2)
Fourth Byte is ASCII value * 16777216 (256 ^ 3)
nMemoBlock = byte1 + ( byte2 * 256 ) + ( byte3 * 65536 ) + ( byte4 * 16777216 )
Now, you'll need to FSEEK() to the
FSEEK( handle, nMemoBlock * nBlockSize +1 )
for the first byte of the block you are looking for. This will point to the BLOCK header. In this case, per the spec, the first 4 bytes identify the Block SIGNATURE, the second 4 bytes is the length of the content. For these two, the bytes are stored with HIGH-BYTE first.
From your FSEEK(), its REVERSE of the nMemoBlock above with the high-byte. The "Byte1-4" here are from your FSEEK() position
nSignature = ( byte1 * 16777216 ) + ( byte2 * 65536 ) + ( byte3 * 256 ) + byte4
nMemoLength = ( byte5 * 16777216 ) + ( byte6 * 65536 ) + ( byte7 * 256 ) + byte8
Now, FSEEK() to the 9th byte (1st actual character of the data AFTER the 8 bytes of the header you just read for signature and memo length). This is the beginning of your data.
Now, read the rest of the content...
FSEEK() +9 characters to new position
cFinalMemoData = FREAD( handle, nMemoLength )
I know this isn't perfect, nor PHP script, but its enough of pseudo-code on hOW things are stored and hopefully gets you WELL on your way.
Again, PLEASE take into consideration as you are stepping through your debug process to ensure 0 or 1 offset basis. To help simplify and test this, I created a simple .DBF with 2 fields... a character field and a memo field, added a few records and some basic content to confirm all content, positions, etc.
I think it's unlikely there are FoxPro libraries in PHP.
You may have to code it from scratch. For byte-wise reading, meet fopen() fread() and colleagues.
Edit: There seems to be a Visual FoxPro ODBC driver. You may be able to connect to a FoxPro database through PDO and that connector. How the chances of success are, and how much work it would be, I don't know.
The FPT file contains memo data. In the DBF you have columns of type memo, and the information in this column is a pointer to the entry in the FPT file.
If you are querying the data from the table you only have to reference the memo column to get the data. You do not need to parse the data out of the FPT file separately. The OLE DB driver (or the ODBC driver if your files are VFP 6 or earlier) should just give you this information.
There is a tool that will automatically migrate your Visual FoxPro data to MySQL. You might want to check it out to see if you can save some time:
Go to http://leafe.com/dls/vfp
and search for "Stru2MySQL_2" for the tool to migrate the structures of the data and "VFP2MySQL Data Upload program" for tools to help with the migration.
Rick Schummer VFP MVP
You also might want to check the PHP dbase libraries.They work quite well with DBF files.
<?
class Prodigy_DBF {
private $Filename, $DB_Type, $DB_Update, $DB_Records, $DB_FirstData, $DB_RecordLength, $DB_Flags, $DB_CodePageMark, $DB_Fields, $FileHandle, $FileOpened;
private $Memo_Handle, $Memo_Opened, $Memo_BlockSize;
private function Initialize() {
if($this->FileOpened) {
fclose($this->FileHandle);
}
if($this->Memo_Opened) {
fclose($this->Memo_Handle);
}
$this->FileOpened = false;
$this->FileHandle = NULL;
$this->Filename = NULL;
$this->DB_Type = NULL;
$this->DB_Update = NULL;
$this->DB_Records = NULL;
$this->DB_FirstData = NULL;
$this->DB_RecordLength = NULL;
$this->DB_CodePageMark = NULL;
$this->DB_Flags = NULL;
$this->DB_Fields = array();
$this->Memo_Handle = NULL;
$this->Memo_Opened = false;
$this->Memo_BlockSize = NULL;
}
public function __construct($Filename, $MemoFilename = NULL) {
$this->Prodigy_DBF($Filename, $MemoFilename);
}
public function Prodigy_DBF($Filename, $MemoFilename = NULL) {
$this->Initialize();
$this->OpenDatabase($Filename, $MemoFilename);
}
public function OpenDatabase($Filename, $MemoFilename = NULL) {
$Return = false;
$this->Initialize();
$this->FileHandle = fopen($Filename, "r");
if($this->FileHandle) {
// DB Open, reading headers
$this->DB_Type = dechex(ord(fread($this->FileHandle, 1)));
$LUPD = fread($this->FileHandle, 3);
$this->DB_Update = ord($LUPD[0])."/".ord($LUPD[1])."/".ord($LUPD[2]);
$Rec = unpack("V", fread($this->FileHandle, 4));
$this->DB_Records = $Rec[1];
$Pos = fread($this->FileHandle, 2);
$this->DB_FirstData = (ord($Pos[0]) + ord($Pos[1]) * 256);
$Len = fread($this->FileHandle, 2);
$this->DB_RecordLength = (ord($Len[0]) + ord($Len[1]) * 256);
fseek($this->FileHandle, 28); // Ignoring "reserved" bytes, jumping to table flags
$this->DB_Flags = dechex(ord(fread($this->FileHandle, 1)));
$this->DB_CodePageMark = ord(fread($this->FileHandle, 1));
fseek($this->FileHandle, 2, SEEK_CUR); // Ignoring next 2 "reserved" bytes
// Now reading field captions and attributes
while(!feof($this->FileHandle)) {
// Checking for end of header
if(ord(fread($this->FileHandle, 1)) == 13) {
break; // End of header!
} else {
// Go back
fseek($this->FileHandle, -1, SEEK_CUR);
}
$Field["Name"] = trim(fread($this->FileHandle, 11));
$Field["Type"] = fread($this->FileHandle, 1);
fseek($this->FileHandle, 4, SEEK_CUR); // Skipping attribute "displacement"
$Field["Size"] = ord(fread($this->FileHandle, 1));
fseek($this->FileHandle, 15, SEEK_CUR); // Skipping any remaining attributes
$this->DB_Fields[] = $Field;
}
// Setting file pointer to the first record
fseek($this->FileHandle, $this->DB_FirstData);
$this->FileOpened = true;
// Open memo file, if exists
if(!empty($MemoFilename) and file_exists($MemoFilename) and preg_match("%^(.+).fpt$%i", $MemoFilename)) {
$this->Memo_Handle = fopen($MemoFilename, "r");
if($this->Memo_Handle) {
$this->Memo_Opened = true;
// Getting block size
fseek($this->Memo_Handle, 6);
$Data = unpack("n", fread($this->Memo_Handle, 2));
$this->Memo_BlockSize = $Data[1];
}
}
}
return $Return;
}
public function GetNextRecord($FieldCaptions = false) {
$Return = NULL;
$Record = array();
if(!$this->FileOpened) {
$Return = false;
} elseif(feof($this->FileHandle)) {
$Return = NULL;
} else {
// File open and not EOF
fseek($this->FileHandle, 1, SEEK_CUR); // Ignoring DELETE flag
foreach($this->DB_Fields as $Field) {
$RawData = fread($this->FileHandle, $Field["Size"]);
// Checking for memo reference
if($Field["Type"] == "M" and $Field["Size"] == 4 and !empty($RawData)) {
// Binary Memo reference
$Memo_BO = unpack("V", $RawData);
if($this->Memo_Opened and $Memo_BO != 0) {
fseek($this->Memo_Handle, $Memo_BO[1] * $this->Memo_BlockSize);
$Type = unpack("N", fread($this->Memo_Handle, 4));
if($Type[1] == "1") {
$Len = unpack("N", fread($this->Memo_Handle, 4));
$Value = trim(fread($this->Memo_Handle, $Len[1]));
} else {
// Pictures will not be shown
$Value = "{BINARY_PICTURE}";
}
} else {
$Value = "{NO_MEMO_FILE_OPEN}";
}
} else {
if($Field["Type"] == "M"){
if(trim($RawData) > 0) {
fseek($this->Memo_Handle, (trim($RawData) * $this->Memo_BlockSize)+8);
$Value = trim(fread($this->Memo_Handle, $this->Memo_BlockSize));
}
}else{
$Value = trim($RawData);
}
}
if($FieldCaptions) {
$Record[$Field["Name"]] = $Value;
} else {
$Record[] = $Value;
}
}
$Return = $Record;
}
return $Return;
}
function __destruct() {
// Cleanly close any open files before destruction
$this->Initialize();
}
}
?>

How could I parse gettext .mo files in PHP4 without relying on setlocale/locales at all?

I made a couple related threads but this is the one direct question that I'm seeking the answer for. My framework will use Zend_Translate if the php version is 5, otherwise I have to mimic the functionality for 4.
It seems that pretty much every implementation of gettext relies on setlocale or locales, I know there's a LOT of inconsistency across systems which is why I don't want to rely upon it.
I've tried a couple times to get the textdomain, bindtextdomain and gettext functions to work but I've always needed to invoke setlocale.
By the way, all the .mo files will be UTF-8.
Here's some reusable code to parse MO files in PHP, based on Zend_Translate_Adapter_Gettext:
<?php
class MoParser {
private $_bigEndian = false;
private $_file = false;
private $_data = array();
private function _readMOData($bytes)
{
if ($this->_bigEndian === false) {
return unpack('V' . $bytes, fread($this->_file, 4 * $bytes));
} else {
return unpack('N' . $bytes, fread($this->_file, 4 * $bytes));
}
}
public function loadTranslationData($filename, $locale)
{
$this->_data = array();
$this->_bigEndian = false;
$this->_file = #fopen($filename, 'rb');
if (!$this->_file) throw new Exception('Error opening translation file \'' . $filename . '\'.');
if (#filesize($filename) < 10) throw new Exception('\'' . $filename . '\' is not a gettext file');
// get Endian
$input = $this->_readMOData(1);
if (strtolower(substr(dechex($input[1]), -8)) == "950412de") {
$this->_bigEndian = false;
} else if (strtolower(substr(dechex($input[1]), -8)) == "de120495") {
$this->_bigEndian = true;
} else {
throw new Exception('\'' . $filename . '\' is not a gettext file');
}
// read revision - not supported for now
$input = $this->_readMOData(1);
// number of bytes
$input = $this->_readMOData(1);
$total = $input[1];
// number of original strings
$input = $this->_readMOData(1);
$OOffset = $input[1];
// number of translation strings
$input = $this->_readMOData(1);
$TOffset = $input[1];
// fill the original table
fseek($this->_file, $OOffset);
$origtemp = $this->_readMOData(2 * $total);
fseek($this->_file, $TOffset);
$transtemp = $this->_readMOData(2 * $total);
for($count = 0; $count < $total; ++$count) {
if ($origtemp[$count * 2 + 1] != 0) {
fseek($this->_file, $origtemp[$count * 2 + 2]);
$original = #fread($this->_file, $origtemp[$count * 2 + 1]);
$original = explode("\0", $original);
} else {
$original[0] = '';
}
if ($transtemp[$count * 2 + 1] != 0) {
fseek($this->_file, $transtemp[$count * 2 + 2]);
$translate = fread($this->_file, $transtemp[$count * 2 + 1]);
$translate = explode("\0", $translate);
if ((count($original) > 1) && (count($translate) > 1)) {
$this->_data[$locale][$original[0]] = $translate;
array_shift($original);
foreach ($original as $orig) {
$this->_data[$locale][$orig] = '';
}
} else {
$this->_data[$locale][$original[0]] = $translate[0];
}
}
}
$this->_data[$locale][''] = trim($this->_data[$locale]['']);
unset($this->_data[$locale]['']);
return $this->_data;
}
}
Ok, I basically ended up writing a mo file parser based on Zend's Gettext Adapter, as far as I know gettext is pretty much reliant upon the locale, so manually parsing the .mo file would save the hassle of running into odd circumstances with locale issues with setlocale. I also plan on parsing the Zend Locale data provided in the form of xml files.

Categories