background image
HomeRecent PostsDrupalSearchTagsRSSContactAboutAccount
Eric.London's picture

In certain situations you need more control of your cron tasks, execution times, and methods. Check out the SuperCron and Elysia Cron modules as alternative solutions. Both give you the ability to order, manage, disable, and execute your cron tasks, among a slew of other functionality. If you are familiar with Linux crontabs and need that level of scheduling and control, see Elysia Cron.

On a recent project, I struggled to get a massive cron hook to complete, and at one point, some of the tasks required a Batch API implementation to manage system resources. I eventually decided to pull the cron task out of the Drupal/Apache/web environment to execute it on the shell in a separate crontab. Here's the gist of the PHP script I put in my Drupal docroot.

<?php
// check if this is web traffic, versus CLI
if (isset($_SERVER['HTTP_USER_AGENT'])) {
header('HTTP/1.1 403 Forbidden');
die(
'You are not authorized to access this page.');
}

// set environment variables:

// remove time limit of script
set_time_limit(0);

// increase memory usage
ini_set('memory_limit', '256M');

// ensure script is being executed from Drupal docroot
$docroot_path = dirname($_SERVER['SCRIPT_NAME']);
if (
getcwd() != $docroot_path) {
chdir($docroot_path);
}

// set bootstrap variables:
// these lines ensure multi-site environments will work with a bootstrap
$_SERVER['HTTP_HOST'] = 'www.myhostname.com';
$_SERVER['SCRIPT_NAME'] = '/' . basename(__file__);

// include Drupal bootstrap
require_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

// check for custom module and cron hook
if (!module_exists(array('MYMODULE')) || !function_exists('MYMODULE_cron')) {
die(
'Module is not enabled.');
}

// execute custom cron hook
MYMODULE_cron();
?>

I then added a line in my crontab on the shell

$ crontab -e

# file contents:

#min hour dayMonth month dayWeek command

0    0    *        *     *       /usr/bin/php /path/to/drupal/docroot/cron.custom.php

The above crontab executes my custom cron hook once a day.

My Drupal photo gallery implements a total of 9 imagecache presets, but due to a PHP memory_limit cap of 90MB in my previous hosting contract, I frequently encountered issues generating images.

Since I was on a shared hosting plan, PHP was executed as CGI and a service ran on the server to kill PHP scripts that reached the PHP memory_limit. This resulted in the following error, which prevented imagecache from creating missing images and redirecting the user properly.

Premature end of script headers: index.php

After changing my hosting plan and increasing my server resources, I decided to write a PHP script to programmatically generate all the missing images. This script when executed on the shell using Drush, will get a list of every image in the files directory, and make an HTTP request for each imagecache preset URL.

<?php
// store original working directory
define('ORIGINAL_DIRECTORY', getcwd());

// define site http host
define('HTTP_HOST','pics.ericlondon.com');

// get imagecache presets data
$presets_data = imagecache_presets();

// loop through presets data and collect preset names
$presets = array();
foreach (
$presets_data as $key => $value) {
 
$presets[] = $value['presetname'];
}

// get a list of pictures
chdir(file_directory_path());
// NOTE: the following line uses find, grep, and sed to generate a list of image files.
// It will vary depending on which file extensions to include and which directories to ignore
$command = 'find . -type f | egrep -ir "\.(gif|jpeg|jpg|png)$" | sed "s/^\.\///" | egrep -iv "^(imagecache|imagefield_thumbs)\/"';
$output = `$command`;
$files = explode("\n", trim($output));
chdir(ORIGINAL_DIRECTORY);

// loop through file list
foreach ($files as $file) {

 
// loop through each preset
 
foreach ($presets as $preset) {
 
   
// define url
   
$url = 'http://' . HTTP_HOST . '/' . file_directory_path() . '/imagecache/' . $preset . '/' . $file;
   
   
// request url
   
$http_request_data = drupal_http_request($url);
   
   
// log entry
   
$log_entry = $http_request_data->code . " " . $http_request_data->status_message . " " . $url;
   
file_put_contents('ic_preset_log.txt', $log_entry . "\n", FILE_APPEND);
 
  }

}
?>

I executed this script on the shell using the following drush command:

drush scr generate_presets.php

The script logs every request and can be reviewed afterward..

Sometimes there's no escaping hard coded hostnames being stored in your Drupal database. If you have ever setup language translations (i18n) in production, and then deployed the site to a development environment (with a different hostname), you'll know exactly what I am talking about. The first thing you'll notice in your dev site is that all your urls are linking back to the production site, and you may even have a hard time logging in.

If you query the languages table, you'll see the following:

select language, domain
from {languages}

Results:

language	domain
de		http://de.ericlondon.com
en		http://www.ericlondon.com
es		http://es.ericlondon.com
fr		http://fr.ericlondon.com

At first I thought it would make sense to write a module or a hook_update_n() function to resolve this problem when deploying to another environment, but this method seemed rather clunky, repeatedly running the same revision of a module's update script. In the end, I decided to simply write a PHP script to run via Drush. The following code fetches your default language and all records in the language table, loops through them, and updates the hostname records to match your new hostname...

<?php
// ensure this script is being called from Drush ONLY
// ensure HTTP_HOST is being used
if (
  !
is_array($_SERVER['argv'])
  ||
substr($_SERVER['argv'][0], -9) != 'drush.php'
 
|| substr($_SERVER['SCRIPT_FILENAME'], -9) != 'drush.php'
 
|| !strlen($_SERVER['HTTP_HOST'])
) {
 
header('HTTP/1.1 403 Forbidden');
  die(
'Access Denied.');
}

$output = "";

// fetch hostname from globals
$http_host = $_SERVER['HTTP_HOST'];

// ensure http host exists
if (!$http_host || !is_string($http_host)) {
  die(
'Error occurred fetching http host.');
}

// set default http host
$default_http_host = 'www.' . $http_host;

// define default language
$default_language = 'en';

// define SQL to fetch default language
$sql = "
  select *
  from {languages}
  where language = '%s'
"
;

// execute SQL
$default_language_record = db_fetch_object(db_query($sql, $default_language));

// ensure record exists
if (!is_object($default_language_record)) {
  die(
'Error occurred fetching default language.');
}

// update default language record
$default_language_record->domain = 'http://' . $default_http_host;

// set new variable for "language_default"
variable_set('language_default', $default_language_record);
$output .= "Setting language_default variable to: " . $default_language_record->domain . "\n";

// fetch language records
$sql = "
  select *
  from {languages}
"
;
$resource = db_query($sql);
$language_records = array();
while (
$row = db_fetch_object($resource)) {
 
$language_records[] = $row;
}

// correct language records
foreach ($language_records as $key => $value) {

 
// determine language prefix 
 
switch ($value->language) {
    case
$default_language:
     
$prefix = 'www.';
      break;
    default:
     
$prefix = $value->language . '.';
      break;
  }

 
$new_domain = 'http://' . $prefix . $http_host;

 
$sql = "
    update {languages}
    set domain = '%s'
    where language = '%s'
  "
;
 
db_query($sql, $new_domain, $value->language);

 
$output .= "Setting language record for language: \"" . $value->language . "\" to: " . $new_domain . "\n";

}

drupal_flush_all_caches();

$output .= "Executed: drupal_flush_all_caches()\n";

$output .= "DONE.\n";

echo
$output;
?>

I dropped this script (named: fix_env.php) in the root of my Drupal site, and then executed it via Drush using the following shell command:

drush --uri=http://tdb.erl.dev scr fix_env.php

The "--uri" flag allows you to specific a hostname for multi-site environments, and the "scr" flag allows you to specify a php script to execute.

NOTE: I'm in the habit of never using sites/default, because it's a real PITA if you ever try to move your site into a multi-site configuration ^_^

After running the script via Drush, my hostname database records have been corrected to match my development environment...

select language, domain
from {languages}

Results:

language	domain
de		http://de.tdb.erl.dev
en		http://tdb.erl.dev
es		http://es.tdb.erl.dev
fr		http://fr.tdb.erl.dev
Eric.London's picture

Drush is a great command line module for administering a Drupal site. It's core features are listed here. Check out the README.txt for usage and installation documentation.

Drush can be used to setup a base Drupal installation with a few quick commands, awesome. One of it's biggest time saving features is downloading all the contributed modules when setting up a new site. Here are a few commands I use to setup a new Drupal site based on feature sets. The following commands should be run in the root directory of your drupal installation.

# Download development modules and themes:
drush dl admin admin_menu coder devel devel_themer reroute_email simpletest views_bulk_operations --destination=sites/all/modules/contrib/ --uri=http://yourhostname.com

# Download common/essential modules:
drush dl cck views date wysiwyg pathauto token captcha recaptcha location emfield link jquery_ui webform htmlpurifier luceneapi vertical_tabs --destination=sites/all/modules/contrib/ --uri=http://yourhostname.com

# Download SEO modules:
drush dl google_analytics xmlsitemap globalredirect site_verify --destination=sites/all/modules/contrib/ --uri=http://yourhostname.com

# Download structural/building modules:
drush dl context features ctools panels --destination=sites/all/modules/contrib/ --uri=http://yourhostname.com

# Download image handling modules:
drush dl imageapi transliteration filefield mimedetect imagefield imagecache imagecache_actions --destination=sites/all/modules/contrib/ --uri=http://yourhostname.com

NOTE: the destination and uri flags on the drush command are optional, but I recommend using them. In recent Drupal development I stopped using sites/default in favor of sites/hostname.com, to ensure that if I ever decide to move the site into a multi-site configuration, I will not have overlapping sites/default/files directories.

You can even enable the downloaded modules from the command line by using the "drush en" command..

drush help en

When I got my new MacBook Pro, I installed Xcode which comes with subversion (version 1.6.x):

$ which svn
/usr/bin/svn

$ /usr/bin/svn --version | head -1
svn, version 1.6.2 (r37639)

After installing Xcode I checked out some repositories to my local filesystem. Soon afterward, I realized I needed to be running an older subversion client to stay compatible with some 1.5.x repositories, so I decided to install CollabNet's OSX subversion binary (registration is required for older releases).

After downloading and installing the package (which defaults to /opt/subversion/bin/svn), I edited the /etc/profile file to override priority of my $PATH variable (since I now had 2 versions of subversion installed):

# lines added to /etc/profile:
export PATH=/opt/subversion/bin:$PATH

Now, if I ran a "which" command for svn, the appropriate svn executable is returned:

$ which svn
/opt/subversion/bin/svn

Unfortunately, the subversion projects I checked out using Xcode's subversion were inaccessible due to differences in the .svn structure:

$ cd /path/to/my/1.6.x/repo

$ svn stat
svn: This client is too old to work with working copy '.'.  You need
to get a newer Subversion client, or to downgrade this working copy.
See http://subversion.tigris.org/faq.html#working-copy-format-change
for details.

Luckily, CollabNet has a downloadable python script which allows you to switch checked out repositories to different versions. I downloaded this file and copied it into /opt/subversion/bin.

I was now able to update/downgrade my repositories using this python script:

$ svn --version | head -1
svn, version 1.5.7 (r36142)

$ cd /path/to/my/1.6.x/repo

$ change-svn-wc-format.py . 1.5
Converted WC at '.' into format 9 for Subversion 1.5

Syndicate content