diff --git a/cmd/xdebug.go b/cmd/xdebug.go index 84d6827..69583fb 100644 --- a/cmd/xdebug.go +++ b/cmd/xdebug.go @@ -1,33 +1,25 @@ package cmd import ( - "fmt" "github.com/gookit/color" "github.com/jonhadfield/findexec" "github.com/sandstorm/drydock/util" "github.com/spf13/cobra" "log" "net" - "net/http" "os" "os/exec" "os/signal" - "runtime" - "strings" "time" ) -func convertToTopLevelExport(exportedPath string) string { - return strings.ReplaceAll(exportedPath, "/", "") -} - -// phpXdebugInstallScript is running in the debugImage (NOTE: we use a different debug image here to support NFS as well. +// phpXdebugInstallScript is running in the debugImage // - we mount the inner container to /container (should be based on some base "official" Docker PHP image) // - we php-spx via Git inside nicolaka/netshoot (because we cannot know if git is installed inside the container) // - then, we compile and install php-spx inside the container. This runs as root, because we use the "execroot" mechanics // (important for the `make install` step). // - reload the config -func phpXdebugInstallScript(pid string, extraMountFolders string) string { +func phpXdebugInstallScript(pid string) string { // fall back to xdebug 3.1.6 for PHP 7.4 if xdebug 3.2 (the newest version) did not work return mountSlashContainer + ` cat << EOF | chroot /container @@ -36,6 +28,12 @@ cat << EOF | chroot /container pecl install xdebug || pecl install xdebug-3.1.6 EOF +if [ ! -d /container$PHP_INI_DIR ]; then + echo "!!!! PHP_INI_DIR not set." + echo "!!!! please set it as env in docker compose." + exit 1 +fi; + cat << EOF > /container$PHP_INI_DIR/conf.d/xdebug.ini zend_extension=xdebug.so @@ -50,31 +48,6 @@ pkill -USR2 php-fpm ` } -func phpXdebugMountScript(extraMountFolders string) string { - if len(extraMountFolders) > 0 { - var davExports []string - for _, extraMountFolder := range strings.Split(extraMountFolders, ",") { - extraMountFolder = strings.Trim(extraMountFolder, "/") - davExports = append(davExports, fmt.Sprintf("/%s,/container/%s,null,null,false", convertToTopLevelExport(extraMountFolder), extraMountFolder)) - } - - return mountSlashContainer + ` - -export HTTP_PROXY="" -export HTTPS_PROXY="" - -echo https://github.com/117503445/GoWebDAV/releases/download/1.9.0/gowebdav_linux_` + runtime.GOARCH + ` - -curl -L -o /bin/gowebdav https://github.com/117503445/GoWebDAV/releases/download/1.9.0/gowebdav_linux_` + runtime.GOARCH + ` -chmod +x /bin/gowebdav - -echo "Starting webdav server for extra mounts on ` + extraMountFolders + `" -/bin/gowebdav --dav "` + strings.Join(davExports, ";") + `" -` - } - return "" -} - func phpXdebugDeactivateScript() string { return mountSlashContainer + ` rm /container$PHP_INI_DIR/conf.d/xdebug.ini @@ -84,7 +57,6 @@ pkill -USR2 php-fpm func buildXdebugCommand() *cobra.Command { var debugImage string = "nicolaka/netshoot" - var extraMountFolders string = "" var command = &cobra.Command{ Use: "xdebug [flags] SERVICE-or-CONTAINER", @@ -97,9 +69,6 @@ the PHP Process such that the debugger is enabled. Options: --debug-image What debugger docker image to use for executing nsenter (and optionally the NFS webdav server). By default, nicolaka/netshoot is used - --mount Extra mounts which should be mounted from the container to the host via webdav. - This is useful to be able to f.e. debug into non-mounted files (like other packages - in Neos/Flow applications) Examples @@ -109,9 +78,6 @@ the PHP Process such that the debugger is enabled. Run Xdebug in a running docker-compose service drydock xdebug my-docker-compose-service -Run Xdebug a Neos/Flow Application - drydock xdebug my-docker-compose-service --mount=app/Data/Temporary - Background: This command installs the Xdebug PHP extension into an existing Docker container, even if the container is locked @@ -131,11 +97,6 @@ the PHP Process such that the debugger is enabled. color.Println("=====================================") color.Println("Installing Xdebug into the container") color.Println("and reloading PHP") - if len(extraMountFolders) > 0 { - color.Printf("Extra Mounted Folders: %s\n", extraMountFolders) - } else { - color.Printf("Extra Mounted Folders: -\n") - } color.Println("=====================================") color.Println("") @@ -174,18 +135,12 @@ the PHP Process such that the debugger is enabled. os.Exit(1) } - if len(extraMountFolders) > 0 { - // needed for WebDav Server to extraMountFolder the extra files - extraDockerRunArgs = append(extraDockerRunArgs, "-p", "19889:80") - } - // Image: https://github.com/vgist/dockerfiles/tree/master/nfs-server - // Install XDEBUG dockerRunCommand := dockerRunNsenterCommand(fullContainerName, debugImage, pid, extraDockerRunArgs) dockerRunCommand = append(dockerRunCommand, "--net") // to download files now, we need to mount the network filesystem. dockerRunCommand = append(dockerRunCommand, "/bin/bash") dockerRunCommand = append(dockerRunCommand, "-c") - dockerRunCommand = append(dockerRunCommand, phpXdebugInstallScript(pid, extraMountFolders)) + dockerRunCommand = append(dockerRunCommand, phpXdebugInstallScript(pid)) dockerRunC := exec.Command(dockerExecutablePathAndFilename, dockerRunCommand[1:]...) dockerRunC.Env = os.Environ() @@ -196,112 +151,10 @@ the PHP Process such that the debugger is enabled. c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) - if len(extraMountFolders) > 0 { - // Install XDEBUG - dockerRunCommand := dockerRunNsenterCommand(fullContainerName, debugImage, pid, extraDockerRunArgs) - // - dockerRunCommand = append(dockerRunCommand, "/bin/bash") - dockerRunCommand = append(dockerRunCommand, "-c") - dockerRunCommand = append(dockerRunCommand, phpXdebugMountScript(extraMountFolders)) - - dockerRunC := exec.Command(dockerExecutablePathAndFilename, dockerRunCommand[1:]...) - dockerRunC.Env = os.Environ() - dockerRunC.Stdout = os.Stdout - dockerRunC.Stderr = os.Stderr - dockerRunC.Start() - - err = waitUntilWebdavIsRunning() - color.Println("") - color.Println("") - color.Println("=====================================") - color.Printf("Trying to mount %s via WebDAV\n", extraMountFolders) - color.Println("=====================================") - color.Println("") - - if err != nil { - color.Printf("FATAL: Webdav not up and running: %s.\n", err) - color.Println("") - os.Exit(1) - } - // Webdav server up and running now :) so we can mount extraMountFolders. - for _, extraMountFolder := range strings.Split(extraMountFolders, ",") { - extraMountFolder = strings.Trim(extraMountFolder, "/") - stat, err := os.Stat(extraMountFolder) - folderExists := err == nil && stat.IsDir() - if !folderExists { - os.MkdirAll(extraMountFolder, 0755) - } - - mountC := exec.Command( - "mount", - "-t", - "webdav", - fmt.Sprintf("http://127.0.0.1:19889/%s", convertToTopLevelExport(extraMountFolder)), - extraMountFolder, - ) - mountC.Env = os.Environ() - mountC.Stdout = os.Stdout - mountC.Stderr = os.Stderr - err = mountC.Run() - if err != nil { - color.Printf("FATAL: Webdav mount did not succeed: %s.\n", err) - color.Println("") - os.Exit(1) - } - } - - printXdebugUsage(fullContainerName) - - // wait for ctrl-c - <-c - - color.Println("Ctrl-C pressed. Aborting...") - - stopC := exec.Command( - dockerExecutablePathAndFilename, - "stop", - "-t", - "0", - fullContainerName+"_DEBUG", - ) - stopC.Env = os.Environ() - stopC.Stdout = os.Stdout - stopC.Stderr = os.Stderr - stopC.Run() - - color.Println("") - color.Println("") - color.Println("=====================================") - color.Printf("Removing %s mounts\n", extraMountFolders) - color.Println("=====================================") - color.Println("") - - for _, extraMountFolder := range strings.Split(extraMountFolders, ",") { - extraMountFolder = strings.Trim(extraMountFolder, "/") - stat, err := os.Stat(extraMountFolder) - folderExists := err == nil && stat.IsDir() - if !folderExists { - os.MkdirAll(extraMountFolder, 0755) - } - - umountC := exec.Command( - "diskutil", - "umount", - "force", - extraMountFolder, - ) - umountC.Env = os.Environ() - umountC.Stdout = os.Stdout - umountC.Stderr = os.Stderr - umountC.Stdin = os.Stdin - umountC.Run() - } - } else { - printXdebugUsage(fullContainerName) - // wait for ctrl-c - <-c - color.Println("Ctrl-C pressed. Aborting...") - } + printXdebugUsage(fullContainerName) + // wait for ctrl-c + <-c + color.Println("Ctrl-C pressed. Aborting...") color.Println("=====================================") color.Printf("Disabling Xdebug\n") @@ -329,7 +182,6 @@ the PHP Process such that the debugger is enabled. } command.Flags().StringVarP(&debugImage, "debug-image", "", "nicolaka/netshoot", "What debugger docker image to use for executing nsenter. By default, gists/nfs-server is used") - command.Flags().StringVarP(&extraMountFolders, "mount", "", "", "What additional folders to mount via webdav") return command } @@ -380,27 +232,6 @@ func printXdebugUsage(fullContainerName string) { color.Println("To stop debugging, press Ctrl-C") } -func waitUntilWebdavIsRunning() error { - client := http.Client{ - Timeout: 200 * time.Millisecond, - } - - // wait 60 seconds - for i := 0; i < 300; i++ { - resp, err := client.Get("http://127.0.0.1:19889") - if err == nil { - resp.Body.Close() - if resp.StatusCode == 200 { - return nil - } - } - time.Sleep(200 * time.Millisecond) - } - - return fmt.Errorf("timeout: webdav server not running after 60 seconds") - -} - func isXdebugPortOpenInIde(host string, port string) bool { timeout := time.Second conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), timeout) diff --git a/docs/_assets/SCR-20240410-qezg-2.png b/docs/_assets/SCR-20240410-qezg-2.png new file mode 100644 index 0000000..c618a48 Binary files /dev/null and b/docs/_assets/SCR-20240410-qezg-2.png differ diff --git a/docs/xdebug.md b/docs/xdebug.md index dba6c9f..c39ec61 100644 --- a/docs/xdebug.md +++ b/docs/xdebug.md @@ -11,83 +11,89 @@ because we want to use the same images for dev and prod; and xdebug comes with a ## Prerequisites - In IntelliJ/PHPStorm you need to enable `Run -> Start Listening for PHP Debug Connections`. -- For Neos/Flow: In case you use the `--mount=app/Data/Temporary,app/Packages` option (read for details below), - it is beneficial to mark the `Data` folder as excluded, and enable `File -> Power Save Mode` to prevent - long-running re-indexing in the IDE. ## Usage ```bash drydock xdebug [container-name] drydock xdebug [docker-compose-name] -# for Neos/Flow applications, additionally mount Data/Temporary, and the full Packages folder to enable -# editing *any* file. NOTE: To prevent PHPStorm from freezing, enable `File -> Power Save Mode` while debugging. -drydock xdebug [container-name] --mount=app/Data/Temporary,app/Packages +# for Neos/Flow applications, read the details below! ``` Convenience: You can either specify a container name, or also a `docker-compose` service name if you run this in a folder with a `docker-compose.yml` file inside). -## Advanced Usage: extra mounts, f.e. for Neos/Flow +## Usage with Neos / Flow -**tl;dr: If you use Neos/Flow with the `DistributionPackages` mounted into the Docker container, -use `drydock xdebug [container-name] --mount=app/Data/Temporary,app/Packages`.** +tl;dr: -**If you use Neos/Flow with the `DistributionPackages` *and* `Packages` mounted into the Docker container, -use `drydock xdebug [container-name] --mount=app/Data/Temporary`.** +1. Mount `Data/` between host and container +2. use `xdebug_break()` to set breakpoints -Read on for the full explanation. +Full details below: -Because mounting lots of files from Mac OS to Docker comes with a certain performance penalty, we often do not -mount the full Neos/Flow project into the container. With newest Docker and *VirtioFS*, the situation has improved -tremendously. +**Mount `Data/` between host and container** -Before VirtioFS, we have used a directory layout like the following: +All PHP files where you want to set breakpoints need to be mounted/synced between Docker host and container. For +Neos/Flow applications, you NEED to mount: -``` -**Old Neos Mount Structure** - -/app - composer.json - DistributionPackages/ <-- MOUNTED from the host (as the only folder) - My.Package - Packages/ - Framework/ - Application/ - My.Package <-- Symlink to DistributionPackages/My.Package - Data/ - Temporary/ -``` +- `Data` (because of transpiled PHP code files in `Data/Temporary/....`) +- `Packages` in order to set breakpoints easily. -With modern Docker, we often also mount the full `Packages` folder (which still comes with a performance cost, -but this is OKish): +This comes with some performance hit (many mounted files make the system a bit slower), BUT it seems manageable +on Apple Silicon and modern Docker for Mac. +As an example, you need the following docker-compose setup: + +```yaml +services: + ##### + # Neos CMS (php-fpm) + ##### + neos: + build: + context: . + dockerfile: ./deployment/local-dev/neos/Dockerfile + + volumes: + - ./app/DistributionPackages/:/app/DistributionPackages/ + + # !!!!!!!!!! + # FOR DEBUGGING, the following two lines are crucial + - ./app/Data/:/app/Data/ + - ./app/Packages/:/app/Packages/ + # !!!!!!!!!! + + # ... other mounts as you need them ``` -**New Neos Mount Structure** - -/app - composer.json - DistributionPackages/ <-- MOUNTED from the host - My.Package - Packages/ <-- MOUNTED from the host (additionally) - Framework/ - Application/ - My.Package <-- Symlink to DistributionPackages/My.Package - Data/ - Temporary/ -``` -Neos/Flow compiles most PHP files into a Code Cache inside `Data/Temporary`. Even with most modern Docker -and VirtioFS, it is too slow to mount the full `Data/Temporary` folder. +Restart the container via `docker compose up -d` after modifying the `docker-compose.yml`. We'd like to get rid of this +restart, but we did not manage to do this yet. + +**Use `xdebug_break()` to set breakpoints** -For debugging to work, the IDE needs to open the temporary files because if you put a breakpoint -in with `xdebug_break()`, these breakpoints appear in the Data/Temporary files. +Because the code which is executed resides somewhere in `Data/Temporary`, you would need to set breakpoint via the IDE there. + +It is recommended to set breakpoints via the `xdebug_break()` function in code -> as then the IDE will open in the correct +location and file in `Data/Temporary`. + +## Debugging Hints + +### Gateway Timeout in nginx / Caddy / ... + +Normally, we have a setup like this: + +``` +┌──────────────┐ ┌───────────┐ +│nginx / Caddy │ │ php-fpm │ +│reverse proxy │─────▶│ │ +└──────────────┘ └───────────┘ +``` -Thus, we've come up with an extra `--mount` option which allow to mount folders **from the container to the host** -(so that is the opposite direction as usual) - and this way, we can make the cached classes available -to PHPStorm/IntelliJ. Then, Xdebug debugging will properly work. +nginx has some timeouts when php-fpm does not react for a certain amount of time. +If we break at a breakpoint for a long time, we will definitely see the 504 gateway timeout in nginx. -Internally we're starting a webdav server in the sidecar debug container, and mount the share via Webdav on OSX. +![SCR-20240410-qezg-2.png](_assets/SCR-20240410-qezg-2.png) ## Help Text @@ -100,9 +106,6 @@ the PHP Process such that the debugger is enabled. Options: --debug-image What debugger docker image to use for executing nsenter (and optionally the NFS webdav server). By default, nicolaka/netshoot is used - --mount Extra mounts which should be mounted from the container to the host via webdav. - This is useful to be able to f.e. debug into non-mounted files (like other packages - in Neos/Flow applications) Examples