Skip to content

php

Scheduling tasks on win32 with PHP

The other product of last night was another win32 only extension, this time for manipulating scheduled tasks. Why do you need an extension? The reason is that Microsoft chose to expose the "mstask" API as non-automation compatible COM APIs; that means that you can't simply use the PHP COM extension (which is really "PHP-OLE") to work with it.

The win32scheduler extension provides 5 functions:

  • win32_scheduler_enum_tasks() - returns an array containing the names of the scheduled tasks that are present on the system.
  • win32_scheduler_run(string taskname) - requests that the system run a given task immediately. It does not wait for the task to complete.
  • win32_scheduler_get_task_info(string taskname) - returns information about a given task.
  • win32_scheduler_set_task_info(string taskname, array info [, bool new]) - updates or creates a new task.
  • win32_scheduler_delete_task(string taskname) - deletes a named task.

It's fairly self explanatory, although the details for the task info are a little weird. I'll document those for PHP at a later date, but if you're interested in working with it now, you'd do well to read the MSDN docs on the Task Scheduler; the structures and unions docs will help you to figure out the task info format. If you want to create a task, you'll find it helpful to var_dump() the info returned from an existing task; the set_task_info() function uses the same data format. Top tip: you need to supply a "Password" field and set it to the password for the user account you set in "RunAs".

This extension should also be showing up on the PHP 5 PECL snaps page in the next couple of hours.

Enjoy!

php|tropics 2005

php|tropics

php|cruise was good, and this years offering from php|arch (php|tropics 2005) looks set to exceed it. I particularly enjoy a good conference; they're not just about learning new things, but also about getting together and sharing ideas, attendee:attendee, speaker:attendee and speaker:speaker.

I'm fortunate enough to be making two presentations, on the following topics:

PHP and Databases Grown-up: PDO


