From f2d2500d1d326e8bf0bacc418337a4c8987d08c4 Mon Sep 17 00:00:00 2001 From: Thad House Date: Tue, 5 Nov 2024 08:51:48 -0800 Subject: [PATCH] [wpiutil] Check MSVC Runtime (#7301) --- .../src/main/native/include/frc/RobotBase.h | 17 +++++ .../java/edu/wpi/first/wpilibj/RobotBase.java | 3 + wpiutil/build.gradle | 2 +- .../dev/java/edu/wpi/first/util/DevMain.java | 1 + .../wpi/first/util/MsvcRuntimeException.java | 51 +++++++++++++ .../java/edu/wpi/first/util/WPIUtilJNI.java | 3 + wpiutil/src/main/native/cpp/RuntimeCheck.cpp | 73 +++++++++++++++++++ .../src/main/native/cpp/jni/WPIUtilJNI.cpp | 33 ++++++++- .../main/native/include/wpi/RuntimeCheck.h | 21 ++++++ 9 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 wpiutil/src/main/java/edu/wpi/first/util/MsvcRuntimeException.java create mode 100644 wpiutil/src/main/native/cpp/RuntimeCheck.cpp create mode 100644 wpiutil/src/main/native/include/wpi/RuntimeCheck.h diff --git a/wpilibc/src/main/native/include/frc/RobotBase.h b/wpilibc/src/main/native/include/frc/RobotBase.h index 6dff3696cd..54715bcb8e 100644 --- a/wpilibc/src/main/native/include/frc/RobotBase.h +++ b/wpilibc/src/main/native/include/frc/RobotBase.h @@ -10,8 +10,10 @@ #include #include #include +#include #include #include +#include #include "frc/Errors.h" #include "frc/RuntimeType.h" @@ -53,6 +55,21 @@ void RunRobot(wpi::mutex& m, Robot** robot) { template int StartRobot() { + uint32_t foundMajor; + uint32_t foundMinor; + uint32_t expectedMajor; + uint32_t expectedMinor; + WPI_String runtimePath; + if (!WPI_IsRuntimeValid(&foundMajor, &foundMinor, &expectedMajor, + &expectedMinor, &runtimePath)) { + // We could make this error better, however unlike Java, there is only a + // single scenario that could be occuring. The entirety of VS is too out + // of date. In most cases the linker should detect this, but not always. + fmt::println( + "Your copy of Visual Studio is out of date. Please update it.\n"); + return 1; + } + int halInit = RunHALInitialization(); if (halInit != 0) { return halInit; diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java index 2de7cf1ce8..4617a1ff99 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java @@ -397,6 +397,9 @@ public abstract class RobotBase implements AutoCloseable { * @param robotSupplier Function that returns an instance of the robot subclass. */ public static void startRobot(Supplier robotSupplier) { + // Check that the MSVC runtime is valid. + WPIUtilJNI.checkMsvcRuntime(); + if (!HAL.initialize(500, 0)) { throw new IllegalStateException("Failed to initialize. Terminating"); } diff --git a/wpiutil/build.gradle b/wpiutil/build.gradle index 99deb79cef..4958fe634b 100644 --- a/wpiutil/build.gradle +++ b/wpiutil/build.gradle @@ -9,7 +9,7 @@ ext { groupId = 'edu.wpi.first.wpiutil' nativeName = 'wpiutil' - devMain = 'edu.wpi.first.wpiutil.DevMain' + devMain = 'edu.wpi.first.util.DevMain' def generateTask = createGenerateResourcesTask('main', 'WPI', 'wpi', project) splitSetup = { diff --git a/wpiutil/src/dev/java/edu/wpi/first/util/DevMain.java b/wpiutil/src/dev/java/edu/wpi/first/util/DevMain.java index e94fceb0c3..dd06a454ba 100644 --- a/wpiutil/src/dev/java/edu/wpi/first/util/DevMain.java +++ b/wpiutil/src/dev/java/edu/wpi/first/util/DevMain.java @@ -9,6 +9,7 @@ public final class DevMain { public static void main(String[] args) { System.out.println("Hello World!"); System.out.println(CombinedRuntimeLoader.getPlatformPath()); + WPIUtilJNI.checkMsvcRuntime(); } private DevMain() {} diff --git a/wpiutil/src/main/java/edu/wpi/first/util/MsvcRuntimeException.java b/wpiutil/src/main/java/edu/wpi/first/util/MsvcRuntimeException.java new file mode 100644 index 0000000000..1cef301b5d --- /dev/null +++ b/wpiutil/src/main/java/edu/wpi/first/util/MsvcRuntimeException.java @@ -0,0 +1,51 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.util; + +/** Exception thrown due to a bad MSVC Runtime. */ +public class MsvcRuntimeException extends RuntimeException { + private static final long serialVersionUID = -9155939328084105142L; + + private static String generateMessage( + int foundMajor, int foundMinor, int expectedMajor, int expectedMinor, String runtimePath) { + String jvmLocation = ProcessHandle.current().info().command().orElse("Unknown"); + + StringBuffer builder = new StringBuffer(100); + builder + .append("Invalid MSVC Runtime Detected.\n") + .append( + String.format( + "Expected at least %d.%d, but found %d.%d\n", + expectedMajor, expectedMinor, foundMajor, foundMinor)) + .append(String.format("JVM Location: %s\n", jvmLocation)) + .append(String.format("Runtime DLL Location: %s\n", runtimePath)) + .append("See https://wpilib.org/jvmruntime for more information\n"); + + return builder.toString(); + } + + /** + * Constructs a runtime exception. + * + * @param foundMajor found major + * @param foundMinor found minor + * @param expectedMajor expected major + * @param expectedMinor expected minor + * @param runtimePath path of runtime + */ + public MsvcRuntimeException( + int foundMajor, int foundMinor, int expectedMajor, int expectedMinor, String runtimePath) { + super(generateMessage(foundMajor, foundMinor, expectedMajor, expectedMinor, runtimePath)); + } + + /** + * Constructs a runtime exception. + * + * @param msg message + */ + public MsvcRuntimeException(String msg) { + super(msg); + } +} diff --git a/wpiutil/src/main/java/edu/wpi/first/util/WPIUtilJNI.java b/wpiutil/src/main/java/edu/wpi/first/util/WPIUtilJNI.java index 136e2c041d..c69895231e 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/WPIUtilJNI.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/WPIUtilJNI.java @@ -63,6 +63,9 @@ public class WPIUtilJNI { libraryLoaded = true; } + /** Checks if the MSVC runtime is valid. Throws a runtime exception if not. */ + public static native void checkMsvcRuntime(); + /** * Write the given string to stderr. * diff --git a/wpiutil/src/main/native/cpp/RuntimeCheck.cpp b/wpiutil/src/main/native/cpp/RuntimeCheck.cpp new file mode 100644 index 0000000000..6cead1a8ab --- /dev/null +++ b/wpiutil/src/main/native/cpp/RuntimeCheck.cpp @@ -0,0 +1,73 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "wpi/RuntimeCheck.h" + +#ifdef _WIN32 +#include +#include + +#include "Windows.h" +extern "C" int32_t WPI_IsRuntimeValid(uint32_t* foundMajor, + uint32_t* foundMinor, + uint32_t* expectedMajor, + uint32_t* expectedMinor, + WPI_String* runtimePath) { + HMODULE msvcRuntimeModule = GetModuleHandleW(L"msvcp140.dll"); + if (!msvcRuntimeModule) { + return 1; + } + HRSRC versionResource = FindResourceW(msvcRuntimeModule, MAKEINTRESOURCEW(1), + MAKEINTRESOURCEW(16)); + if (!versionResource) { + return 1; + } + HGLOBAL loadedVersion = LoadResource(msvcRuntimeModule, versionResource); + if (!loadedVersion) { + return 1; + } + // No need to unlock. Thats vestigial of windows before my time... + LPVOID lockedResource = LockResource(loadedVersion); + if (!lockedResource) { + return 1; + } + DWORD resourceSize = SizeofResource(msvcRuntimeModule, versionResource); + if (resourceSize == 0) { + return 1; + } + std::unique_ptr resourceBuffer = // NOLINT + std::make_unique(resourceSize); // NOLINT + std::memcpy(resourceBuffer.get(), lockedResource, resourceSize); + VS_FIXEDFILEINFO* fileInfo = nullptr; + UINT fileInfoLen = 0; + if (!VerQueryValueW(resourceBuffer.get(), L"\\", + reinterpret_cast(&fileInfo), &fileInfoLen)) { + return 1; + } + *foundMajor = HIWORD(fileInfo->dwProductVersionMS); + *foundMinor = LOWORD(fileInfo->dwProductVersionMS); + *expectedMajor = 14; + *expectedMinor = 40; + bool ValidRuntime = + *foundMajor != *expectedMajor || *foundMinor >= *expectedMinor; + if (!ValidRuntime) { + DWORD Size = MAX_PATH; + char* ValidBuffer = WPI_AllocateString(runtimePath, Size); + DWORD RetVal = GetModuleFileNameA(msvcRuntimeModule, ValidBuffer, Size); + while (RetVal == Size) { + Size *= 2; + WPI_FreeString(runtimePath); + ValidBuffer = WPI_AllocateString(runtimePath, Size); + RetVal = GetModuleFileNameA(msvcRuntimeModule, ValidBuffer, Size); + } + runtimePath->len = RetVal; + } + return ValidRuntime ? 1 : 0; +} +#else +extern "C" int32_t WPI_IsRuntimeValid(uint32_t*, uint32_t*, uint32_t*, + uint32_t*, WPI_String*) { + return 1; +} +#endif diff --git a/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp b/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp index 15b78958ac..6eb87259ce 100644 --- a/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp +++ b/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp @@ -10,6 +10,7 @@ #include "wpi/DataLog.h" #include "wpi/FileLogger.h" #include "wpi/RawFrame.h" +#include "wpi/RuntimeCheck.h" #include "wpi/Synchronization.h" #include "wpi/jni_util.h" #include "wpi/print.h" @@ -25,13 +26,15 @@ static JException indexOobEx; static JException interruptedEx; static JException ioEx; static JException nullPointerEx; +static JException msvcRuntimeEx; static const JExceptionInit exceptions[] = { {"java/lang/IllegalArgumentException", &illegalArgEx}, {"java/lang/IndexOutOfBoundsException", &indexOobEx}, {"java/lang/InterruptedException", &interruptedEx}, {"java/io/IOException", &ioEx}, - {"java/lang/NullPointerException", &nullPointerEx}}; + {"java/lang/NullPointerException", &nullPointerEx}, + {"edu/wpi/first/util/MsvcRuntimeException", &msvcRuntimeEx}}; void wpi::ThrowIllegalArgumentException(JNIEnv* env, std::string_view msg) { illegalArgEx.Throw(env, msg); @@ -78,6 +81,34 @@ JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) { } } +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: checkMsvcRuntime + * Signature: ()V + */ +JNIEXPORT void JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_checkMsvcRuntime + (JNIEnv* env, jclass) +{ + uint32_t foundMajor; + uint32_t foundMinor; + uint32_t expectedMajor; + uint32_t expectedMinor; + WPI_String runtimePath; + + if (!WPI_IsRuntimeValid(&foundMajor, &foundMinor, &expectedMajor, + &expectedMinor, &runtimePath)) { + static jmethodID ctor = + env->GetMethodID(msvcRuntimeEx, "", "(IIIILjava/lang/String;)V"); + jstring jmsvcruntime = MakeJString(env, wpi::to_string_view(&runtimePath)); + jobject exception = + env->NewObject(msvcRuntimeEx, ctor, foundMajor, foundMinor, + expectedMajor, expectedMinor, jmsvcruntime); + WPI_FreeString(&runtimePath); + env->Throw(static_cast(exception)); + } +} + /* * Class: edu_wpi_first_util_WPIUtilJNI * Method: writeStderr diff --git a/wpiutil/src/main/native/include/wpi/RuntimeCheck.h b/wpiutil/src/main/native/include/wpi/RuntimeCheck.h new file mode 100644 index 0000000000..c3b223166f --- /dev/null +++ b/wpiutil/src/main/native/include/wpi/RuntimeCheck.h @@ -0,0 +1,21 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#include "wpi/string.h" + +#ifdef __cplusplus +extern "C" { +#endif + +int32_t WPI_IsRuntimeValid(uint32_t* FoundMajor, uint32_t* FoundMinor, + uint32_t* ExpectedMajor, uint32_t* ExpectedMinor, + WPI_String* RuntimePath); + +#ifdef __cplusplus +} // extern "C" +#endif