Skip to content

Commit e41cb2f

Browse files
Resolve relative paths against original working directory (#12972)
- `adjustCwdToProject` records _original working directory_ - original working directory is passed thru the launcher and used in resolution of relative paths - adds a unit tests to prevent `IllegalArgumentException` - avoids `System.err` in favor of `ExitCode` exception
1 parent d6ab597 commit e41cb2f

File tree

8 files changed

+197
-114
lines changed

8 files changed

+197
-114
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.enso.runner;
2+
3+
import java.io.IOException;
4+
5+
/**
6+
* Thrown to instruct the {@link Main} launcher to exit with given exit code and provided error
7+
* message.
8+
*/
9+
final class ExitCode extends IOException {
10+
final int exitCode;
11+
12+
ExitCode(String msg, int exitCode) {
13+
super(msg);
14+
assert msg != null;
15+
this.exitCode = exitCode;
16+
}
17+
}

engine/runner/src/main/java/org/enso/runner/Main.java

Lines changed: 42 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import org.enso.distribution.Environment;
3737
import org.enso.editions.DefaultEdition;
3838
import org.enso.libraryupload.LibraryUploader.UploadFailedError;
39-
import org.enso.os.environment.chdir.WorkingDirectory;
4039
import org.enso.pkg.Contact;
4140
import org.enso.pkg.PackageManager;
4241
import org.enso.pkg.PackageManager$;
@@ -51,11 +50,11 @@
5150
import org.enso.runner.common.WrongOption;
5251
import org.enso.version.BuildVersion;
5352
import org.enso.version.VersionDescription;
54-
import org.graalvm.nativeimage.ImageInfo;
5553
import org.graalvm.polyglot.PolyglotException;
5654
import org.graalvm.polyglot.PolyglotException.StackFrame;
5755
import org.graalvm.polyglot.SourceSection;
5856
import org.graalvm.polyglot.io.MessageTransport;
57+
import org.slf4j.Logger;
5958
import org.slf4j.LoggerFactory;
6059
import org.slf4j.MDC;
6160
import org.slf4j.event.Level;
@@ -110,7 +109,7 @@ public class Main {
110109

111110
private static final String DEFAULT_MAIN_METHOD_NAME = "main";
112111

113-
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Main.class);
112+
private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);
114113

115114
Main() {}
116115

