diff --git a/app/project-manager-shim/src/desktopEnvironment.ts b/app/project-manager-shim/src/desktopEnvironment.ts index 0f4fa342481d..0f85c52d11ab 100644 --- a/app/project-manager-shim/src/desktopEnvironment.ts +++ b/app/project-manager-shim/src/desktopEnvironment.ts @@ -50,22 +50,19 @@ function getMacOsDocumentsPath(): string { } /** Get the path to the `My Documents` Windows directory. */ -function getWindowsDocumentsPath(): string | undefined { +function getWindowsDocumentsPath() { const out = childProcess.spawnSync( - 'reg', + 'powershell', [ - 'query', - 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders', - '/v', - 'personal', + '[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; [Environment]::GetFolderPath("MyDocuments")', ], { timeout: CHILD_PROCESS_TIMEOUT }, ) - if (out.error !== undefined) { - return - } else { - const stdoutString = out.stdout.toString() - return stdoutString.split(/\s\s+/)[4] + if (out.error) { + console.warn(`getWindowsDocumentsPath.error: ${out.error.message}`) + return undefined } + + return out.stdout.toString().trim() || undefined } diff --git a/engine/runner/src/main/java/org/enso/runner/Main.java b/engine/runner/src/main/java/org/enso/runner/Main.java index 8897dbea5841..6e332aeb18cc 100644 --- a/engine/runner/src/main/java/org/enso/runner/Main.java +++ b/engine/runner/src/main/java/org/enso/runner/Main.java @@ -41,6 +41,7 @@ import org.enso.libraryupload.LibraryUploader.UploadFailedError; import org.enso.logger.Converter; import org.enso.logger.ObservedMessage; +import org.enso.os.environment.Arguments; import org.enso.pkg.Contact; import org.enso.pkg.PackageManager; import org.enso.pkg.PackageManager$; @@ -1124,6 +1125,8 @@ private URI parseUri(String string) { * @param args the command line arguments */ public static void main(String[] args) throws Exception { + // Handle an issue with Windows arguments containing UTF-16 characters + args = Arguments.getCurrent().alterArgs(args); new Main().launch(args); } diff --git a/engine/runner/src/main/java/org/enso/runner/Utils.java b/engine/runner/src/main/java/org/enso/runner/Utils.java index 99d6de6f4140..22163d2fe097 100644 --- a/engine/runner/src/main/java/org/enso/runner/Utils.java +++ b/engine/runner/src/main/java/org/enso/runner/Utils.java @@ -212,6 +212,7 @@ static String adjustCwdToProject(String fileToRun) { if (!ImageInfo.inImageRuntimeCode()) { return System.getProperty("enso.user.dir"); } + var nativeApi = WorkingDirectory.getInstance(); var projectRoot = nativeApi.findProjectRoot(fileToRun); if (projectRoot != null) { @@ -222,6 +223,8 @@ static String adjustCwdToProject(String fileToRun) { var dirChanged = nativeApi.changeWorkingDir(parentDir); if (!dirChanged) { LOGGER.error("Cannot change working directory to {}", parentDir); + } else { + LOGGER.debug("Changed working directory to {}", parentDir); } } return curDir; diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/Arguments.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/Arguments.java new file mode 100644 index 000000000000..1a1d91658548 --- /dev/null +++ b/lib/java/os-environment/src/main/java/org/enso/os/environment/Arguments.java @@ -0,0 +1,14 @@ +package org.enso.os.environment; + +import org.enso.common.Platform; + +public sealed interface Arguments permits WindowsArguments, LinuxArguments { + static Arguments getCurrent() { + return switch (Platform.getOperatingSystem()) { + case LINUX, MACOS -> LinuxArguments.INSTANCE; + case WINDOWS -> WindowsArguments.INSTANCE; + }; + } + + String[] alterArgs(String[] originalArgs); +} diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/LinuxArguments.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/LinuxArguments.java new file mode 100644 index 000000000000..625101045a0d --- /dev/null +++ b/lib/java/os-environment/src/main/java/org/enso/os/environment/LinuxArguments.java @@ -0,0 +1,12 @@ +package org.enso.os.environment; + +final class LinuxArguments implements Arguments { + static final LinuxArguments INSTANCE = new LinuxArguments(); + + private LinuxArguments() {} + + @Override + public String[] alterArgs(String[] originalArgs) { + return originalArgs; + } +} diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/WindowsArguments.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/WindowsArguments.java new file mode 100644 index 000000000000..c93fc7cea6c1 --- /dev/null +++ b/lib/java/os-environment/src/main/java/org/enso/os/environment/WindowsArguments.java @@ -0,0 +1,101 @@ +package org.enso.os.environment; + +import java.util.List; +import org.enso.common.Platform; +import org.graalvm.nativeimage.ImageInfo; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.struct.CPointerTo; +import org.graalvm.nativeimage.c.type.CIntPointer; +import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.word.PointerBase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@CContext(WindowsArguments.Directives.class) +final class WindowsArguments implements Arguments { + static final WindowsArguments INSTANCE = new WindowsArguments(); + + private WindowsArguments() {} + + @Override + public String[] alterArgs(String[] originalArgs) { + if (!ImageInfo.inImageRuntimeCode()) { + return originalArgs; + } + + return readCommandLineArgs(); + } + + private static final Logger LOGGER = LoggerFactory.getLogger(WindowsArguments.class); + + private static final int WCHAR_SIZE = 2; + + private static String[] readCommandLineArgs() { + var cmd = GetCommandLineW(); + + CIntPointer numOfArgs = StackValue.get(Long.BYTES); + + WCharPointerPointer args = CommandLineToArgvW(cmd, numOfArgs); + try { + var numArgs = numOfArgs.read(); + + var results = new String[numArgs - 1]; + for (var i = 0; i < results.length; i++) { + var arg = args.read(i + 1); + results[i] = toJavaString(arg); + LOGGER.trace("Read command line argument {}: {}", i, results[i]); + } + + return results; + } finally { + LocalFree(args); + } + } + + private static String toJavaString(WCharPointer arg) { + return CTypeConversion.asByteBuffer(arg, wcslen(arg) * WCHAR_SIZE) + .order(java.nio.ByteOrder.LITTLE_ENDIAN) + .asCharBuffer() + .toString(); + } + + @CPointerTo(nameOfCType = "wchar_t") + private interface WCharPointer extends PointerBase {} + + @CPointerTo(WCharPointer.class) + private interface WCharPointerPointer extends PointerBase { + WCharPointer read(int index); + } + + @CFunction + private static native WCharPointer GetCommandLineW(); + + @CFunction + private static native WCharPointerPointer CommandLineToArgvW( + WCharPointer cmdLine, CIntPointer numArgsOut); + + @CFunction + private static native int wcslen(WCharPointer str); + + @CFunction + private static native void LocalFree(PointerBase p); + + static final class Directives implements CContext.Directives { + @Override + public boolean isInConfiguration() { + return Platform.getOperatingSystem().isWindows(); + } + + @Override + public List getHeaderFiles() { + return List.of("", ""); + } + + @Override + public List getLibraries() { + return List.of("Kernel32", "Shell32"); + } + } +} diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/chdir/WindowsWorkingDirectory.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/chdir/WindowsWorkingDirectory.java index 03a04bd05b20..51a959c4bb56 100644 --- a/lib/java/os-environment/src/main/java/org/enso/os/environment/chdir/WindowsWorkingDirectory.java +++ b/lib/java/os-environment/src/main/java/org/enso/os/environment/chdir/WindowsWorkingDirectory.java @@ -1,42 +1,71 @@ package org.enso.os.environment.chdir; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.util.List; import org.enso.common.Platform; +import org.graalvm.nativeimage.UnmanagedMemory; import org.graalvm.nativeimage.c.CContext; import org.graalvm.nativeimage.c.function.CFunction; -import org.graalvm.nativeimage.c.type.CCharPointer; import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.word.PointerBase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @CContext(WindowsWorkingDirectory.Directives.class) final class WindowsWorkingDirectory extends WorkingDirectory { + static final WindowsWorkingDirectory INSTANCE = new WindowsWorkingDirectory(); private static final Logger LOGGER = LoggerFactory.getLogger(WindowsWorkingDirectory.class); + // WChars in windows are 2 bytes + private static final int WCHAR_SIZE = 2; + + // Windows MAX_PATH is 260 and MAX_PATH_WIDE is 32767 + private static final int MAX_LENGTH = 32767; + + private static String wcharPtrAsString(PointerBase buffer, int length) { + return CTypeConversion.asByteBuffer(buffer, length * WCHAR_SIZE) + .order(ByteOrder.LITTLE_ENDIAN) + .asCharBuffer() + .toString(); + } + + private static PointerBase stringAsWCharPtr(String input) { + var bytes = input.getBytes(StandardCharsets.UTF_16LE); + var buffer = UnmanagedMemory.malloc(bytes.length + 2); + CTypeConversion.asByteBuffer(buffer, bytes.length + 2) + .order(ByteOrder.LITTLE_ENDIAN) + .put(bytes) + .put(new byte[] {0, 0}); + return buffer; + } + @Override public String currentWorkingDir() { - byte[] buf = new byte[4096]; - String path; - try (var ptrHolder = CTypeConversion.toCBytes(buf)) { - var ptr = ptrHolder.get(); - var ret = GetCurrentDirectoryA(4096, ptr); - if (ret == 0) { - LOGGER.error("GetCurrentDirectory failed with {}", ret); + var buffer = UnmanagedMemory.malloc(MAX_LENGTH * WCHAR_SIZE); + try { + int length = GetCurrentDirectoryW(MAX_LENGTH, buffer); + if (length == 0 || length == MAX_LENGTH) { + LOGGER.error("GetCurrentDirectory failed with length {}", length); return null; - } else { - path = new String(buf); } + + var result = wcharPtrAsString(buffer, length); + LOGGER.debug("Current working directory is {}", result); + return result; + } finally { + UnmanagedMemory.free(buffer); } - return path.trim(); } @Override public boolean changeWorkingDir(String path) { path = normalizeSlashes(path); - try (var cPath = CTypeConversion.toCString(path)) { - var res = SetCurrentDirectoryA(cPath.get()); + var buffer = stringAsWCharPtr(path); + try { + var res = SetCurrentDirectoryW(buffer); if (res == 0) { LOGGER.error("SetCurrrentDirectory to {} failed with {}", path, res); return false; @@ -45,6 +74,8 @@ public boolean changeWorkingDir(String path) { } catch (Throwable t) { LOGGER.error("Cannot change working directory to " + path + " on Windows", t); throw t; + } finally { + UnmanagedMemory.free(buffer); } } @@ -53,12 +84,15 @@ public boolean exists(String dir, String file) { dir = normalizeSlashes(dir); file = normalizeSlashes(file); var full = dir + Platform.separatorChar() + file; - try (var cPath = CTypeConversion.toCString(full)) { - var res = PathFileExistsA(cPath.get()); + var buffer = stringAsWCharPtr(full); + try { + var res = PathFileExistsW(buffer); return res != 0; } catch (Throwable t) { LOGGER.error("Cannot check if {} exists on Windows", full, t); return false; + } finally { + UnmanagedMemory.free(buffer); } } @@ -77,7 +111,7 @@ private static String normalizeSlashes(String path) { * docs */ @CFunction - static native int GetCurrentDirectoryA(int nBufferLength, CCharPointer lpBuffer); + static native int GetCurrentDirectoryW(int nBufferLength, PointerBase lpBuffer); /** * */ @CFunction - static native int SetCurrentDirectoryA(CCharPointer lpPathName); + static native int SetCurrentDirectoryW(PointerBase lpPathName); /** * */ @CFunction - static native int PathFileExistsA(CCharPointer pszPath); + static native int PathFileExistsW(PointerBase pszPath); static final class Directives implements CContext.Directives { @Override diff --git a/lib/java/os-environment/src/test/java/org/enso/os/environment/chdir/TestChangeDirectory.java b/lib/java/os-environment/src/test/java/org/enso/os/environment/chdir/TestChangeDirectory.java index 54cec9d28677..0fb2a43f8998 100644 --- a/lib/java/os-environment/src/test/java/org/enso/os/environment/chdir/TestChangeDirectory.java +++ b/lib/java/os-environment/src/test/java/org/enso/os/environment/chdir/TestChangeDirectory.java @@ -66,6 +66,19 @@ public void changeDir() throws IOException { assertEquals(tmpDirAbs, curDir); } + @Test + public void changeDirUnicode() throws IOException { + var tmpDir = TMP_DIR.newFolder().toPath(); + var subDir = tmpDir.resolve("使用者"); + var dirCreated = subDir.toFile().mkdir(); + assertTrue(dirCreated); + var subDirAbs = subDir.toAbsolutePath().toRealPath().toString(); + var succeeded = nativeApi.changeWorkingDir(subDirAbs); + assertTrue(succeeded); + var curDir = nativeApi.currentWorkingDir(); + assertEquals(subDirAbs, curDir); + } + @Test public void changeDir_NonExistingDir() throws IOException { var tmpDir = TMP_DIR.newFolder().toPath();