diff --git a/cmd/excimer.go b/cmd/excimer.go new file mode 100644 index 0000000..b356a87 --- /dev/null +++ b/cmd/excimer.go @@ -0,0 +1,226 @@ +package cmd + +import ( + "github.com/gookit/color" + "github.com/jonhadfield/findexec" + "github.com/sandstorm/drydock/util" + "github.com/spf13/cobra" + "log" + "os" + "os/exec" + "os/signal" +) + +// phpExcimerInstallScript is running in the debugImage +// - we mount the inner container to /container (should be based on some base "official" Docker PHP image) +// - reload the config +func phpExcimerInstallScript(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 + export HTTP_PROXY="" + export HTTPS_PROXY="" + pecl install excimer +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/excimer.ini +auto_prepend_file=/app/tracing/auto_prepend_file.php + +extension=excimer.so + +EOF + +mkdir -p /container/app/tracing +mkdir -p /container/app/tracing/_traces +chmod -R 777 /container/app/tracing + +cat << 'EOF' > /container/app/tracing/auto_prepend_file.php +setPeriod( 0.001 ); // 1ms + $excimer->setEventType( EXCIMER_REAL ); // OR: EXCIMER_CPU, but does not work on FreeBSD. + $excimer->start(); + register_shutdown_function( function () use ( $excimer ) { + $excimer->stop(); + $data = $excimer->getLog()->formatCollapsed(); + file_put_contents('/app/tracing/_traces/' . getmypid(), $data, FILE_APPEND); + } ); +} + +// HINT: to start PHP continuous profiling, comment-in the following line. +startExcimer(); + +EOF + +echo "restarting php-fpm" +pkill -USR2 php-fpm +` +} + +func phpXExcimerDeactivateScript() string { + return mountSlashContainer + ` +rm /container$PHP_INI_DIR/conf.d/excimer.ini +pkill -USR2 php-fpm +` +} + +func buildExcimerCommand() *cobra.Command { + var debugImage string = "nicolaka/netshoot" + + var command = &cobra.Command{ + Use: "excimer [flags] SERVICE-or-CONTAINER", + Short: "Install Excimer Sampling Continuous Profiler in the given container", + Long: color.Sprintf(`Usage: drydock excimer [flags] SERVICE-OR-CONTAINER + +Run excimer Continuous Profiler in the given PHP Container, and reloads +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 + +Examples + +Run excimer in a running PHP container + drydock excimer myContainer + +Run excimer in a running docker-compose service + drydock excimer my-docker-compose-service + +Background: + + This command installs the excimer PHP extension into an existing Docker container, even if the container is locked + down to a non-root user. Additionally, we reload the PHP process by using kill -USR2. + + This command is using nsenter wrapped in a privileged docker container to install the PHP extension + inside a running container as root. + +`), + Args: cobra.ExactArgs(1), + + Run: func(cmd *cobra.Command, args []string) { + //isOpen := isXdebugPortOpenInIde("127.0.0.1", "9003") + + color.Println("") + color.Println("") + color.Println("=====================================") + color.Println("Installing excimer into the container") + color.Println("and reloading PHP") + color.Println("=====================================") + color.Println("") + + dockerContainerIdentifier, err := util.TryGetDockerContainerNameFromDockerCompose(args[0]) + + if err != nil { + // could not identify the docker container identifier; e.g. no docker-compose used. + dockerContainerIdentifier = args[0] + } else { + color.Printf("docker compose service %s found, entering it.\n", args[0]) + color.Println("") + } + + pid, err := util.GetRootPidForDockerContainer(dockerContainerIdentifier) + + if err != nil || pid == "0" { + // container not running + color.Printf("FATAL: Container %s not running.\n", dockerContainerIdentifier) + color.Println("") + os.Exit(1) + } + + fullContainerName, err := util.GetFullContainerName(dockerContainerIdentifier) + if err != nil { + color.Printf("FATAL: Could not extract container name for container %s - THIS SHOULD NOT HAPPEN. Please file a bug report.\n", dockerContainerIdentifier) + color.Println("") + os.Exit(1) + } + + dockerExecutablePathAndFilename := findexec.Find("docker", "") + + // we need to get the ENV of the original container to find the PHP_INI_DIR (needed such that "docker-php-ext-enable" will work: https://github.com/docker-library/php/blob/67c242cb1529c70a3969a373ab333c53001c95b8/8.2-rc/bullseye/cli/docker-php-ext-enable) + extraDockerRunArgs, err := util.GetEnvCliCallsForDockerRunFromContainerMetadata(fullContainerName) + if err != nil { + log.Printf("FATAL: Could not extract env variables for container '%s': %s - THIS SHOULD NOT HAPPEN. Please file a bug report.\n", dockerContainerIdentifier, err) + os.Exit(1) + } + + // Install excimer + 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, phpExcimerInstallScript(pid)) + + dockerRunC := exec.Command(dockerExecutablePathAndFilename, dockerRunCommand[1:]...) + dockerRunC.Env = os.Environ() + dockerRunC.Stdout = os.Stdout + dockerRunC.Stderr = os.Stderr + dockerRunC.Run() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + printExcimerUsage(fullContainerName) + // wait for ctrl-c + <-c + color.Println("Ctrl-C pressed. Aborting...") + + color.Println("=====================================") + color.Printf("Disabling Excimer\n") + color.Println("=====================================") + color.Println("") + // Removing XDebug + // Install XDEBUG and prepare for NFS Server + dockerRunCommand = dockerRunNsenterCommand(fullContainerName, debugImage, pid, extraDockerRunArgs) + dockerRunCommand = append(dockerRunCommand, "/bin/bash") + dockerRunCommand = append(dockerRunCommand, "-c") + dockerRunCommand = append(dockerRunCommand, phpXExcimerDeactivateScript()) + + dockerRunC = exec.Command(dockerExecutablePathAndFilename, dockerRunCommand[1:]...) + dockerRunC.Env = os.Environ() + dockerRunC.Stdout = os.Stdout + dockerRunC.Stderr = os.Stderr + dockerRunC.Run() + + color.Println("=====================================") + color.Printf("All done!\n") + color.Println("=====================================") + color.Println("") + + }, + } + + command.Flags().StringVarP(&debugImage, "debug-image", "", "nicolaka/netshoot", "What debugger docker image to use for executing nsenter. By default, gists/nfs-server is used") + + return command +} + +func printExcimerUsage(fullContainerName string) { + color.Println("") + color.Println("") + color.Println("=====================================") + color.Printf("Excimer fully set up for %s\n", fullContainerName) + color.Println("") + color.Println("=====================================") + color.Println("") + color.Println("To stop debugging, press Ctrl-C") +} diff --git a/cmd/root.go b/cmd/root.go index 428f367..4df0fd5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -89,6 +89,7 @@ func Execute(version, commit string) { rootCmd.AddCommand(buildVsCodeCommand()) rootCmd.AddCommand(buildSpxCommand()) rootCmd.AddCommand(buildXdebugCommand()) + rootCmd.AddCommand(buildExcimerCommand()) if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1)