UltraBob
UltraBob

Reputation: 220

How can I temporarily change the time in my ddev web container (faketime, libfaketime)

I’m trying to debug something that happens a few days after another event, and would like to speed up the test without circumventing the date delay related code. Is there anyway to change the date in the ddev containers temporarily?

Upvotes: 2

Views: 719

Answers (3)

Marco von Frieling
Marco von Frieling

Reputation: 31

The answer from @stasadev works for plain PHP but not for Drupal. This is because of how Drupal stores generated PHP code using an mtime protected file storage which seems to be incompatible with faketime.so, as well as some minor config requirements.

The documentation of Drupal\Component\PhpStorage\MTimeProtectedFileStorage says:

Each file is stored in its own unique containing directory. The hash is based on the virtual file name, the containing directory's mtime, and a cryptographically hard to guess secret string. Thus, even if the hashed file name is discovered and replaced by an untrusted file (e.g., via a move_uploaded_file() invocation by a script that performs insufficient validation), the directory's mtime gets updated in the process, invalidating the hash and preventing the untrusted file from getting loaded. Also, the file mtime will be checked providing security against overwriting in-place, at the cost of an additional system call for every load() and exists().

The PHP function utilized is filemtime and it looks that it isn't compatible with faketime.so.

Customize DDEV Dockerfile

Use this .ddev/web-build/Dockerfile (based on this SO answer, amd64 only):

COPY --from=trajano/ubuntu-faketime /faketime.so /lib/faketime.so
ENV LD_PRELOAD=/lib/faketime.so DONT_FAKE_MONOTONIC=1 \
  FAKETIME_DONT_RESET=1

There is a small difference to the Dockerfile in @stasadev's answer, because we must set another parameter (FAKETIME_DONT_RESET=1) to make sure all processes (including sub-processes) use the same time. Without this parameter each (sub-) process would start with the defined time again which makes no sense for a web application and could lead to problems.

Replace the PHP file storage

We can solve this by replacing the mtime protected file storage with a normal file storage. Be aware that this is insecure and therefore should only be used locally in the DDEV project but NOT on production!!!

Apply the following patch to Drupal Core manually (not using Composer patches or similar which persists in git):

diff --git a/core/lib/Drupal/Core/PhpStorage/PhpStorageFactory.php b/core/lib/Drupal/Core/PhpStorage/PhpStorageFactory.>
index 85ca11bb63..5d5801feae 100644
--- a/core/lib/Drupal/Core/PhpStorage/PhpStorageFactory.php
+++ b/core/lib/Drupal/Core/PhpStorage/PhpStorageFactory.php
@@ -38,7 +38,7 @@ public static function get($name) {
       $configuration = $overrides['default'];
     }
     // Make sure all the necessary configuration values are set.
-    $class = $configuration['class'] ?? 'Drupal\Component\PhpStorage\MTimeProtectedFileStorage';
+    $class = $configuration['class'] ?? 'Drupal\Component\PhpStorage\FileStorage';
     if (!isset($configuration['secret'])) {
       $configuration['secret'] = Settings::getHashSalt();
     }

(Patch created for 10.2)