PHP 5.1 features a new data access layer, PHP Data Objects (PDO), that provides a clear, simple (but powerful) unified API for working with all our favourite databases. Features include prepared statements with bound parameters (for all databases, even those that don't natively support them), transactions, cursors, LOBs and flexible error handling. Join me in this talk to learn more about the future of database access in PHP.

Speaking of grown up, following my talk is Dan Scott from IBM on using DB2 with PHP (hopefully your audience will be bigger this time Dan :-)

PHP Streams: Lucky Dip


A roundup of a bunch of handy things you can do with streams in PHP 5. We'll look at time saving functions like get and put contents, as well as multiplexing clients/servers, user space wrappers and other streamy tricks. In short, a whole bunch of Streams tid-bits for your toolbox; let's get out of the fread($fp, filesize($filename)) age.

I very warmly encourage you to attend the conference; the pricing is pretty good (and keep in mind that it's an all inclusive resort--that includes alcoholic beverages and food). If your boss isn't interested, but you really are, why not make php|tropics your vacation destination for the year?

What I miss in Thunderbird

Is the ability to display a couple of extra non-standard headers in addition to the usual From, Subject and Date stuff.

I know I can turn on all the headers, but that's just a bit too much noise.

Maybe someone out there can write an extension for that?

[PS: I know this isn't strictly PHP based, but since I spend a lot of time managing the php.net mail server, I guess it could be construed as PHP].

PECL::event

Yesterday I added the product of my weekend-of-hackery to PECL: the event extension.

PECL::event implements an event based scheduling engine that will execute a callback when a particular event is triggered. The various different triggers allowed are:

  • EV_READ - a stream becomes ready to read
  • EV_WRITE - a stream becomes ready to write
  • EV_EXCEP - a stream has out-of-band data available to read
  • EV_TIMEOUT - a certain amount of time has elapsed
  • EV_SIGNAL - a signal was raised

As you might have already guessed, PECL::event is probably most useful for longer-lived scripts that need to perform more than network "operation" without either taking too long (why do one after the other if you can do both at the same time?) or without one blocking the other and making it time out.

I've previously given a talk (or was it a magazine article?) on multiplexing with streams in PHP; this extension takes things a step further by taking the complex scheduling logic out of your script. As a bonus, it can also take advantage of the more scalable IO scheduling interfaces (epoll(4), kqueue, /dev/poll, poll(2) and select(2)) available on various different operating systems. Scalable is one of those phrases that can easily be misinterpreted, so in this context more scalable means lower overhead per file descriptor , which should translate to faster execution time in your script.

In practice, you probably won't notice much difference between the different engines in PECL::event, but you should notice the difference between a userspace implementation using stream_select() and PECL::event.

How do I use it?

<?php
  # our callback
  function readable($stream, $mask, $arg) {
     if ($mask & EV_READ) {
       echo "$arg is readable:\\n";
       echo fread($stream, 8192);
       fwrite($stream, "QUIT\\r\\n");
       fclose($stream);
     }
  }
  # detect and activate the best available scheduling engine  
  event_init();
  # create a new event
  $e = event_new(
       # associate it with an async connection to a gmail MX
       stream_socket_client("tcp://gsmtp171.google.com:25",
          $errno, $errstr, 0,
          STREAM_CLIENT_CONNECT|STREAM_CLIENT_ASYNC_CONNECT),
       # ask the engine to tell us when it is ready to read
       EV_READ,
       # tell it to call the 'readable' function when triggered
       'readable',
       # and pass in the following string as an argument
       'gmail mx');
  # put the event into the scheduling engine   
  event_schedule($e);
  # similarly, create another event, this time connecting
  # to the PHP MX 
  $e = event_new(
       stream_socket_client("tcp://osu1.php.net:25",
          $errno, $errstr, 0,
          STREAM_CLIENT_CONNECT|STREAM_CLIENT_ASYNC_CONNECT),
       EV_READ,
       'readable',
       'php mx');
  event_schedule($e);
  # now service all registered events
  event_dispatch();
  # we get here when both events have been run
?>

If you run this script, you should see it output something like this:

  gmail mx is readable:
  220 mx.gmail.com ESMTP 71si267099rna
  php mx is readable:
  220-PHP  is a widely-used general-purpose scripting language
  220-that is especially suited for Web development and can be
  220-embedded into HTML. If you are new to PHP and want to get
  220-some idea of how it works, try the introductory tutorial.
  220-After that, check out the online manual, and the example
  220-archive sites and some of the other resources available in
  220-the links section.
  220-
  220-Do not send UBE to this server.
  220-
  220 osu1.php.net ESMTP Ecelerity HEAD (r3928) Mon, 13 Dec 2004 17:59:20 -0800

You'll see the osu1 banner a couple of seconds after running the script, because that server deliberately pauses.

What's the point?

The script above connects to two SMTP servers, reads their banners and then gracefully closes the connection. So what? Big deal. I can write a much shorter script that does the same thing using just a couple of lines for each host:

<?php
   $fp = fsockopen('gsmtp171.google.com', 25);
   echo fgets($fp);
   fwrite($fp, "QUIT\\r\\n");
   fclose($fp);
   $fp = fsockopen('osu1.php.net', 25);
   echo fgets($fp);
   fwrite($fp, "QUIT\\r\\n");
   fclose($fp);
?>

That may be the case, but with the PECL::event approach, you're not blocking on each connection (remember, osu1 makes you wait a couple of seconds) and you're not blocking on each read. In fact, you could happily add 10s or perhaps even 100s of machines to monitor and have them all happen concurrently. Neat stuff, huh?

That's all I'm going to write for now, but before I go I'll leave you with a couple of interesting snippets if you're interested in playing with the extension.

  • Events are automatically descheduled after they have run once. To get them to run again, you need to either call event_schedule() again or set the EV_PERSIST flag in your event mask when you call event_new(). You can manually event_deschedule() a persistent event to take it out of the scheduling engine (effectively pausing the event).

  • You can specify a deadline (well, a timeout) for an event when you schedule it; the 2nd and 3rd parameters are seconds and microseconds for the timeout.

  • You can call event_new() with a null stream parameter to create an event that is not associated with a stream. event_schedule() it using a timeout to have that function called after a certain amount of elapsed time.

  • The extension is still beta; docs are pending and bug reports are welcome ;)

Java: How to Program

I was asked to review this book recently (with an eye on how it equates to PHP), and having just gone through it, I'm quite impressed. It is well written, well structured (taking you from no knowledge and going from there) and teaches you good design principles, without beating you over the head with them.

Having completed my review, it is apparent that PHP 5 has most of the good parts from java, without bogging you down in syntax. I view this as a good thing, making it easier for programmers to drift between the two languages more easily.

If I was asked to recommend a book on java programming, I wouldn't hesitate to recommend this one. I would even consider recommending it to someone new to programming, because most of what it teaches applies equally well to other languages; it serves as a very useful primer, going into the right amount of detail, to make future employees useful regardless of the language that you're going to use.

PHP Marketing War Chest

Marco aims to publish a number of case studies on the adoption of PHP in small, medium, and large businesses (or enterprise, if you will) with the intention of creating a kind of marketing "war chest". The idea is to form a kind of business PHP hub that will help other businesses to make a more informed decision when it comes to adopting PHP.

Marco is looking for people to write case studies in marketing-speak to use there; for more information, read Marco's blog entry.

.Net annoyances

I've spent some hacking time this weekend improving the COM/.Net extension for PHP. I am both happy and annoyed with the results. In order to convey why I'm annoyed, you'll need a bit of background.

COM is all about interfaces. PHP uses the OLE Automation interfaces that allow run time binding of classes and methods; the key here is IDispatch. There are also a set of interfaces known as Connection Points that effectively allow an object to call you back, via another object that you supply, to be able to handle events. The guts of Connection Points are hidden away from you in VB via the WithEvents keyword. PHP also supports Connection Points and also hides the details from your script via the com_event_sink() function.

Now, .Net is similar, but not. It has a number of COM-ish concepts and even ships with the "interop" layer that will automagically map COM into .Net and vice versa. PHP uses the interop layer to bind to .Net at runtime; this is cool because it means that PHP doesn't then require the .Net runtime to be installed on the target machine.

As part of my quest for a better offline blogging tool, I thought about using .Net from PHP to get at the System.Windows.Forms classes. It quickly became apparent that there are some shortcomings with the .Net interop stuff. The theory is that .Net will map its classes to IDispatch'able interfaces via CCW's (COM Compatible Wrappers). In practice it does that, but doesn't explicitly implement the interfaces; instead, IDispatch is present implicitly in the System.Object interface. The effect of this is that the PHP stuff breaks when it sees these objects coming back out from COM, because it can't see IDispatch: .Net is being naughty.

The fix for this is simply to import the interface definition for System.Object and explicitly check for that interface in each of the places where we are looking for IDispatch. This is fine, except that it isn't simple. System.Object is defined in the mscorlib assembly. There is no mscorlib.h header file, and there is no mscorlib.idl to generate a C header file either. In order to get at this stuff I had to load up the binary type library file and export it as IDL. I then needed to hand-edit the 20k lines of IDL so that it could be compiled by MIDL (the order of many declarations was wrong). This process resulted in a 3MB mscorlib.h file (115k lines).

Armed with the header, I added support for System.Object and was able to instantiate the System.Windows.Form::Form class from PHP, set its caption and size and see it on-screen. Yay!

The next step was events, because a form that doesn't react to user input is not very useful.

Events in .Net are based on delegates. Delegates is a posh way of saying "callbacks"; your delegate is essentially a structure containing a pointer to a function or method and a pointer to the object to which the method belongs. Hooking up the event is simply a matter of telling the source object (say, a button on a form) where your delegate structure lives.

At the lower level, the procedure for hooking up a 'Click' event for a button is something like this:

  • $type = $button->get_Type();
  • $event = $type->GetEvent('Click');
  • $event->AddHandler($button, new Delegate('my_click_handler'));

With the help of our 3MB header file, we can execute this code. We need to implement the Delegate interface on our own stub, because there is no standard Delegate class that we can set up with a generic C callback function.

And this is where things fall down: .Net doesn't accept our Delegate instance because it is not managed code.

The way to get this stuff to work is to build the PHP com extension as managed code, and re-implement chunks to use the Microsoft specific extensions to C++. This poses 3 problems:

  • Not everyone has a CLR capable compiler, so our build system needs to either detect this or provide a switch for the user to turn it on.
  • /CLR mode is not compatible with the majority of regular CFLAGS we use (like turning on debug info), so it requires some hackery in the build system to even get it to build.
  • Building with the /CLR switch will force a hard dependency on the .Net runtime, so we would need to:
    • make COM a shared extension (php_com_dotnet.dll)
    • build one version with and one version without .Net support (more build hackery)

Big ouch.

A really nice clean solution would appear to be to separate the .Net stuff out in its own extension; that would solve a couple of problems, but introduce others: there are a few places that need to handle .net specific cases, and those introduce linkage dependencies on the .Net extension.

This is fixable, but we're now into the realms of work-exceeds-fun-returns, so I'm probably just going to leave things as they are. Sorry folks.

If you want events in the mean time, all the .Net docs suggest that you should instead make your own wrappers for the objects, using a .Net compatible language, and implement them as Connection Point style interfaces. The interop layer can then perform behind the scenes voodoo for the CCW's, and you should then be able to use com_event_sink() to hook up your events.

Blog API and blogging tools

After reading George's solicitation, I decided to take a look around for win32 offline blogging apps. The two I looked at this morning (w.bloggar and SharpMT) didn't work with my old 'zlog, which is a bit annoying. From what I could see, w.bloggar uses methods not supported by s9y and SharpMT is broken (the correct responses were being sent, it just kept thinking that there was an HTTP violation).

Is there good, bloat-free (or at least, not over bloated) app for win32? Is there one for linux ?

It's like 5 bucks, right?

Well, I'm finally back from php|works, feeling pretty good about how well the conference went for everyone, and pretty jet-lagged and travel tired.

If there is one thing I'll always remember from the conference, it will be the slur on Canadian currency value from the title of this post; it got a lot of mileage (all in good spirits though!).

The conference seemed to go very well; I didn't see (m)?any attendees playing truant, and that suggests the there was enough going on to satisfy them all. After hours there was plenty to do in and around Toronto (even if it meant going to Best Buy first) and that made a welcome change compared to the venues for some of the other conferences I have been to--they have a tendency to be somewhat isolated, and that can lead to a kind of cabin fever.

I was one of the unfortunate speakers to be harasmussed, but I didn't mind; it was kinda funny to see people walk in, hang around for a bit, and then sneak out carrying the vacant chairs from the room.

Thumbs up to Marco and php|a for another great conference! :-)

PDO semi-toy example

Here's a little script I just cooked up for my crontab. It watches the sites listed in the URLs array for changes in the "plain text"; when a change is detected, it outputs the URL.

Although the task is somewhat trivial, this script does a good job of demonstrating how convenient bound parameters can be. To make the update, I simply need to call $upd->execute(). No need to manipulate or massage the variables in the script, and no need to generate the SQL statement on each iteration.

<?php   
   $URLS = array(
      'http://www.nvidia.com/object/linux.html',
      'http://ipw2200.sourceforge.net/news.php',
   );
   if (!extension_loaded('pdo_sqlite')) {
      dl('pdo.so');
      dl('pdo_sqlite.so');
   }
   $db = new PDO('sqlite:' . getenv('HOME') . "/.web-watch.sql3");
   $db->query('create table hash(url primary key, hash)');
   $db->setAttribute(PDO_ATTR_ERRMODE, PDO_ERRMODE_EXCEPTION);
   $cached_hash = null;
   $stmt = $db->prepare('select hash from hash where url = :url');
   $stmt->bindParam(':url', $url);
   $stmt->bindColumn('hash', $cached_hash);
   $upd = $db->prepare('replace into hash (url, hash) values (:url, :hash)');
   $upd->bindParam(':url', $url);
   $upd->bindParam(':hash', $hash);
   foreach ($URLS as $url) {
      $body = file_get_contents($url);
      $hash = md5(strip_tags($body));
      $cached_hash = null;
      if ($stmt->execute()) {
         $stmt->fetch(PDO_FETCH_BOUND);
      }
      if ($cached_hash != $hash) {
         $upd->execute();
         echo "Changed web:\\n$url\\n";
      }
   }
?>