@@ -611,9 +610,9 @@ private void createNew(
611610
: join(new Contact(authorName, authorEmail), nil());
612611

613612
var edition = DefaultEdition.getDefaultEdition();
614-
if (logger.isTraceEnabled()) {
613+
if (LOGGER.isTraceEnabled()) {
615614
var baseEdition = edition.parent().getOrElse(() -> "<no-base>");
616-
logger.trace("Creating a new project " + name + " based on edition [" + baseEdition + "].");
615+
LOGGER.trace("Creating a new project " + name + " based on edition [" + baseEdition + "].");
617616
}
618617

619618
var template =
@@ -659,6 +658,7 @@ private void createNew(
659658
* @param logMasking whether or not log masking is enabled
660659
*/
661660
private void compile(
661+
String cwd,
662662
String path,
663663
boolean shouldCompileDependencies,
664664
boolean shouldUseGlobalCache,
@@ -669,10 +669,8 @@ private void compile(
669669
Level logLevel,
670670
boolean logMasking)
671671
throws IOException {
672-
var fileAndProject = Utils.findFileAndProject(path, null);
673-
if (fileAndProject == null) {
674-
throw exitFail("No package exists at " + path + ".");
675-
}
672+
var fileAndProject = Utils.findFileAndProject(cwd, path, null);
673+
assert fileAndProject != null;
676674

677675
boolean isProjectMode = fileAndProject._1();
678676
String projectPath = fileAndProject._3();
@@ -708,7 +706,7 @@ private void compile(
708706
throw exitFail("Compilation failed due to " + reason + ".");
709707
} else {
710708
String message = "Unexpected internal error: " + t.getMessage();
711-
logger.error(message, t);
709+
LOGGER.error(message, t);
712710
throw exitFail(message);
713711
}
714712

@@ -737,6 +735,7 @@ private void compile(
737735
* null}
738736
*/
739737
private void handleRun(
738+
String cwd,
740739
String path,
741740
List<String> additionalArgs,
742741
String projectPath,
@@ -752,10 +751,8 @@ private void handleRun(
752751
String executionEnvironment,
753752
int warningsLimit)
754753
throws IOException {
755-
var fileAndProject = Utils.findFileAndProject(path, projectPath);
756-
if (fileAndProject == null) {
757-
throw exitFail("Cannot find " + path + " and " + projectPath);
758-
}
754+
var fileAndProject = Utils.findFileAndProject(cwd, path, projectPath);
755+
assert fileAndProject != null;
759756
var projectMode = fileAndProject._1();
760757
var file = fileAndProject._2();
761758
var mainFile = file;
@@ -820,7 +817,7 @@ private void handleRun(
820817
} catch (RuntimeException e) {
821818
// forces computation of the exception message sooner than context is closed
822819
// should work around issues seen at #11127
823-
logger.debug("Execution failed with " + e.getMessage());
820+
LOGGER.debug("Execution failed with " + e.getMessage());
824821
throw e;
825822
} finally {
826823
context.context().close();
@@ -840,24 +837,23 @@ private void handleRun(
840837
*/
841838
private void genDocs(
842839
String docsFormat,
840+
String cwd,
843841
String projectPath,
844842
Level logLevel,
845843
boolean logMasking,
846-
boolean enableIrCaches) {
844+
boolean enableIrCaches)
845+
throws IOException {
847846
if (projectPath == null || projectPath.isEmpty()) {
848847
throw exitFail("Specify path to a project with --in-project option");
849848
}
850-
if (!fileExists(projectPath)) {
849+
var fileAndProject = Utils.findFileAndProject(cwd, projectPath, null);
850+
if (fileAndProject == null) {
851851
throw exitFail("Project specified in --in-project option does not exist: " + projectPath);
852852
}
853853
generateDocsFrom(docsFormat, projectPath, logLevel, logMasking, enableIrCaches);
854854
throw exitSuccess();
855855
}
856856

857-
private static boolean fileExists(String path) {
858-
return new File(path).exists();
859-
}
860-
861857
/**
862858
* Subroutine of `genDocs` function. Generates the documentation for given Enso project at given
863859
* path.
@@ -901,7 +897,7 @@ private void preinstallDependencies(String projectPath, Level logLevel) {
901897
DependencyPreinstaller.preinstallDependencies(new File(projectPath), logLevel);
902898
throw exitSuccess();
903899
} catch (RuntimeException error) {
904-
logger.error("Dependency installation failed: " + error.getMessage(), error);
900+
LOGGER.error("Dependency installation failed: " + error.getMessage(), error);
905901
throw exitFail("Dependency installation failed: " + error.getMessage());
906902
}
907903
}
@@ -960,7 +956,7 @@ private void runMain(
960956
listOfArgs = join(e, listOfArgs);
961957
}
962958
listOfArgs = listOfArgs.reverse();
963-
logger.debug("Executing the main function with arguments {}", listOfArgs.mkString(", "));
959+
LOGGER.debug("Executing the main function with arguments {}", listOfArgs.mkString(", "));
964960
var res = main.execute(listOfArgs);
965961
if (!res.isNull()) {
966962
var textRes = res.isString() ? res.asString() : res.toString();
@@ -1045,7 +1041,7 @@ private static MessageTransport replTransport() {
10451041
var repl = futureRepl.get();
10461042
return new DebuggerSessionManagerEndpoint(repl, peer);
10471043
} catch (InterruptedException | ExecutionException ex) {
1048-
logger.error("Cannot initialize REPL transport", ex);
1044+
LOGGER.error("Cannot initialize REPL transport", ex);
10491045
}
10501046
}
10511047
return null;
@@ -1105,11 +1101,13 @@ public static void main(String[] args) throws Exception {
11051101
/**
11061102
* Main entry point for the CLI program.
11071103
*
1104+
* @param cwd current working directory to use
11081105
* @param line the provided command line arguments
11091106
* @param logLevel the provided log level
11101107
* @param logMasking the flag indicating if the log masking is enabled
11111108
*/
1112-
final void mainEntry(CommandLine line, Level logLevel, boolean logMasking) throws IOException {
1109+
final void mainEntry(String cwd, CommandLine line, Level logLevel, boolean logMasking)
1110+
throws IOException {
11131111
if (line.hasOption(HELP_OPTION)) {
11141112
printHelp();
11151113
throw exitSuccess();
@@ -1180,6 +1178,7 @@ final void mainEntry(CommandLine line, Level logLevel, boolean logMasking) throw
11801178
var shouldUseGlobalCache = !line.hasOption(NO_GLOBAL_CACHE_OPTION);
11811179

11821180
compile(
1181+
cwd,
11831182
packagePath,
11841183
shouldCompileDependencies,
11851184
shouldUseGlobalCache,
@@ -1191,8 +1190,10 @@ final void mainEntry(CommandLine line, Level logLevel, boolean logMasking) throw
11911190
logMasking);
11921191
}
11931192

1193+
LOGGER.debug("Original working directory={}, cwd={}", cwd, System.getProperty("user.dir"));
11941194
if (line.hasOption(RUN_OPTION)) {
11951195
handleRun(
1196+
cwd,
11961197
line.getOptionValue(RUN_OPTION),
11971198
Arrays.asList(line.getArgs()),
11981199
line.getOptionValue(IN_PROJECT_OPTION),
@@ -1222,6 +1223,7 @@ final void mainEntry(CommandLine line, Level logLevel, boolean logMasking) throw
12221223
if (line.hasOption(DOCS_OPTION)) {
12231224
genDocs(
12241225
line.getOptionValue(DOCS_OPTION),
1226+
cwd,
12251227
line.getOptionValue(IN_PROJECT_OPTION),
12261228
logLevel,
12271229
logMasking,
@@ -1296,7 +1298,7 @@ private static <A> A withProfiling(
12961298
try {
12971299
sampler.stop();
12981300
} catch (IOException ex) {
1299-
logger.error("Error stopping sampler", ex);
1301+
LOGGER.error("Error stopping sampler", ex);
13001302
}
13011303
return BoxedUnit.UNIT;
13021304
});
@@ -1504,8 +1506,11 @@ private void launchJvm(
15041506
private void launch(String[] args) throws IOException, InterruptedException, URISyntaxException {
15051507
var line = preprocessArguments(args);
15061508

1507-
if (line.hasOption(RUN_OPTION)) {
1508-
maybeChangeWorkingDirToProjectRoot(line.getOptionValue(RUN_OPTION));
1509+
String originalCwdOrNull = null;
1510+
if (line.hasOption(IN_PROJECT_OPTION)) {
1511+
originalCwdOrNull = Utils.adjustCwdToProject(line.getOptionValue(IN_PROJECT_OPTION));
1512+
} else if (line.hasOption(RUN_OPTION)) {
1513+
originalCwdOrNull = Utils.adjustCwdToProject(line.getOptionValue(RUN_OPTION));
15091514
}
15101515

15111516
var logMasking = new boolean[1];
@@ -1556,15 +1561,15 @@ private void launch(String[] args) throws IOException, InterruptedException, URI
15561561
}
15571562
}
15581563

1559-
launch(line, logLevel, logMasking[0]);
1564+
handleLaunch(originalCwdOrNull, line, logLevel, logMasking[0]);
15601565
}
15611566

15621567
final CommandLine preprocessArguments(String... args) {
15631568
var parser = new DefaultParser();
15641569
try {
15651570
var startParsing = System.currentTimeMillis();
15661571
var line = parser.parse(CLI_OPTIONS, args);
1567-
logger.trace(
1572+
LOGGER.trace(
15681573
"Parsing Language Server arguments took {0}ms",
15691574
System.currentTimeMillis() - startParsing);
15701575
return line;
@@ -1574,41 +1579,6 @@ final CommandLine preprocessArguments(String... args) {
15741579
}
15751580
}
15761581

1577-
/**
1578-
* This method has to be called as early as possible. It attempts to find the project root
1579-
* directory of the given file, and if the project root is found, it uses native code to change
1580-
* the working directory to the project root. In order for the JVM's {@code java.io} to reflect
1581-
* the working directory change, this methods must be called before any class from {@code java.io}
1582-
* is accessed.
1583-
*
1584-
* <p>Note that invoking native code is the only reliable way to change the working directory in
1585-
* the current process.
1586-
*
1587-
* <p>For detailed explanation see this <a
1588-
* href="https://github.com/enso-org/enso/pull/12618#issuecomment-2778451448">GH comment</a>.
1589-
*
1590-
* @param fileToRun the file to run, value of the {@code --run} option.
1591-
*/
1592-
private void maybeChangeWorkingDirToProjectRoot(String fileToRun) {
1593-
assert fileToRun != null;
1594-
if (!ImageInfo.inImageRuntimeCode()) {
1595-
return;
1596-
}
1597-
var nativeApi = WorkingDirectory.getInstance();
1598-
var projectRoot = nativeApi.findProjectRoot(fileToRun);
1599-
if (projectRoot != null) {
1600-
var parentDir = nativeApi.parentFile(projectRoot);
1601-
assert parentDir != null;
1602-
var curDir = nativeApi.currentWorkingDir();
1603-
if (!parentDir.equals(curDir)) {
1604-
var dirChanged = nativeApi.changeWorkingDir(parentDir);
1605-
if (!dirChanged) {
1606-
logger.error("Cannot change working directory to {}", parentDir);
1607-
}
1608-
}
1609-
}
1610-
}
1611-
16121582
private Level setupLogging(CommandLine line, boolean[] logMasking) {
16131583
var logLevel =
16141584
scala.Option.apply(line.getOptionValue(LOG_LEVEL))
@@ -1638,7 +1608,8 @@ private Level setupLogging(CommandLine line, boolean[] logMasking) {
16381608
return logLevel;
16391609
}
16401610

1641-
private void launch(CommandLine line, Level logLevel, boolean logMasking) {
1611+
private final void handleLaunch(
1612+
String cwd, CommandLine line, Level logLevel, boolean logMasking) {
16421613
if (line.hasOption(LANGUAGE_SERVER_OPTION)) {
16431614
try {
16441615
var conf = parseProfilingConfig(line);
@@ -1663,12 +1634,14 @@ private void launch(CommandLine line, Level logLevel, boolean logMasking) {
16631634
conf,
16641635
ExecutionContext.global(),
16651636
() -> {
1666-
mainEntry(line, logLevel, logMasking);
1637+
mainEntry(cwd, line, logLevel, logMasking);
16671638
return BoxedUnit.UNIT;
16681639
});
1640+
} catch (ExitCode ex) {
1641+
throw exitFail(ex.getMessage());
16691642
} catch (IOException ex) {
1670-
if (logger.isDebugEnabled()) {
1671-
logger.error("Error during execution", ex);
1643+
if (LOGGER.isDebugEnabled()) {
1644+
LOGGER.error("Error during execution", ex);
16721645
}
16731646
throw exitFail("Command failed with an error: " + ex.getMessage());
16741647
}

0 commit comments

Comments
 (0)