Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Watch mode in run doesn't kill and re-compile/re-run project #346

Open
carlosedp opened this issue Jul 29, 2023 · 5 comments
Open

Watch mode in run doesn't kill and re-compile/re-run project #346

carlosedp opened this issue Jul 29, 2023 · 5 comments

Comments

@carlosedp
Copy link

Trying to use bleep run httpserver -w in my ZIO-HTTP sample project doesn't re-compile/re-run the project.

Doing bleep compile httpserver -w works though.

To reproduce, clone https://github.com/carlosedp/bleepziohttp and run / compile with -w.

@carlosedp
Copy link
Author

I've also tested with a Cask minimal HTTP project and same happens, watch mode in run doesn't reload.

Used files from https://github.com/com-lihaoyi/cask/tree/master/example/minimalApplication and the build below:

$schema: https://raw.githubusercontent.com/oyvindberg/bleep/master/schema.json
$version: 0.0.2
jvm:
  name: graalvm-java17:22.3.1
projects:
  caskhttp:
    dependencies:
      - com.lihaoyi::cask:0.9.1
    extends: template-common
  tests:
    dependencies:
      - com.lihaoyi::utest:0.8.1
      - com.lihaoyi::requests:0.8.0
    dependsOn: caskhttp
    extends: template-common
    isTestProject: true
templates:
  template-common:
    platform:
      name: jvm
    scala:
      options: -encoding utf8 -feature -unchecked
      strict: true
      version: 3.3.0

@carlosedp carlosedp changed the title Watch mode in run doesn't work with ZIO/ZIO-HTTP project Watch mode in run doesn't re-compile / re-run project Jul 29, 2023
@oyvindberg
Copy link
Owner

I think it does work, just not in the way you expect it to. if the program you're watch/running completes, it will be restarted on source changes. we need to extend the run/watch functionality to kill a running program and restart it on source changes for this to work better

@carlosedp
Copy link
Author

Ah yes, that was the expected way... to make it kill and re-run the application.
Can we keep this open to track when the feature will be available?
Thanks

@oyvindberg
Copy link
Owner

Absolutely

@carlosedp carlosedp changed the title Watch mode in run doesn't re-compile / re-run project Watch mode in run doesn't kill and re-compile/re-run project Jul 31, 2023
@nafg
Copy link
Contributor

nafg commented Jul 29, 2024

Here is what I'm using in a project lately, as a local script.

Note that it includes a little Cask server to support live reload.

import java.io.File
import java.nio.file.Files

import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Promise}
import scala.jdk.CollectionConverters.*
import scala.jdk.OptionConverters.RichOptional

import bleep.commands.Compile
import bleep.internal.{TransitiveProjects, jvmRunCommand}
import bleep.model.{CrossProjectName, ProjectName}
import bleep.{BleepFileWatching, BleepScript, Commands, FileWatching, PathOps, Started}
import io.undertow.Undertow

object RunBg extends BleepScript("runBackground") {
  private var promise: Promise[Unit] = Promise()

  private object MyServer extends cask.MainRoutes {
    @cask.get("/wait-for-event")
    def waitForEvent() = {
      Await.result(promise.future, Duration.Inf)
      promise = Promise() // Reset for the next event
      cask.Response("Done", headers = Seq("Access-Control-Allow-Origin" -> "*"))
    }

    initialize()
  }

  override def run(started: Started, commands: Commands, args: List[String]): Unit = {
    Undertow.builder
      .addHttpListener(4000, "localhost")
      .setHandler(MyServer.defaultHandler)
      .build
      .start()

    val projectName = "main"
    val pidFile     = started.buildPaths.buildsDir / s"$projectName.pid"
    val project     = CrossProjectName(ProjectName(projectName), None)

    var process: Option[Process] = None

    def terminateProcessHandle(p: ProcessHandle): Unit = {
      val desc     = s"[${p.pid()}] ${p.info().commandLine().orElse("")}"
      val children = p.children().toList.asScala
      p.destroy()
      if (p.isAlive) {
        println(s"Waiting for $desc to terminate")
        val start = System.currentTimeMillis()
        while (p.isAlive && System.currentTimeMillis() - start < 20000) {
          Thread.sleep(100)
        }
        if (p.isAlive) {
          println(s"Killing $desc")
          p.destroyForcibly()
        }
      }
      children.foreach(terminateProcessHandle)
    }

    def terminate(): Unit = {
      val pid =
        try
          Option.when(Files.exists(pidFile)) {
            Files.readString(pidFile).toInt
          }
        catch {
          case e: Throwable =>
            println(s"Error reading pid file $pidFile")
            e.printStackTrace()
            None
        }
      pid.flatMap(ProcessHandle.of(_).toScala).foreach(terminateProcessHandle)
      process.foreach(p => terminateProcessHandle(p.toHandle))
    }

    def runApp(): Unit =
      Compile(watch = false, Array(project)).run(started) match {
        case Left(value) =>
          value.printStackTrace()
        case Right(())   =>
          terminate()
          val value   = jvmRunCommand(started.bloopProject(project), started.resolvedJvm, project, None, args)
          value.left.foreach(_.printStackTrace())
          val command = value.orThrow
          val p       =
            new ProcessBuilder(command*)
              .directory(new File(sys.env("PWD")))
              .inheritIO()
              .start()
          Files.writeString(pidFile, p.pid().toString)
          process = Some(p)

          promise.success(())
          promise = Promise()
      }

    val watcher = BleepFileWatching.projects(started, TransitiveProjects(started.build, Array(project))) { projects =>
      println("changed: " + projects)
      runApp()
    }

    try {
      runApp()
      watcher.run(FileWatching.StopWhen.OnStdInput)
    } finally terminate()
  }
}

The live reload works with the following code in the web page, rendered only in dev:

function waitUntilAvailable() {
  fetch(location.href)
    .then(
      res => {
        console.dir(res);
        location.reload()
      },
      () => setTimeout(waitUntilAvailable, 1000)
    )
}
async function longPoll() {
  try {
    const response = await fetch('http://localhost:4000/wait-for-event');
    await response.text();
    waitUntilAvailable();
  } catch (error) {
    console.error('Error during long polling:', error);
    setTimeout(longPoll, 5000); // Retry after delay in case of error
  }
}

longPoll()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants