oyvindberg / bleep

A bleeping fast scala build tool!
MIT License
149 stars 21 forks source link

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

Open carlosedp opened 1 year ago

carlosedp commented 1 year ago

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 commented 1 year ago

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
oyvindberg commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

Absolutely

nafg commented 2 months ago

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()