Elevate Java application while running

2019-01-24 00:18发布

问题:

A nasty problem popped out with my software. I am making a program that interacts with another existing software (a game). User has reported that he runs the game with administrator privileges and under that circumstances, my program stops working for him.

Short investigation revealed that some people really need to run the game under administrator account and some don't. It would be great if my program would be able to detect this and warn user if the game is running under administrator account:

If the user clicks "Elevate", I'd like to ask windows to elevate the java.exe running my jar file and invoke the typical UAC dialog.


Obviously, this time the question would not be about java updater but JRE

My question is: Is this possible? Can windows elevate my java.exe instance's privilege? Does java have a way to do it? Or can I use command line command?

I want to avoid restarting the program (though it wouldn't probably be such a big deal).

Edit: If you look in the comments, you'll see that there's no avoiding the restart of an application - process can only start elevated, not become elevated. This kinda shifts the question, unfortunately. Basically, it now sounds more like: "How to restart my application with admin rights?". Unless, of course, there's a trick like two java.exe sharing one jar...

回答1:

If still of interest: In Windows 7 my JavaElevator works. It elevates a running Java process when used in the main method of the Java application. Simply add -elevate as last program parameter and use the elevator in the main method.

The elevator class:

package test;

import com.sun.jna.Native;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.Kernel32Util;
import com.sun.jna.platform.win32.ShellAPI;
import com.sun.jna.platform.win32.WinDef;

/**
 * Elevates a Java process to administrator rights if requested.
 */
public class JavaElevator {
    /** The program argument indicating the need of being elevated */
    private static final String ELEVATE_ARG = "-elevate";

    /**
     * If requested, elevates the Java process started with the given arguments to administrator level.
     * 
     * @param args The Java program arguments
     * @return The cleaned program arguments
     */
    public static String[] elevate(String[] args) {
        String[] result = args;

        // Check for elevation marker.  
        boolean elevate = false;
        if (args.length > 0) {
            elevate = args[args.length - 1].equals(ELEVATE_ARG);
        }
        if (elevate) {
            // Get the command and remove the elevation marker.
            String command = System.getProperty("sun.java.command");
            command = command.replace(ELEVATE_ARG, "");

            // Get class path and default java home.
            String classPath = System.getProperty("java.class.path");
            String javaHome = System.getProperty("java.home");
            String vm = javaHome + "\\bin\\java.exe";

            // Check for alternate VM for elevation. Full path to the VM may be passed with: -Delevation.vm=...
            if (System.getProperties().contains("elevation.vm")) {
                vm = System.getProperty("elevation.vm");
            }
            String parameters = "-cp " + classPath;
            parameters += " " + command;
                Shell32.INSTANCE.ShellExecute(null, "runas", vm, parameters, null, 0);

            int lastError = Kernel32.INSTANCE.GetLastError();
            if (lastError != 0) {
                String errorMessage = Kernel32Util.formatMessageFromLastErrorCode(lastError);
                errorMessage += "\n  vm: " + vm;
                errorMessage += "\n  parameters: " + parameters;
                throw new IllegalStateException("Error performing elevation: " + lastError + ": " + errorMessage);
            }
            System.exit(0);
        }
        return result;
    }
}

Usage in the main method of the Java application:

public static void main(String[] args) {
    String[] args1 = JavaElevator.elevate(args);
    if (args1.length > 0) {
       // Continue as intended.
       ... 

I know, this is a very basic implementation - sufficient for one of my daily hiccups: Starting an elevated process from Eclipse. But maybe it points someone in some dicrection...



回答2:

As has been pointed in comments, sadly the Java (or any other process) cannot be elevated while running. While in the case of JWM, it could be theoretically possible to move whole program context from normal user java.exe to elevated one, I don't think it's possible. I hope some day someone will come and tell me I'm wrong.

Surprisingly, even with restart in place, this was a tricky task that took me a while to figure out.

The non java part

First, how do we exactly run a program elevated from command line? There's an answer and you can see it's not simple. But we can break it to this VBS script:

Set UAC = CreateObject("Shell.Application") 
UAC.ShellExecute "program name", "command line parameters", "working directory", "runas", 1 

Soon, it also turns out that we won't have any success running java.exe from VBS script. In the end, I decided to run a helper batch file. Finally, here (answer to question in the last link) we have a complete set of two scripts which really run the given .jar file elevated. Here's improved version that allows quick testing by drag'n'dropping the Jar file on it:

' Require first command line parameter
if WScript.Arguments.Count = 0 then
  MsgBox("Jar file name required.")
  WScript.Quit 1
end if

' Get the script location, the directorry where it's running
Set objShell = CreateObject("Wscript.Shell")

strPath = Wscript.ScriptFullName

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.GetFile(strPath)
strFolder = objFSO.GetParentFolderName(objFile) 
'MsgBox(strFolder)

' Create the object that serves as runnable something
Set UAC = CreateObject("Shell.Application") 
' Args:
'   path to executable to run
'   command line parameters - first parameter of this file, which is the jar file name
'   working directory (this doesn't work but I use it nevertheless)
'   runas command which invokes elevation
'   0 means do not show the window. Normally, you show the window, but not this console window
'     which just blinks and disappears anyway
UAC.ShellExecute "run-normally.bat", WScript.Arguments(0), strFolder, "runas", 0 

WScript.Quit 0

The Java part

Java part is more straightforward. What we need to do is to open new process and execute the prepared scripts in it.

   /**
    * Start this very jar file elevated on Windows. It is strongly recommended to close any existing IO
    * before calling this method and avoid writing anything more to files. The new instance of this same
    * program will be started and simultaneous write/write or read/write would cause errors.
    * @throws FileNotFoundException if the helper vbs script was not found
    * @throws IOException if there was another failure inboking VBS script
    */
   public void StartWithAdminRights() throws FileNotFoundException, IOException {
     //The path to the helper script. This scripts takes 1 argument which is a Jar file full path
     File runAsAdmin = new File("run-as-admin.vbs");;
     //Our 
     String jarPath;

     //System.out.println("Current relative path is: " + s);

     try {
       jarPath = "\""+new File(Main.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getAbsolutePath()+"\"";
     } catch (URISyntaxException ex) {
       throw new FileNotFoundException("Could not fetch the path to the current jar file. Got this URISyntax exception:"+ex);
     }
     //If the jar path was created but doesn't contain .jar, we're (most likely) not running from jar
     //typically this happens when running the program from IDE
     //These 4 lines just serve as a fallback in testing, should be deleted in production
     //code and replaced with another FileNotFoundException
     if(!jarPath.contains(".jar")) {
       Path currentRelativePath = Paths.get("");
       jarPath = "\""+currentRelativePath.toAbsolutePath().toString()+"\\AutoClient.jar\"";
     }
     //Now we check if the path to vbs script exists, if it does we execute it
     if(runAsAdmin.exists()) {
       String command = "cscript \""+runAsAdmin.getAbsolutePath()+"\" "+jarPath;
       System.out.println("Executing '"+command+"'");
       //Note that .exec is asynchronous
       //After it starts, you must terminate your program ASAP, or you'll have 2 instances running
       Runtime.getRuntime().exec(command);

     }
     else
       throw new FileNotFoundException("The VBSScript used for elevation not found at "+runAsAdmin.getAbsolutePath());
   }


回答3:

This is my version. It creates a VBScript script, then executes it. This only works if the program that is being run is in a jar file, so you will have to run your IDE as administrator to actually test your program.

public static void relaunchAsAdmin() throws IOException {
    relaunchAsAdmin(ThisClass.class); //Change ThisClass to the class that this method is in
}

public static void relaunchAsAdmin(Class<?> clazz) throws IOException {
    if(isCurrentProcessElevated()) {
        return;
    }
    final String dir = System.getProperty("java.io.tmpdir");
    final File script = new File(dir, "relaunchAsAdmin" + System.nanoTime() +
            ".vbs");
    try {
        script.createNewFile();
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(script));
        osw.append("Set s=CreateObject(\"Shell.Application\")" + ln + "s.ShellExecute \"" +
                System.getProperty("java.home") + "\\bin\\java.exe" + "\",\"-jar \"\"" +
                new File(clazz.getProtectionDomain().getCodeSource(
                ).getLocation().toURI()).getAbsolutePath() + "\"\"\",,\"runas\",0" +
                        ln + "x=createObject(\"scripting.fileSystemObject\").deleteFile(" +
                        "WScript.scriptfullname)");
        osw.close();
        if(System.getenv("processor_architecture").equals("x86")) {
            Runtime.getRuntime().exec("C:\\Windows\\System32\\wscript.exe \"" +
                    script.getAbsolutePath() + "\"");
        } else {
            Runtime.getRuntime().exec("C:\\Windows\\SysWoW64\\wscript.exe \"" +
                    script.getAbsolutePath() + "\"");
        }
    } catch(URISyntaxException e) {
        e.printStackTrace();
    }
    Runtime.getRuntime().exit(0);
}

Note that it is a bit messy. I have been using this method before, so it has been line wrapped to 100 characters (except the comment I wrote for this answer). The

isCurrentProcessElevated()

method will have to be implemented in one way or another. You could try using JNI, or you could use a pure Java method, such as writing in the Program Files or System32 directory and seeing if it failed.

Obviously, this solution will only work on Windows. I never needed to elevate on Linux or Mac systems (mainly because I don't have any Mac systems, and I don't use Linux - I just play with it).