Skip to content

Commit

Permalink
Universal Main ( #1729)
Browse files Browse the repository at this point in the history
This PR is an attempt to improve the startup process of Mill. Currently, to run in different modes (client/server, standalone/no-server) we use different entry points which reside in different classes. Also, the client main is implemented in pure Java to improve the startup time of the client (by keep classloading at a minimum). To reflect the different entry points, we currently need a rather sophisticated start script. Although this PR does not simplify the process as such, it simplifies it's user side by supporting all modes through only one main class, the `mill.main.client.MillClientMain`. That way the good startup times in client mode remain.

Because we handle all modes in a single point, we can better handle server startup issues. E.g. we can now gracefully fall back to a in-process or sub-process runner when the server runner can not be properly started. This should improve the user experience on some Windows setups and newer M1 Macs a lot.

I didn't removed the complexity of the startup script for now, but simply ensured it always calls the client main.

The client main now ensures that custom JVM properties (`.mill-jvm-opts`) are properly applied in case we can't start the server and will, if necessary, start in a sub-process (with properly applied JVM options) instead of in-process.

The `.mill-version` is currently not handled (as before), but there is no reason that we could not detect a mismatch and even implement fetching and starting of another version in future. 

So, this PR should also improve the way Mill can be embedded into other tools or launcher, like coursier.

I have no idea how to test all this automatically, so I tried to test as much locally as possible, but any help in testing this, especially in edge cases like special environments, high load scenarios, many mill server instances (e.g. with different JVMs) would be really appreciated.

Pull request: #1729
  • Loading branch information
lefou authored Feb 5, 2022
2 parents 677a10b + d097f81 commit c556181
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 116 deletions.
23 changes: 20 additions & 3 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ object main extends MillModule {
s"""
|package mill
|
|/** Generated by mill. */
|object BuildInfo {
| /** Scala version used to compile mill core. */
| val scalaVersion = "$scalaVersion"
Expand Down Expand Up @@ -341,6 +342,21 @@ object main extends MillModule {
override def ivyDeps = Agg(
Deps.ipcsocketExcludingJna
)
def generatedBuildInfo = T{
val dest = T.dest
val code =
s"""package mill.main.client;
|
|/** Generated by mill. */
|public class BuildInfo {
| /** Mill version. */
| public static String millVersion() { return "${millVersion()}"; }
|}
|""".stripMargin
os.write(dest / "mill" / "main" / "client" / "BuildInfo.java", code, createFolders = true)
Seq(PathRef(dest))
}
override def generatedSources: T[Seq[PathRef]] = super.generatedSources() ++ generatedBuildInfo()
object test extends Tests with TestModule.Junit4 {
override def ivyDeps = Agg(Deps.junitInterface, Deps.lambdaTest)
}
Expand Down Expand Up @@ -841,7 +857,7 @@ def launcherScript(
shellClassPath: Agg[String],
cmdClassPath: Agg[String]
) = {
val millMainClass = "mill.MillMain"
val millMainClass = "mill.main.client.MillClientMain"
val millClientMainClass = "mill.main.client.MillClientMain"

mill.modules.Jvm.universalScript(
Expand Down Expand Up @@ -875,6 +891,7 @@ def launcherScript(
| "-X"*) mill_jvm_opts="$${mill_jvm_opts} $$line"
| esac
| done <"$$mill_jvm_opts_file"
| mill_jvm_opts="$${mill_jvm_opts} -Dmill.jvm_opts_applied=true"
| fi
|}
|
Expand Down Expand Up @@ -1023,7 +1040,7 @@ object dev extends MillModule {

def run(args: String*) = T.command {
args match {
case Nil => mill.eval.Result.Failure("Need to pass in cwd as first argument to dev.run")
case Nil => mill.api.Result.Failure("Need to pass in cwd as first argument to dev.run")
case wd0 +: rest =>
val wd = os.Path(wd0, os.pwd)
os.makeDir.all(wd)
Expand All @@ -1032,7 +1049,7 @@ object dev extends MillModule {
forkEnv(),
workingDir = wd
)
mill.eval.Result.Success(())
mill.api.Result.Success(())
}

}
Expand Down
79 changes: 79 additions & 0 deletions main/client/src/mill/main/client/IsolatedMillMainLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package mill.main.client;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

class IsolatedMillMainLoader {

public static class LoadResult {

public final Optional<Method> millMainMethod;
public final boolean canLoad;
public final long loadTime;

public LoadResult(Optional<Method> millMainMethod, final long loadTime) {
this.millMainMethod = millMainMethod;
this.canLoad = millMainMethod.isPresent();
this.loadTime = loadTime;
}
}

private static Optional<LoadResult> canLoad = Optional.empty();

public static LoadResult load() {
if (canLoad.isPresent()) {
return canLoad.get();
} else {
long startTime = System.currentTimeMillis();
Optional<Method> millMainMethod = Optional.empty();
try {
Class<?> millMainClass = IsolatedMillMainLoader.class.getClassLoader().loadClass("mill.MillMain");
Method mainMethod = millMainClass.getMethod("main", String[].class);
millMainMethod = Optional.of(mainMethod);
} catch (ClassNotFoundException | NoSuchMethodException e) {
millMainMethod = Optional.empty();
}

long loadTime = System.currentTimeMillis() - startTime;
LoadResult result = new LoadResult(millMainMethod, loadTime);
canLoad = Optional.of(result);
return result;
}
}

public static void runMain(String[] args) throws Exception {
LoadResult loadResult = load();
if (loadResult.millMainMethod.isPresent()) {
if (!MillEnv.millJvmOptsAlreadyApplied() && MillEnv.millJvmOptsFile().exists()) {
System.err.println("Launching Mill as sub-process ...");
int exitVal = launchMillAsSubProcess(args);
System.exit(exitVal);
} else {
// launch mill in-process
// it will call System.exit for us
Method mainMethod = loadResult.millMainMethod.get();
mainMethod.invoke(null, new Object[]{args});
}
} else {
throw new RuntimeException("Cannot load mill.MillMain class");
}
}

private static int launchMillAsSubProcess(String[] args) throws Exception {
boolean setJnaNoSys = System.getProperty("jna.nosys") == null;

List<String> l = new ArrayList<>();
l.addAll(MillEnv.millLaunchJvmCommand(setJnaNoSys));
l.add("mill.MillMain");
l.addAll(Arrays.asList(args));

Process running = new ProcessBuilder()
.command(l)
.inheritIO()
.start();
return running.waitFor();
}
}
Loading

0 comments on commit c556181

Please sign in to comment.