base.inc 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015
  1. <?php
  2. /**
  3. * @file
  4. * Defines the base class for migration processes.
  5. */
  6. /**
  7. * The base class for all objects representing distinct steps in a migration
  8. * process. Most commonly these will be Migration objects which actually import
  9. * data from a source into a Drupal destination, but by deriving classes directly
  10. * from MigrationBase one can have other sorts of tasks (e.g., enabling/disabling
  11. * of modules) occur during the migration process.
  12. */
  13. abstract class MigrationBase {
  14. /**
  15. * Track the migration currently running, so handlers can easily determine it
  16. * without having to pass a Migration object everywhere.
  17. *
  18. * @var Migration
  19. */
  20. protected static $currentMigration;
  21. public static function currentMigration() {
  22. return self::$currentMigration;
  23. }
  24. /**
  25. * The machine name of this Migration object, derived by removing the 'Migration'
  26. * suffix from the class name. Used to construct default map/message table names,
  27. * displayed in drush migrate-status, key to migrate_status table...
  28. *
  29. * @var string
  30. */
  31. protected $machineName;
  32. public function getMachineName() {
  33. return $this->machineName;
  34. }
  35. /**
  36. * The name of a migration group, used to collect related migrations.
  37. *
  38. * @var string
  39. */
  40. protected $group;
  41. public function getGroup() {
  42. return $this->group;
  43. }
  44. /**
  45. * Detailed information describing the migration.
  46. *
  47. * @var string
  48. */
  49. protected $description;
  50. public function getDescription() {
  51. return $this->description;
  52. }
  53. /**
  54. * Save options passed to current operation
  55. * @var array
  56. */
  57. protected $options;
  58. public function getOption($option_name) {
  59. if (isset($this->options[$option_name])) {
  60. return $this->options[$option_name];
  61. }
  62. else {
  63. return NULL;
  64. }
  65. }
  66. public function getItemLimit() {
  67. if (isset($this->options['limit']) &&
  68. ($this->options['limit']['unit'] == 'items' || $this->options['limit']['unit'] == 'item')) {
  69. return $this->options['limit']['value'];
  70. }
  71. else {
  72. return NULL;
  73. }
  74. }
  75. public function getTimeLimit() {
  76. if (isset($this->options['limit']) &&
  77. ($this->options['limit']['unit'] == 'seconds' || $this->options['limit']['unit'] == 'second')) {
  78. return $this->options['limit']['value'];
  79. }
  80. else {
  81. return NULL;
  82. }
  83. }
  84. /**
  85. * Indicates that we are processing a rollback or import - used to avoid
  86. * excess writes in endProcess()
  87. *
  88. * @var boolean
  89. */
  90. protected $processing = FALSE;
  91. /**
  92. * Are we importing, rolling back, or doing nothing?
  93. *
  94. * @var enum
  95. */
  96. protected $status = MigrationBase::STATUS_IDLE;
  97. /**
  98. * When the current operation started.
  99. * @var int
  100. */
  101. protected $starttime;
  102. /**
  103. * Whether to maintain a history of migration processes in migrate_log
  104. *
  105. * @var boolean
  106. */
  107. protected $logHistory = TRUE;
  108. /**
  109. * Primary key of the current history record (inserted at the beginning of
  110. * a process, to be updated at the end)
  111. *
  112. * @var int
  113. */
  114. protected $logID;
  115. /**
  116. * Number of "items" processed in the current migration process (whatever that
  117. * means for the type of process)
  118. *
  119. * @var int
  120. */
  121. protected $total_processed = 0;
  122. /**
  123. * List of other Migration classes which should be imported before this one.
  124. * E.g., a comment migration class would typically have node and user migrations
  125. * as dependencies.
  126. *
  127. * @var array
  128. */
  129. protected $dependencies = array(), $softDependencies = array();
  130. public function getHardDependencies() {
  131. return $this->dependencies;
  132. }
  133. public function getSoftDependencies() {
  134. return $this->softDependencies;
  135. }
  136. public function getDependencies() {
  137. return array_merge($this->dependencies, $this->softDependencies);
  138. }
  139. /**
  140. * Name of a function for displaying feedback. It must take the message to display
  141. * as its first argument, and a (string) message type as its second argument
  142. * (see drush_log()).
  143. * @var string
  144. */
  145. protected static $displayFunction;
  146. public static function setDisplayFunction($display_function) {
  147. self::$displayFunction = $display_function;
  148. }
  149. /**
  150. * The fraction of the memory limit at which an operation will be interrupted.
  151. * Can be overridden by a Migration subclass if one would like to push the
  152. * envelope. Defaults to 85%.
  153. *
  154. * @var float
  155. */
  156. protected $memoryThreshold = 0.85;
  157. /**
  158. * The PHP memory_limit expressed in bytes.
  159. *
  160. * @var int
  161. */
  162. protected $memoryLimit;
  163. /**
  164. * The fraction of the time limit at which an operation will be interrupted.
  165. * Can be overridden by a Migration subclass if one would like to push the
  166. * envelope. Defaults to 90%.
  167. *
  168. * @var float
  169. */
  170. protected $timeThreshold = 0.90;
  171. /**
  172. * The PHP max_execution_time.
  173. *
  174. * @var int
  175. */
  176. protected $timeLimit;
  177. /**
  178. * MigrateTeamMember objects representing people involved with this
  179. * migration.
  180. *
  181. * @var array
  182. */
  183. protected $team = array();
  184. public function getTeam() {
  185. return $this->team;
  186. }
  187. /**
  188. * If provided, an URL for an issue tracking system containing :id where
  189. * the issue number will go (e.g., 'http://example.com/project/ticket/:id').
  190. *
  191. * @var string
  192. */
  193. protected $issuePattern;
  194. public function getIssuePattern() {
  195. return $this->issuePattern;
  196. }
  197. /**
  198. * If we set an error handler (during import), remember the previous one so
  199. * it can be restored.
  200. *
  201. * @var callback
  202. */
  203. protected $previousErrorHandler = NULL;
  204. /**
  205. * Disabling a migration prevents it from running with --all, or individually
  206. * without --force
  207. *
  208. * @var boolean
  209. */
  210. protected $enabled = TRUE;
  211. public function getEnabled() {
  212. return $this->enabled;
  213. }
  214. /**
  215. * Codes representing the result of a rollback or import process.
  216. */
  217. const RESULT_COMPLETED = 1; // All records have been processed
  218. const RESULT_INCOMPLETE = 2; // The process has interrupted itself (e.g., the
  219. // memory limit is approaching)
  220. const RESULT_STOPPED = 3; // The process was stopped externally (e.g., via
  221. // drush migrate-stop)
  222. const RESULT_FAILED = 4; // The process had a fatal error
  223. const RESULT_SKIPPED = 5; // Dependencies are unfulfilled - skip the process
  224. const RESULT_DISABLED = 6; // This migration is disabled, skipping
  225. /**
  226. * Codes representing the current status of a migration, and stored in the
  227. * migrate_status table.
  228. */
  229. const STATUS_IDLE = 0;
  230. const STATUS_IMPORTING = 1;
  231. const STATUS_ROLLING_BACK = 2;
  232. const STATUS_STOPPING = 3;
  233. const STATUS_DISABLED = 4;
  234. /**
  235. * Message types to be passed to saveMessage() and saved in message tables.
  236. * MESSAGE_INFORMATIONAL represents a condition that did not prevent the operation
  237. * from succeeding - all others represent different severities of conditions
  238. * resulting in a source record not being imported.
  239. */
  240. const MESSAGE_ERROR = 1;
  241. const MESSAGE_WARNING = 2;
  242. const MESSAGE_NOTICE = 3;
  243. const MESSAGE_INFORMATIONAL = 4;
  244. /**
  245. * Get human readable name for a message constant.
  246. *
  247. * @return string
  248. * Name.
  249. */
  250. public function getMessageLevelName($constant) {
  251. $map = array(
  252. MigrationBase::MESSAGE_ERROR => t('Error'),
  253. MigrationBase::MESSAGE_WARNING => t('Warning'),
  254. MigrationBase::MESSAGE_NOTICE => t('Notice'),
  255. MigrationBase::MESSAGE_INFORMATIONAL => t('Informational'),
  256. );
  257. return $map[$constant];
  258. }
  259. /**
  260. * General initialization of a MigrationBase object.
  261. */
  262. public function __construct($group = NULL) {
  263. $this->machineName = $this->generateMachineName();
  264. if (empty($group)) {
  265. $this->group = MigrateGroup::getInstance('default');
  266. }
  267. else {
  268. $this->group = $group;
  269. }
  270. // Record the memory limit in bytes
  271. $limit = trim(ini_get('memory_limit'));
  272. if ($limit == '-1') {
  273. $this->memoryLimit = PHP_INT_MAX;
  274. }
  275. else {
  276. if (!is_numeric($limit)) {
  277. $last = drupal_strtolower($limit[strlen($limit)-1]);
  278. switch ($last) {
  279. case 'g':
  280. $limit *= 1024;
  281. case 'm':
  282. $limit *= 1024;
  283. case 'k':
  284. $limit *= 1024;
  285. break;
  286. default:
  287. throw new Exception(t('Invalid PHP memory_limit !limit',
  288. array('!limit' => $limit)));
  289. }
  290. }
  291. $this->memoryLimit = $limit;
  292. }
  293. // Record the time limit
  294. $this->timeLimit = ini_get('max_execution_time');
  295. // Prevent any emails from being sent out on migration
  296. global $conf;
  297. if (!empty($conf['mail_system'])) {
  298. foreach ($conf['mail_system'] as $system => $class) {
  299. $conf['mail_system'][$system] = 'MigrateMailIgnore';
  300. }
  301. }
  302. // Make sure we clear our semaphores in case of abrupt exit
  303. register_shutdown_function(array($this, 'endProcess'));
  304. }
  305. /**
  306. * Initialize static members, before any class instances are created.
  307. */
  308. static public function staticInitialize() {
  309. // Default the displayFunction outputFunction based on context
  310. if (function_exists('drush_log')) {
  311. self::$displayFunction = 'drush_log';
  312. }
  313. else {
  314. self::$displayFunction = 'drupal_set_message';
  315. }
  316. }
  317. /**
  318. * Register a new migration process in the migrate_status table. This will
  319. * generally be used in two contexts - by the class detection code for
  320. * static (one instance per class) migrations, and by the module implementing
  321. * dynamic (parameterized class) migrations.
  322. *
  323. * @param string $class_name
  324. * @param string $machine_name
  325. * @param array $arguments
  326. */
  327. static public function registerMigration($class_name, $machine_name = NULL, array $arguments = array()) {
  328. if (!$machine_name) {
  329. $machine_name = self::machineFromClass($class_name);
  330. }
  331. // Register the migration if it's not already there; if it is,
  332. // update the class and arguments in case they've changed.
  333. db_merge('migrate_status')
  334. ->key(array('machine_name' => $machine_name))
  335. ->fields(array(
  336. 'class_name' => $class_name,
  337. 'arguments' => serialize($arguments)
  338. ))
  339. ->execute();
  340. }
  341. /**
  342. * Deregister a migration - remove all traces of it from the database (without
  343. * touching any content which was created by this migration).
  344. *
  345. * @param string $machine_name
  346. */
  347. static public function deregisterMigration($machine_name) {
  348. $rows_deleted = db_delete('migrate_status')
  349. ->condition('machine_name', $machine_name)
  350. ->execute();
  351. }
  352. /**
  353. * By default, the migration machine name is the class name (with the
  354. * Migration suffix, if present, stripped).
  355. */
  356. protected function generateMachineName() {
  357. $class_name = get_class($this);
  358. return self::machineFromClass($class_name);
  359. }
  360. protected static function machineFromClass($class_name) {
  361. if (preg_match('/Migration$/', $class_name)) {
  362. $machine_name = drupal_substr($class_name, 0,
  363. strlen($class_name) - strlen('Migration'));
  364. }
  365. else {
  366. $machine_name = $class_name;
  367. }
  368. return $machine_name;
  369. }
  370. /**
  371. * Return the single instance of the given migration.
  372. *
  373. * @param string $machine_name
  374. */
  375. static public function getInstance($machine_name, $class_name = NULL, array $arguments = array()) {
  376. $migrations = &drupal_static(__FUNCTION__, array());
  377. // Otherwise might miss cache hit on case difference
  378. $machine_name_key = drupal_strtolower($machine_name);
  379. if (!isset($migrations[$machine_name_key])) {
  380. // Skip the query if our caller already made it
  381. if (!$class_name) {
  382. // See if we know about this migration
  383. $row = db_select('migrate_status', 'ms')
  384. ->fields('ms', array('class_name', 'arguments'))
  385. ->condition('machine_name', $machine_name)
  386. ->execute()
  387. ->fetchObject();
  388. if ($row) {
  389. $class_name = $row->class_name;
  390. $arguments = unserialize($row->arguments);
  391. }
  392. else {
  393. // Can't find a migration with this name
  394. throw new MigrateException(t('No migration found with machine name !machine',
  395. array('!machine' => $machine_name)));
  396. }
  397. }
  398. $migrations[$machine_name_key] = new $class_name($arguments);
  399. }
  400. return $migrations[$machine_name_key];
  401. }
  402. /**
  403. * Identifies whether this migration is "dynamic" (that is, allows multiple
  404. * instances distinguished by differing parameters). A dynamic class should
  405. * override this with a return value of TRUE.
  406. */
  407. static public function isDynamic() {
  408. return FALSE;
  409. }
  410. /**
  411. * Default to printing messages, but derived classes are expected to save
  412. * messages indexed by current source ID.
  413. *
  414. * @param string $message
  415. * The message to record.
  416. * @param int $level
  417. * Optional message severity (defaults to MESSAGE_ERROR).
  418. */
  419. public function saveMessage($message, $level = MigrationBase::MESSAGE_ERROR) {
  420. switch ($level) {
  421. case MigrationBase::MESSAGE_ERROR:
  422. $level = 'error';
  423. break;
  424. case MigrationBase::MESSAGE_WARNING:
  425. $level = 'warning';
  426. break;
  427. case MigrationBase::MESSAGE_NOTICE:
  428. $level = 'notice';
  429. break;
  430. case MigrationBase::MESSAGE_INFORMATIONAL:
  431. $level = 'status';
  432. break;
  433. }
  434. self::displayMessage($message, $level);
  435. }
  436. /**
  437. * Output the given message appropriately (drush_print/drupal_set_message/etc.)
  438. *
  439. * @param string $message
  440. * The message to output.
  441. * @param int $level
  442. * Optional message severity as understood by drupal_set_message and drush_log
  443. * (defaults to 'error').
  444. */
  445. static public function displayMessage($message, $level = 'error') {
  446. call_user_func(self::$displayFunction, $message, $level);
  447. }
  448. /**
  449. * Custom PHP error handler.
  450. * TODO: Redundant with hook_watchdog?
  451. *
  452. * @param $error_level
  453. * The level of the error raised.
  454. * @param $message
  455. * The error message.
  456. * @param $filename
  457. * The filename that the error was raised in.
  458. * @param $line
  459. * The line number the error was raised at.
  460. * @param $context
  461. * An array that points to the active symbol table at the point the error occurred.
  462. */
  463. public function errorHandler($error_level, $message, $filename, $line, $context) {
  464. if ($error_level & error_reporting()) {
  465. $message .= "\n" . t('File !file, line !line',
  466. array('!line' => $line, '!file' => $filename));
  467. // Record notices and continue
  468. if ($error_level == E_NOTICE || $error_level == E_USER_NOTICE) {
  469. $this->saveMessage($message . "(file: $filename, line $line)", MigrationBase::MESSAGE_INFORMATIONAL);
  470. }
  471. // Simply ignore strict and deprecated errors
  472. // Note DEPRECATED constants introduced in PHP 5.3
  473. elseif (!($error_level == E_STRICT || $error_level == 8192 ||
  474. $error_level == 16384)) {
  475. throw new MigrateException($message, MigrationBase::MESSAGE_ERROR);
  476. }
  477. }
  478. }
  479. /**
  480. * Takes an Exception object and both saves and displays it, pulling additional
  481. * information on the location triggering the exception.
  482. *
  483. * @param Exception $exception
  484. * Object representing the exception.
  485. * @param boolean $save
  486. * Whether to save the message in the migration's mapping table. Set to FALSE
  487. * in contexts where this doesn't make sense.
  488. */
  489. public function handleException($exception, $save = TRUE) {
  490. $result = _drupal_decode_exception($exception);
  491. $message = $result['!message'] . ' (' . $result['%file'] . ':' . $result['%line'] . ')';
  492. if ($save) {
  493. $this->saveMessage($message);
  494. }
  495. self::displayMessage($message);
  496. }
  497. /**
  498. * Check the current status of a migration.
  499. * @return int
  500. * One of the MigrationBase::STATUS_* constants
  501. */
  502. public function getStatus() {
  503. if (!$this->enabled) {
  504. return MigrationBase::STATUS_DISABLED;
  505. }
  506. $status = db_select('migrate_status', 'ms')
  507. ->fields('ms', array('status'))
  508. ->condition('machine_name', $this->machineName)
  509. ->execute()
  510. ->fetchField();
  511. if (!isset($status)) {
  512. $status = MigrationBase::STATUS_IDLE;
  513. }
  514. return $status;
  515. }
  516. /**
  517. * Retrieve the last time an import operation completed successfully.
  518. * @return string
  519. * Date/time string, formatted... How? Default DB server format?
  520. */
  521. public function getLastImported() {
  522. $last_imported = db_select('migrate_log', 'ml')
  523. ->fields('ml', array('endtime'))
  524. ->condition('machine_name', $this->machineName)
  525. ->isNotNull('endtime')
  526. ->orderBy('endtime', 'DESC')
  527. ->execute()
  528. ->fetchField();
  529. if ($last_imported) {
  530. $last_imported = date('Y-m-d H:i:s', $last_imported/1000);
  531. }
  532. else {
  533. $last_imported = '';
  534. }
  535. return $last_imported;
  536. }
  537. /**
  538. * Fetch the current highwater mark for updated content.
  539. *
  540. * @return string
  541. * The highwater mark.
  542. */
  543. public function getHighwater() {
  544. $highwater = db_select('migrate_status', 'ms')
  545. ->fields('ms', array('highwater'))
  546. ->condition('machine_name', $this->machineName)
  547. ->execute()
  548. ->fetchField();
  549. return $highwater;
  550. }
  551. /**
  552. * Save the highwater mark for this migration (but not when using an idlist).
  553. *
  554. * @param mixed $highwater
  555. * Highwater mark to save
  556. * @param boolean $force
  557. * If TRUE, save even if it's lower than the previous value.
  558. */
  559. protected function saveHighwater($highwater, $force = FALSE) {
  560. if (!isset($this->options['idlist'])) {
  561. $query = db_update('migrate_status')
  562. ->fields(array('highwater' => $highwater))
  563. ->condition('machine_name', $this->machineName);
  564. if (!$force) {
  565. if (!empty($this->highwaterField['type']) && $this->highwaterField['type'] == 'int') {
  566. // If the highwater is an integer type, we need to force the DB server
  567. // to treat the varchar highwater field as an integer (otherwise it will
  568. // think '5' > '10'). CAST(highwater AS INTEGER) would be ideal, but won't
  569. // work in MySQL. This hack is thought to be portable.
  570. $query->where('(highwater+0) < :highwater', array(':highwater' => $highwater));
  571. }
  572. else {
  573. $query->condition('highwater', $highwater, '<');
  574. }
  575. }
  576. $query->execute();
  577. }
  578. }
  579. /**
  580. * Retrieve the last throughput for current Migration (items / minute).
  581. * @return integer
  582. */
  583. public function getLastThroughput() {
  584. $last_throughput = 0;
  585. $row = db_select('migrate_log', 'ml')
  586. ->fields('ml', array('starttime', 'endtime', 'numprocessed'))
  587. ->condition('machine_name', $this->machineName)
  588. ->condition('process_type', 1)
  589. ->isNotNull('endtime')
  590. ->orderBy('starttime', 'DESC')
  591. ->execute()
  592. ->fetchObject();
  593. if ($row) {
  594. $elapsed = ($row->endtime - $row->starttime)/1000;
  595. if ($elapsed > 0) {
  596. $last_throughput = round(($row->numprocessed / $elapsed) * 60);
  597. }
  598. }
  599. return $last_throughput;
  600. }
  601. /**
  602. * Reports whether this migration process is complete. For a Migration, for
  603. * example, this would be whether all available source rows have been processed.
  604. * Other MigrationBase classes will need to return TRUE/FALSE appropriately.
  605. */
  606. abstract public function isComplete();
  607. /**
  608. * Reports whether all (hard) dependencies have completed migration
  609. */
  610. protected function dependenciesComplete($rollback = FALSE) {
  611. if ($rollback) {
  612. foreach (migrate_migrations() as $migration) {
  613. $dependencies = $migration->getHardDependencies();
  614. if (array_search($this->machineName, $dependencies) !== FALSE) {
  615. if (method_exists($migration, 'importedCount') && $migration->importedCount() > 0) {
  616. return FALSE;
  617. }
  618. }
  619. }
  620. }
  621. else {
  622. foreach ($this->dependencies as $dependency) {
  623. $migration = MigrationBase::getInstance($dependency);
  624. if (!$migration->isComplete()) {
  625. return FALSE;
  626. }
  627. }
  628. }
  629. return TRUE;
  630. }
  631. /**
  632. * Returns an array of the migration's dependencies that are incomplete.
  633. */
  634. public function incompleteDependencies() {
  635. $incomplete = array();
  636. foreach ($this->getDependencies() as $dependency) {
  637. $migration = MigrationBase::getInstance($dependency);
  638. if (!$migration->isComplete()) {
  639. $incomplete[] = $dependency;
  640. }
  641. }
  642. return $incomplete;
  643. }
  644. /**
  645. * Begin a process, ensuring only one process can be active
  646. * at once on a given migration.
  647. *
  648. * @param int $newStatus
  649. * MigrationBase::STATUS_IMPORTING or MigrationBase::STATUS_ROLLING_BACK
  650. */
  651. protected function beginProcess($newStatus) {
  652. // So hook_watchdog() knows what migration (if any) is running
  653. self::$currentMigration = $this;
  654. // Try to make the semaphore handling atomic (depends on DB support)
  655. $transaction = db_transaction();
  656. $this->starttime = microtime(TRUE);
  657. // Check to make sure there's no process already running for this migration
  658. $status = $this->getStatus();
  659. if ($status != MigrationBase::STATUS_IDLE) {
  660. throw new MigrateException(t('There is already an active process on !machine_name',
  661. array('!machine_name' => $this->machineName)));
  662. }
  663. $this->processing = TRUE;
  664. $this->status = $newStatus;
  665. db_merge('migrate_status')
  666. ->key(array('machine_name' => $this->machineName))
  667. ->fields(array('class_name' => get_class($this), 'status' => $newStatus))
  668. ->execute();
  669. // Set an error handler for imports
  670. if ($newStatus == MigrationBase::STATUS_IMPORTING) {
  671. $this->previousErrorHandler = set_error_handler(array($this, 'errorHandler'));
  672. }
  673. // Save the initial history record
  674. if ($this->logHistory) {
  675. $this->logID = db_insert('migrate_log')
  676. ->fields(array(
  677. 'machine_name' => $this->machineName,
  678. 'process_type' => $newStatus,
  679. 'starttime' => round(microtime(TRUE) * 1000),
  680. 'initialHighwater' => $this->getHighwater(),
  681. ))
  682. ->execute();
  683. }
  684. }
  685. /**
  686. * End a rollback or import process, releasing the semaphore. Note that it must
  687. * be public to be callable as the shutdown function.
  688. */
  689. public function endProcess() {
  690. if ($this->previousErrorHandler) {
  691. set_error_handler($this->previousErrorHandler);
  692. $this->previousErrorHandler = NULL;
  693. }
  694. if ($this->processing) {
  695. $this->status = MigrationBase::STATUS_IDLE;
  696. $fields = array('class_name' => get_class($this), 'status' => MigrationBase::STATUS_IDLE);
  697. db_merge('migrate_status')
  698. ->key(array('machine_name' => $this->machineName))
  699. ->fields($fields)
  700. ->execute();
  701. // Complete the log record
  702. if ($this->logHistory) {
  703. try {
  704. db_merge('migrate_log')
  705. ->key(array('mlid' => $this->logID))
  706. ->fields(array(
  707. 'endtime' => round(microtime(TRUE) * 1000),
  708. 'finalhighwater' => $this->getHighwater(),
  709. 'numprocessed' => $this->total_processed,
  710. ))
  711. ->execute();
  712. }
  713. catch (PDOException $e) {
  714. Migration::displayMessage(t('Could not log operation on migration !name - possibly MigrationBase::beginProcess() was not called',
  715. array('!name' => $this->machineName)));
  716. }
  717. }
  718. $this->processing = FALSE;
  719. }
  720. self::$currentMigration = NULL;
  721. }
  722. /**
  723. * Signal that any current import or rollback process should end itself at
  724. * the earliest opportunity
  725. */
  726. public function stopProcess() {
  727. // Do not change the status of an idle migration
  728. db_update('migrate_status')
  729. ->fields(array('status' => MigrationBase::STATUS_STOPPING))
  730. ->condition('machine_name', $this->machineName)
  731. ->condition('status', MigrationBase::STATUS_IDLE, '<>')
  732. ->execute();
  733. }
  734. /**
  735. * Reset the status of the migration to IDLE (to be used when the status
  736. * gets stuck, e.g. if a process core-dumped)
  737. */
  738. public function resetStatus() {
  739. // Do not change the status of an already-idle migration
  740. db_update('migrate_status')
  741. ->fields(array('status' => MigrationBase::STATUS_IDLE))
  742. ->condition('machine_name', $this->machineName)
  743. ->condition('status', MigrationBase::STATUS_IDLE, '<>')
  744. ->execute();
  745. }
  746. /**
  747. * Perform an operation during the rollback phase.
  748. *
  749. * @param array $options
  750. * List of options provided (usually from a drush command). Specific to
  751. * the derived class.
  752. */
  753. public function processRollback(array $options = array()) {
  754. if ($this->enabled) {
  755. $return = MigrationBase::RESULT_COMPLETED;
  756. if (method_exists($this, 'rollback')) {
  757. $this->options = $options;
  758. if (!isset($options['force'])) {
  759. if (!$this->dependenciesComplete(TRUE)) {
  760. return MigrationBase::RESULT_SKIPPED;
  761. }
  762. }
  763. $this->beginProcess(MigrationBase::STATUS_ROLLING_BACK);
  764. try {
  765. $return = $this->rollback();
  766. }
  767. catch (Exception $exception) {
  768. // If something bad happened, make sure we clear the semaphore
  769. $this->endProcess();
  770. throw $exception;
  771. }
  772. $this->endProcess();
  773. }
  774. }
  775. else {
  776. $return = MigrationBase::RESULT_DISABLED;
  777. }
  778. return $return;
  779. }
  780. /**
  781. * Perform an operation during the import phase
  782. *
  783. * @param array $options
  784. * List of options provided (usually from a drush command). Specific to
  785. * the derived class.
  786. */
  787. public function processImport(array $options = array()) {
  788. if ($this->enabled) {
  789. $return = MigrationBase::RESULT_COMPLETED;
  790. if (method_exists($this, 'import')) {
  791. $this->options = $options;
  792. if (!isset($options['force']) || !$options['force']) {
  793. if (!$this->dependenciesComplete()) {
  794. return MigrationBase::RESULT_SKIPPED;
  795. }
  796. }
  797. $this->beginProcess(MigrationBase::STATUS_IMPORTING);
  798. try {
  799. $return = $this->import();
  800. }
  801. catch (Exception $exception) {
  802. // If something bad happened, make sure we clear the semaphore
  803. $this->endProcess();
  804. throw $exception;
  805. }
  806. if ($return == MigrationBase::RESULT_COMPLETED && isset($this->total_successes)) {
  807. $overallThroughput = round(60*$this->total_successes / (microtime(TRUE) - $this->starttime));
  808. }
  809. else {
  810. $overallThroughput = 0;
  811. }
  812. $this->endProcess($overallThroughput);
  813. }
  814. }
  815. else {
  816. $return = MigrationBase::RESULT_DISABLED;
  817. }
  818. return $return;
  819. }
  820. /**
  821. * A derived migration class does the actual rollback or import work in these
  822. * methods - we cannot declare them abstract because some classes may define
  823. * only one.
  824. *
  825. * abstract protected function rollback();
  826. * abstract protected function import();
  827. */
  828. /**
  829. * Test whether we've exceeded the desired memory threshold. If so, output a message.
  830. *
  831. * @return boolean
  832. * TRUE if the threshold is exceeded, FALSE if not.
  833. */
  834. protected function memoryExceeded() {
  835. $usage = memory_get_usage();
  836. $pct_memory = $usage/$this->memoryLimit;
  837. if ($pct_memory > $this->memoryThreshold) {
  838. self::displayMessage(
  839. t('Memory usage is !usage (!pct% of limit !limit), resetting statics',
  840. array('!pct' => round($pct_memory*100),
  841. '!usage' => format_size($usage),
  842. '!limit' => format_size($this->memoryLimit))),
  843. 'warning');
  844. // First, try resetting Drupal's static storage - this frequently releases
  845. // plenty of memory to continue
  846. drupal_static_reset();
  847. $usage = memory_get_usage();
  848. $pct_memory = $usage/$this->memoryLimit;
  849. // Use a lower threshold - we don't want to be in a situation where we keep
  850. // coming back here and trimming a tiny amount
  851. if ($pct_memory > (.90 * $this->memoryThreshold)) {
  852. self::displayMessage(
  853. t('Memory usage is now !usage (!pct% of limit !limit), not enough reclaimed, starting new batch',
  854. array('!pct' => round($pct_memory*100),
  855. '!usage' => format_size($usage),
  856. '!limit' => format_size($this->memoryLimit))),
  857. 'warning');
  858. return TRUE;
  859. }
  860. else {
  861. self::displayMessage(
  862. t('Memory usage is now !usage (!pct% of limit !limit), reclaimed enough, continuing',
  863. array('!pct' => round($pct_memory*100),
  864. '!usage' => format_size($usage),
  865. '!limit' => format_size($this->memoryLimit))),
  866. 'warning');
  867. return FALSE;
  868. }
  869. }
  870. else {
  871. return FALSE;
  872. }
  873. }
  874. /**
  875. * Test whether we're approaching the PHP time limit.
  876. *
  877. * @return boolean
  878. * TRUE if the threshold is exceeded, FALSE if not.
  879. */
  880. protected function timeExceeded() {
  881. if ($this->timeLimit == 0) {
  882. return FALSE;
  883. }
  884. $time_elapsed = time() - REQUEST_TIME;
  885. $pct_time = $time_elapsed / $this->timeLimit;
  886. if ($pct_time > $this->timeThreshold) {
  887. return TRUE;
  888. }
  889. else {
  890. return FALSE;
  891. }
  892. }
  893. /**
  894. * Test whether we've exceeded the designated time limit.
  895. *
  896. * @return boolean
  897. * TRUE if the threshold is exceeded, FALSE if not.
  898. */
  899. protected function timeOptionExceeded() {
  900. if (!$timelimit = $this->getTimeLimit()) {
  901. return FALSE;
  902. }
  903. $time_elapsed = time() - REQUEST_TIME;
  904. if ($time_elapsed >= $timelimit) {
  905. return TRUE;
  906. }
  907. else {
  908. return FALSE;
  909. }
  910. }
  911. /**
  912. * Convert an incoming string (which may be a UNIX timestamp, or an arbitrarily-formatted
  913. * date/time string) to a UNIX timestamp.
  914. *
  915. * @param string $value
  916. */
  917. static public function timestamp($value) {
  918. // Default empty values to now
  919. if (empty($value)) {
  920. return time();
  921. }
  922. // Does it look like it's already a timestamp? Just return it
  923. if (is_numeric($value)) {
  924. return $value;
  925. }
  926. $time = strtotime($value);
  927. if ($time == FALSE) {
  928. // Handles form YYYY-MM-DD HH:MM:SS.garbage
  929. if (drupal_strlen($value) > 19) {
  930. $time = strtotime(drupal_substr($value, 0, 19));
  931. }
  932. }
  933. return $time;
  934. }
  935. }
  936. // Make sure static members (in particular, $displayFunction) get
  937. // initialized even if there are no class instances.
  938. MigrationBase::staticInitialize();