Next you have to delete the PHP file storage rm -rf [public://php] which usually is located in web/sites/default/files/php: (replace [public://php] with the real path as the shell cannot resolve this PHP stream wrapper) to remove all mtime protected generated code. Then clear the Drupal caches (drush cr).

Clean up

After you're done debugging your site with faketime you should disable the Dockerfile customization (i.e. by renaming the file), remove the above patch, delete the PHP file storage again and then clear the Drupal caches.

Removing the PHP file storage after applying/removing the patch is necessary because both implementations generate the file paths differently:

  • the normal file storage generates a file php/[machine_name] (without PHP extension)
  • the mtime protected file storage generates php/[machine_name]/ as folder and inside it a file with a cryptographic name.

Configure faked time

Use FAKETIME with DDEV config:

ddev config --web-environment-add 'FAKETIME=[timestamp]'

There are two ways to define [timestamp]:

  • 2020-01-01 01:00:00 sets the time of the DDEV web container statically to the defined time. This means the clock doesn't count and every time you execute code requesting the current time from the system you will get exactly this timestamp. This is not very useful for Drupal or other websites and apps.
  • @2020-01-01 01:00:00 (note the starting @) sets the time and starts the clock, which is the expected behavior of a (web) server.

Notes

Be aware that this only fakes the time of the DDEV web container but neither the database container nor other systems. This can cause strange problems/results if you for example have SQL queries using the current server time (CURDATE(), CURRENT_DATE(), CURRENT_TIME(), NOW(), UNIX_TIMESTAMP(), UTC_DATE(), UTC_TIME(), UTC_TIMESTAMP() etc.) or use a JavaScript provided (browser time) timestamp in comparison with a server generated timestamp either on the client or the server!

Upvotes: 3

stasadev
stasadev

Reputation: 157

If another answer gives an error on ddev restart and you see this in ddev logs:

libfaketime: In ft_shm_init(), sem_open failed and recreation attempts failed: No such file or directory
libfaketime: sem_name was /faketime_sem_136, created locally: false

Try this .ddev/web-build/Dockerfile (based on this SO answer, amd64 only):

COPY --from=trajano/ubuntu-faketime /faketime.so /lib/faketime.so
ENV LD_PRELOAD=/lib/faketime.so DONT_FAKE_MONOTONIC=1

And use FAKETIME with DDEV config:

ddev config --web-environment-add 'FAKETIME=2020-01-01 01:00:00'

Upvotes: 1

UltraBob
UltraBob

Reputation: 220

Thanks to help from this question and kind support from @rfay. I came up with this solution using the libfaketime

I created .ddev/web-build/Dockerfile with the following content:

ARG BASE_IMAGE
FROM $BASE_IMAGE
RUN apt-get update && apt-get install -y make build-essential
WORKDIR /
RUN git clone https://github.com/wolfcw/libfaketime.git
WORKDIR /libfaketime/src
RUN make install

I ran ddev start to make sure the container would still build.

Then added a command at .ddev/commands/host/faketime:

#!/bin/bash

## Description: Enable or disable faketime
## Usage: faketime on YYYY-mm-dd|off|status
## Example: "ddev faketime on 2020-05-04", "ddev faketime off", "ddev faketime status"

if [ $# -eq 0 ] ; then
 echo "usage faketime YYYY-mm-dd or faketime off"
fi

case $1 in
    on|true|enable)
    echo $1
    echo $2
    echo "turning on"
      if date -j -f "%Y-%m-%d" -j ${2} > /dev/null 2>&1
      then
        echo "time set to ${2} 11:00:00, restarting..."
      ddev config --web-environment=LD_PRELOAD="/usr/local/lib/faketime/libfaketime.so.1",FAKETIME="${2} 11:00:00" && ddev start
    else
      echo "faketime on usage:  ddev faketime on YYYY-MM-DD"
    fi
    ;;
    off|false|disable)
      echo "turning faketime off"
      ddev config --web-environment=LD_PRELOAD="" && ddev start
    ;;
    status)
      if grep -q 'FAKETIME' ${DDEV_APPROOT}/.ddev/config.yaml;
      then
        echo "faketime is on."
        ddev exec date +%F
      else
        echo "faketime is off."
        ddev exec date +%F
      fi
    ;;
  *)
    echo "invalid argument"
    ;;
esac

On a Mac, this allows me to run ddev faketime on 2020-05-4 to set the container date to May 4th, 2020, and ddev faketime off to turn it back off. On a unix based system, the date validation part of the script would need to be different.

if date -j -f "%Y-%m-%d" -j ${2} > /dev/null 2>&1;

would need to be something like

if date "+%Y-%m-%d" -d ${2} 2>&1;

faketime off and faketime on will both cause the containers to restart, while faketime status will just read whether the time is being faked and report the current container date.

For my purposes, setting the time to 11 AM was fine. I just cared about the date itself. For your application you may want to change the arguments and specify time as well.

Upvotes: 1

Related Questions