diff options
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/windows/jni')
4 files changed, 451 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/windows/jni/BUILD b/src/main/java/com/google/devtools/build/lib/windows/jni/BUILD new file mode 100644 index 0000000000..ed8ca69799 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/windows/jni/BUILD @@ -0,0 +1,37 @@ +package( + default_visibility = [ + "//src/main/java/com/google/devtools/build/lib:__subpackages__", + "//src/test/java/com/google/devtools/build/lib:__subpackages__", + ], +) + +filegroup( + name = "srcs", + srcs = glob(["**"]), +) + +java_library( + name = "jni", + exports = [ + ":file", + ":processes", + ], +) + +java_library( + name = "file", + srcs = ["WindowsFileOperations.java"], + deps = [":jni-loader"], +) + +java_library( + name = "processes", + srcs = ["WindowsProcesses.java"], + deps = [":jni-loader"], +) + +java_library( + name = "jni-loader", + srcs = ["WindowsJniLoader.java"], + deps = ["//src/main/java/com/google/devtools/build/lib/windows/runfiles"], +) diff --git a/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsFileOperations.java b/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsFileOperations.java new file mode 100644 index 0000000000..44b6cba976 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsFileOperations.java @@ -0,0 +1,124 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.devtools.build.lib.windows.jni; + +import java.io.IOException; + +/** File operations on Windows. */ +public class WindowsFileOperations { + + // A note about UNC paths and path prefixes on Windows. The prefixes can be: + // - "\\?\", meaning it's a UNC path that is passed to user mode unicode WinAPI functions + // (e.g. CreateFileW) or a return value of theirs (e.g. GetLongPathNameW); this is the + // prefix we'll most often see + // - "\??\", meaning it's Device Object path; it's mostly only used by kernel/driver functions + // but we may come across it when resolving junction targets, as the target's path is + // specified with this prefix, see usages of DeviceIoControl with FSCTL_GET_REPARSE_POINT + // - "\\.\", meaning it's a Device Object path again; both "\??\" and "\\.\" are shorthands + // for the "\DosDevices\" Object Directory, so "\\.\C:" and "\??\C:" and "\DosDevices\C:" + // and "C:\" all mean the same thing, but functions like CreateFileW don't understand the + // fully qualified device path, only the shorthand versions; the difference between "\\.\" + // is "\??\" is not entirely clear (one is not available while Windows is booting, but + // that only concerns device drivers) but we most likely won't come across them anyway + // Some of this is documented here: + // - https://msdn.microsoft.com/en-us/library/windows/hardware/ff557762(v=vs.85).aspx + // - https://msdn.microsoft.com/en-us/library/windows/hardware/ff565384(v=vs.85).aspx + // - http://stackoverflow.com/questions/23041983 + // - http://stackoverflow.com/questions/14482421 + + private WindowsFileOperations() { + // Prevent construction + } + + private static final int MAX_PATH = 260; + + // Keep IS_JUNCTION_* values in sync with src/main/native/windows_file_operations.cc. + private static final int IS_JUNCTION_YES = 0; + private static final int IS_JUNCTION_NO = 1; + private static final int IS_JUNCTION_ERROR = 2; + + private static native int nativeIsJunction(String path, String[] error); + + private static native boolean nativeGetLongPath(String path, String[] result, String[] error); + + private static native boolean nativeCreateJunction(String name, String target, String[] error); + + /** Determines whether `path` is a junction point or directory symlink. */ + public static boolean isJunction(String path) throws IOException { + WindowsJniLoader.loadJni(); + String[] error = new String[] {null}; + switch (nativeIsJunction(asLongPath(path), error)) { + case IS_JUNCTION_YES: + return true; + case IS_JUNCTION_NO: + return false; + default: + throw new IOException(error[0]); + } + } + + /** + * Returns the long path associated with the input `path`. + * + * <p>This method resolves all 8dot3 style components of the path and returns the long format. For + * example, if the input is "C:/progra~1/micros~1" the result may be "C:\Program Files\Microsoft + * Visual Studio 14.0". The returned path is Windows-style in that it uses backslashes, even if + * the input uses forward slashes. + * + * <p>May return an UNC path if `path` or its resolution is sufficiently long. + * + * @throws IOException if the `path` is not found or some other I/O error occurs + */ + public static String getLongPath(String path) throws IOException { + WindowsJniLoader.loadJni(); + String[] result = new String[] {null}; + String[] error = new String[] {null}; + if (nativeGetLongPath(asLongPath(path), result, error)) { + return result[0]; + } else { + throw new IOException(error[0]); + } + } + + /** + * Returns a Windows-style path suitable to pass to unicode WinAPI functions. + * + * <p>Returns an UNC path if `path` is at least `MAX_PATH` long. If it's shorter or is already an + * UNC path, then this method returns `path` itself. + */ + static String asLongPath(String path) { + return path.length() >= MAX_PATH && !path.startsWith("\\\\?\\") + ? ("\\\\?\\" + path.replace('/', '\\')) + : path; + } + + /** + * Creates a junction at `name`, pointing to `target`. + * + * <p>Both `name` and `target` may be Unix-style Windows paths (i.e. use forward slashes), and + * they don't need to have a UNC prefix, not even if they are longer than `MAX_PATH`. The + * underlying logic will take care of adding the prefixes if necessary. + * + * @throws IOException if some error occurs + */ + public static void createJunction(String name, String target) throws IOException { + WindowsJniLoader.loadJni(); + String[] error = new String[] {null}; + if (!nativeCreateJunction(name.replace('/', '\\'), target.replace('/', '\\'), error)) { + throw new IOException( + String.format("Cannot create junction (name=%s, target=%s): %s", name, target, error[0])); + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsJniLoader.java b/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsJniLoader.java new file mode 100644 index 0000000000..8b53d0c1a8 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsJniLoader.java @@ -0,0 +1,42 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.devtools.build.lib.windows.jni; + +import com.google.devtools.build.lib.windows.runfiles.WindowsRunfiles; +import java.io.IOException; + +/** Loads native code under Windows. */ +public class WindowsJniLoader { + private static boolean jniLoaded = false; + + public static synchronized void loadJni() { + if (jniLoaded) { + return; + } + + try { + System.loadLibrary("windows_jni"); + } catch (UnsatisfiedLinkError ex) { + // We are probably in tests, let's try to find the library in the runfiles + try { + System.load(WindowsRunfiles.getRunfile("io_bazel/src/main/native/windows_jni.dll")); + } catch (IOException e) { + // We throw the UnsatisfiedLinkError if we cannot find the runfiles + throw ex; + } + } + jniLoaded = true; + } +} diff --git a/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsProcesses.java b/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsProcesses.java new file mode 100644 index 0000000000..1eaab25904 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsProcesses.java @@ -0,0 +1,248 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.devtools.build.lib.windows.jni; + +import java.util.List; + +/** Process management on Windows. */ +public class WindowsProcesses { + public static final long INVALID = -1; + + private WindowsProcesses() { + // Prevent construction + } + + /** + * Creates a process with the specified Windows command line. + * + * <p>Appropriately quoting arguments is the responsibility of the caller. + * + * @param argv0 the binary to run; must be unquoted; must be either an absolute, normalized + * Windows path with a drive letter (e.g. "c:\foo\bar app.exe") or a single file name (e.g. + * "foo app.exe") + * @param argvRest the rest of the command line, i.e. argv[1:] (needs to be quoted Windows style) + * @param commandLine the command line (needs to be quoted Windows style) + * @param env the environment of the new process. null means inherit that of the Bazel server + * @param cwd the working directory of the new process. if null, the same as that of the current + * process + * @param stdoutFile the file the stdout should be redirected to. if null, nativeReadStdout will + * work. + * @param stderrFile the file the stdout should be redirected to. if null, nativeReadStderr will + * work. + * @return the opaque identifier of the created process + */ + public static long createProcess( + String argv0, String argvRest, byte[] env, String cwd, String stdoutFile, String stderrFile) { + WindowsJniLoader.loadJni(); + return nativeCreateProcess(argv0, argvRest, env, cwd, stdoutFile, stderrFile); + } + + private static native long nativeCreateProcess( + String argv0, String argvRest, byte[] env, String cwd, String stdoutFile, String stderrFile); + + /** + * Writes data from the given array to the stdin of the specified process. + * + * <p>Blocks until either some data was written or the process is terminated. + * + * @return the number of bytes written + */ + public static int writeStdin(long process, byte[] bytes, int offset, int length) { + WindowsJniLoader.loadJni(); + return nativeWriteStdin(process, bytes, offset, length); + } + + private static native int nativeWriteStdin(long process, byte[] bytes, int offset, int length); + + /** Returns an opaque identifier of stdout stream for the process. */ + public static long getStdout(long process) { + WindowsJniLoader.loadJni(); + return nativeGetStdout(process); + } + + private static native long nativeGetStdout(long process); + + /** Returns an opaque identifier of stderr stream for the process. */ + public static long getStderr(long process) { + WindowsJniLoader.loadJni(); + return nativeGetStderr(process); + } + + private static native long nativeGetStderr(long process); + + /** + * Reads data from the stream into the given array. {@code stream} should come from {@link + * #nativeGetStdout(long)} or {@link #nativeGetStderr(long)}. + * + * <p>Blocks until either some data was read or the process is terminated. + * + * @return the number of bytes read, 0 on EOF, or -1 if there was an error. + */ + public static int readStream(long stream, byte[] bytes, int offset, int length) { + WindowsJniLoader.loadJni(); + return nativeReadStream(stream, bytes, offset, length); + } + + private static native int nativeReadStream(long stream, byte[] bytes, int offset, int length); + + /** + * Waits until the given process terminates. If timeout is non-negative, it indicates the number + * of milliseconds before the call times out. + * + * <p>Return values: + * <li>0: Process finished + * <li>1: Timeout + * <li>2: Something went wrong + */ + public static int waitFor(long process, long timeout) { + WindowsJniLoader.loadJni(); + return nativeWaitFor(process, timeout); + } + + private static native int nativeWaitFor(long process, long timeout); + + /** + * Returns the exit code of the process. Throws {@code IllegalStateException} if something goes + * wrong. + */ + public static int getExitCode(long process) { + WindowsJniLoader.loadJni(); + return nativeGetExitCode(process); + } + + private static native int nativeGetExitCode(long process); + + /** Returns the process ID of the given process or -1 if there was an error. */ + public static int getProcessPid(long process) { + WindowsJniLoader.loadJni(); + return nativeGetProcessPid(process); + } + + private static native int nativeGetProcessPid(long process); + + /** Terminates the given process. Returns true if the termination was successful. */ + public static boolean terminate(long process) { + WindowsJniLoader.loadJni(); + return nativeTerminate(process); + } + + private static native boolean nativeTerminate(long process); + + /** + * Releases the native data structures associated with the process. + * + * <p>Calling any other method on the same process after this call will result in the JVM crashing + * or worse. + */ + public static void deleteProcess(long process) { + WindowsJniLoader.loadJni(); + nativeDeleteProcess(process); + } + + private static native void nativeDeleteProcess(long process); + + /** + * Closes the stream + * + * @param stream should come from {@link #nativeGetStdout(long)} or {@link + * #nativeGetStderr(long)}. + */ + public static void closeStream(long stream) { + WindowsJniLoader.loadJni(); + nativeCloseStream(stream); + } + + private static native void nativeCloseStream(long stream); + + /** + * Returns a string representation of the last error caused by any call on the given process or + * the empty string if the last operation was successful. + * + * <p>Does <b>NOT</b> terminate the process if it is still running. + * + * <p>After this call returns, subsequent calls will return the empty string if there was no + * failed operation in between. + */ + public static String processGetLastError(long process) { + WindowsJniLoader.loadJni(); + return nativeProcessGetLastError(process); + } + + private static native String nativeProcessGetLastError(long process); + + public static String streamGetLastError(long process) { + WindowsJniLoader.loadJni(); + return nativeStreamGetLastError(process); + } + + private static native String nativeStreamGetLastError(long process); + + /** returns the PID of the current process. */ + public static int getpid() { + WindowsJniLoader.loadJni(); + return nativeGetpid(); + } + + private static native int nativeGetpid(); + + public static String quoteCommandLine(List<String> argv) { + StringBuilder result = new StringBuilder(); + for (int iArg = 0; iArg < argv.size(); iArg++) { + if (iArg != 0) { + result.append(" "); + } + String arg = argv.get(iArg); + boolean hasSpace = arg.contains(" "); + if (!arg.contains("\"") && !arg.contains("\\") && !hasSpace) { + // fast path. Just append the input string. + result.append(arg); + } else { + // We need to quote things if the argument contains a space. + if (hasSpace) { + result.append("\""); + } + + for (int iChar = 0; iChar < arg.length(); iChar++) { + boolean lastChar = iChar == arg.length() - 1; + switch (arg.charAt(iChar)) { + case '"': + // Escape double quotes + result.append("\\\""); + break; + case '\\': + // Backslashes at the end of the string are quoted if we add quotes + if (lastChar) { + result.append(hasSpace ? "\\\\" : "\\"); + } else { + // Backslashes everywhere else are quoted if they are followed by a + // quote or a backslash + result.append( + arg.charAt(iChar + 1) == '"' || arg.charAt(iChar + 1) == '\\' ? "\\\\" : "\\"); + } + break; + default: + result.append(arg.charAt(iChar)); + } + } + // Add ending quotes if we added a quote at the beginning. + if (hasSpace) { + result.append("\""); + } + } + } + + return result.toString(); + } +} |