Is it possible to add a new path for native libraries at runtime ?.
(Instead of starting Java with the property java.library.path), so a call to System.loadLibrary(nativeLibraryName) will include that path when trying to find nativeLibraryName.
Is that possible or these paths are frozen once the JVM has started ?
[This solution don't work with Java 10+]
It seems impossible without little hacking (i.e. accessing private fields of the ClassLoader class)
This blog provide 2 ways of doing it.
For the record, here is the short version.
Option 1: fully replace java.library.path with the new value)
public static void setLibraryPath(String path) throws Exception {
System.setProperty("java.library.path", path);
//set sys_paths to null so that java.library.path will be reevalueted next time it is needed
final Field sysPathsField = ClassLoader.class.getDeclaredField("sys_paths");
sysPathsField.setAccessible(true);
sysPathsField.set(null, null);
}
Option 2: add a new path to the current java.library.path
/**
* Adds the specified path to the java library path
*
* #param pathToAdd the path to add
* #throws Exception
*/
public static void addLibraryPath(String pathToAdd) throws Exception{
final Field usrPathsField = ClassLoader.class.getDeclaredField("usr_paths");
usrPathsField.setAccessible(true);
//get array of paths
final String[] paths = (String[])usrPathsField.get(null);
//check if the path to add is already present
for(String path : paths) {
if(path.equals(pathToAdd)) {
return;
}
}
//add the new path
final String[] newPaths = Arrays.copyOf(paths, paths.length + 1);
newPaths[newPaths.length-1] = pathToAdd;
usrPathsField.set(null, newPaths);
}
I used this in Java 12/13 which should work for any JVM with MethodHandles:
Lookup cl = MethodHandles.privateLookupIn(ClassLoader.class, MethodHandles.lookup());
VarHandle sys_paths = cl.findStaticVarHandle(ClassLoader.class, "sys_paths", String[].class);
sys_paths.set(null);
It has the benefit of being a Java API.
It is replaces the:
final Field sysPathsField = ClassLoader.class.getDeclaredField("sys_paths");
sysPathsField.setAccessible(true);
sysPathsField.set(null, null);
Related
I am writing a program to allow my students to engage in a rudimentary AI game (similar to something IBM did years ago). The idea is pretty simple. Everyone has a project with the game jar, and their AI class MyAI.java (which implements AbstractAI). The structure is all working, they can write code into their AI class, and submit it to a common folder. The structure of the folder once a few students have submitted is:
school/stud1/MyAI.class
school/stud2/MyAI.class
I have also written code that I thought (in retrospect quite naively) would load and instantiate all of the classes into an ArrayList. The problem, is that I end up with an ArrayList of x instances of the current class.
I've found some similar questions, but the accepted answers did not work in this instance.
Some of the Loader class (not prettied up, it was just a proof of concept) is included below:
/**
* Load a single ai from a given location
* #param location The path where the ai is: example: c:\\tourney
* #param className The complete class: "org.mrd.Tournament.MyAI"
* #return The instance of AbstractAI loaded
*/
public static AbstractAI loadAI(String location, String className){
Object o = null;
try {
o = new URLClassLoader( new URL[]{new File(location).toURI().toURL()}
).loadClass(className).newInstance();
} catch ...{
}
if (o == null) return null;
return (AbstractAI)o;
}
/**
* Load all current files in tournament folder.
*/
public static ArrayList<AbstractAI> loadCurrentTourneyFiles(){
File dirs = new File("d:\\tourney\\school");
//list of all file names
ArrayList<String> names = new ArrayList<String>(Arrays.asList(dirs.list()));
//Create an arraylist for all loaded AIs and load them.
ArrayList<AbstractAI> arar = new ArrayList();
for (String dir:names){
arar.add(loadAI(dirs.getAbsolutePath() + "\\" + dir, "org.mrd.Tournament.MyAI"));
}
return arar;
}
Most relevant threads:
Java ClassLoader: load same class twice
Java - how to load different versions of the same class?
You can try to use compilation-toolbox, the idea is that you would try to load each of student jar with the following snippet:
JavaSourceCompiler javaSourceCompiler = new JavaSourceCompilerImpl();
JavaSourceCompiler.CompilationUnit compilationUnit = javaSourceCompiler.createCompilationUnit();
compilationUnit.addClassPathEntry("ai_student1.jar");
compilationUnit.addClassPathEntry("abstract_ai.jar");
String aiProvider = "package com.ai;\n" +
" import com.ai.student.AI;\n" +
"import com.ai.AbstractAI;\n" +
" public class AIProvider {\n" +
" public AbstractAI get() {\n" +
" return new AI();\n" +
" }\n\n" +
" }";
ClassLoader classLoader = javaSourceCompiler.compile(compilationUnit);
Class aIProvider = classLoader.loadClass("com.ai.Provider");
Due to a bug in the sigar library version I am using (returns bogus values for swap), I tried using com.sun.management.OperatingSystemMXBean instead. This worked fine and gave me the desired results (on Windows).
Class<?> sunMxBeanClass = Class.forName("com.sun.management.OperatingSystemMXBean");
sunMxBeanInstance = sunMxBeanClass.cast(ManagementFactory.getOperatingSystemMXBean());
getFreeSwapSpaceSize = getMethodWithName(sunMxBeanClass, "getFreeSwapSpaceSize");
getTotalSwapSpaceSize = getMethodWithName(sunMxBeanClass, "getTotalSwapSpaceSize");
However this breaks with java 9. Is there another way to query swap file / partition information using java? I don't want to introduce a new library or version of sigar.
Cross platform solutions appreciated but windows is enough :--)
Thanks
You may try to discover available MX attributes dynamically:
public class ExtendedOsMxBeanAttr {
public static void main(String[] args) {
String[] attr={ "TotalPhysicalMemorySize", "FreePhysicalMemorySize",
"FreeSwapSpaceSize", "TotalSwapSpaceSize"};
OperatingSystemMXBean op = ManagementFactory.getOperatingSystemMXBean();
List<Attribute> al;
try {
al = ManagementFactory.getPlatformMBeanServer()
.getAttributes(op.getObjectName(), attr).asList();
} catch (InstanceNotFoundException | ReflectionException ex) {
Logger.getLogger(ExtendedOsMxBeanAttr.class.getName())
.log(Level.SEVERE, null, ex);
al = Collections.emptyList();
}
for(Attribute a: al) {
System.out.println(a.getName()+": "+a.getValue());
}
}
}
There is no dependency to com.sun classes here, not even a reflective access.
The jdk.management module exports the com.sun.management API and it works the same way in JDK 9 as it did in JDK 8. So either of the following should work:
com.sun.management.OperatingSystemMXBean mbean
= (com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
long free = mbean.getFreePhysicalMemorySize();
long swap = mbean.getTotalSwapSpaceSize();
or
OperatingSystemMXBean mbean = ManagementFactory.getOperatingSystemMXBean();
Class<?> klass = Class.forName("com.sun.management.OperatingSystemMXBean");
Method freeSpaceMethod = klass.getMethod("getFreeSwapSpaceSize");
Method totalSpaceMethod = klass.getMethod("getTotalSwapSpaceSize");
long free = (long) freeSpaceMethod.invoke(mbean);
long swap = (long) totalSpaceMethod.invoke(mbean);
ADDED 7/23.
Many views: Not even a "that's dumb" question in response. Can anyone at least tell me why such an embarrassingly trivial question seems to have no answer anywhere.
Q:
--- Have Wildfly 8 running on local machine localhost:9990.
--- Have a Java program that need's Wildfly's IntialContext.
--- Every reference says use: "Context ctx = new InitialContext(env);"
--- Yet a week of searching turns up no set of properties that returns one.
And no example of a java program that gets one.
Does no one ever do this? Really need help
Original Msg Below
I know many people have asked how to get an Initial context from Wildfly 8. But I have yet to find a simple answer with a simple example.
Therefore, I hope someone can tell my why this doesn’t work.
I start Wildfly with standalone-full.xml
The three sections below have
A - Code summary of my test Class whose only purpose is to secure an Initial Context. (I only removed a lot of printing code that produced the next section.]
B - The Eclipse console output for a failure.
C - Cut and paste code. Just in case anyone can help me get this to work. I’d like to leave behind something the next new WF user can cut and past and run. The only difference from 1 above is that this version has all the static methods I used to format the output. NOTE: I know the comments I inserted about the less than sign sound dumb. BUT ... they are true.
A Code Summary
import java.util.Properties;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.InitialContext;
public class JmsTestGetJNDIContext {
//members
final private Properties env = new Properties() {
private static final long serialVersionUID = 1L;
{
/* These are Properties used by a standalone JavaClient to secure a WIldFly InitialContext()*/
put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.naming.remote.client.InitialContextFactory");
put(Context.PROVIDER_URL,"http-remoting://localhost:9990");
put(Context.SECURITY_PRINCIPAL,"userGLB");
put(Context.SECURITY_CREDENTIALS,"Open");
put("jboss.naming.client.ejb.context", true);
/*The above URL, ID and PW successfully open Wildfly's Admin Console*/
}
};
//constructor
private JmsTestGetJNDIContext (){
/*print "beg"*/
/*print "env"*/
try {
/*print "Requesting InitialContext"*/
Context ctx = new InitialContext(this.env);
/*print "JNDI Context: " + ctx)*/
/*print "end");
} catch (CommunicationException e) {
/* print "You forgot to start WildFly dummy!"*/
} catch (Exception e) {
/* print"caught: " + e.getClass().getName()*/
/*print e.getMessage()*/
/* "end")*/
}
static public void main (String[] args) {
/*print "beg"*/
JmsTestGetJNDIContext client = new JmsTestGetJNDIContext ();
/*print "end"*/
}
}
B - Console Output
JmsTestGetJNDIContext.main () beg
JmsTestGetJNDIContext.<init> () beg
JmsTestGetJNDIContext.<init> () These are Properties used to obtain IntialContext
Key: java.naming.provider.url
Value: http-remoting://localhost:9990
Key: java.naming.factory.initial
Value: org.jboss.naming.remote.client.InitialContextFactory
Key: jboss.naming.client.ejb.context
Value: true
Key: java.naming.security.principal
Value: userGLB
Key: java.naming.security.credentials
Value: Open
JmsTestGetJNDIContext.<init> () Requesting InitialContext
JmsTestGetJNDIContext.<init> () caught: javax.naming.NamingException
JmsTestGetJNDIContext.<init> () Failed to create remoting connection
JmsTestGetJNDIContext.<init> () end
JmsTestGetJNDIContext.main () end
Cut and Paste Code
package org.america3.gotest.xtra;
import java.util.Properties;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.InitialContext;
public class JmsTestGetJNDIContext {
//members
final private Properties env = new Properties() {
/**
* Properties used by a standalone JavaClient to secure
* a WIldFly InitialContext()*/
private static final long serialVersionUID = 1L;
{
put(Context.INITIAL_CONTEXT_FACTORY,"org.jboss.naming.remote.client.InitialContextFactory");
put(Context.PROVIDER_URL, "http-remoting://localhost:9990");
put(Context.SECURITY_PRINCIPAL, "userGLB");
put(Context.SECURITY_CREDENTIALS, "Open");
// The above URL, ID and PW successfully open Wildfly's Admin Console
put("jboss.naming.client.ejb.context", true);
}
};
//constructor
private JmsTestGetJNDIContext (){/*ignore*/String iAm = JmsTestGetJNDIContext.getIAm(" ", Thread.currentThread().getStackTrace());
P (iAm, "beg");
pProps(iAm, env);
try {
P (sp + iAm, "Requesting InitialContext");
Context ctx = new InitialContext(this.env);
P (sp + iAm, "JNDI Context: " + ctx);
P (sp + iAm, "end");
} catch (CommunicationException e) {
P (sp + iAm, "You forgot to start WildFly dummy!");
} catch (Exception e) {
P (sp + iAm, "caught: " + e.getClass().getName());
P (sp + iAm, e.getMessage());
P (iAm, "end");
}
}
static public void main (String[] args) {/*ignore*/String iAm = JmsTestGetJNDIContext.getIAm("",Thread.currentThread().getStackTrace());
P (iAm, "beg");
JmsTestGetJNDIContext client = new JmsTestGetJNDIContext ();
P (iAm , "end");
}
/*The remaining static methods are just to facilitate printing.
* They are normally in a Untility package I add to my projects.
* I put them here so this code would run for anyone.*/
static private void pProps (String leader, Properties p) {
StringBuffer sb = new StringBuffer ();
String s = JmsTestGetJNDIContext.padRight(leader, 45, ' ');
s = " " + s + "These are Properties used to obtain IntialContext"+"\n";
sb.append(s);
String skip = "";
for (Object key: p.keySet()) {
sb.append(skip + " " + JmsTestGetJNDIContext.padRight("\""
+ (String)key + "\"", 40, ' ')
+ " \"" + p.get(key) + "\"");
skip = "\n";
}
System.out.println(sb);
}
static private void P (String s, String s2) {
System.out.println(s + s2);
}
static public String getClassMethodName (StackTraceElement[] elements) {
String className = null;
for (int i = 0; i * elements.length; i++]i ) {
/* You need to type in a less than sign for the '*'
* because when I do, the editor will not show any code
* that comes after it.
* I have no idea why, but I've spent over an hour trying,
* and every time I type a less than sign all the following
* code dissappears!*/
className = elements[i].getClassName ();
if (className.startsWith ("org.america3")) {
int end = className.lastIndexOf ('.');
return className.substring (end + 1) + "." + elements[i].getMethodName ();
} else {
continue;
}
}
return "no project method found in elements beginning with org.america3" ;
}
static private String getIAm (String indent, StackTraceElement[] elements) {
StringBuffer sb = new StringBuffer ();
sb.append(JmsTestGetJNDIContext.getClassMethodName(elements));
sb.append(" ()");
return indent + JmsTestGetJNDIContext.padRight (sb.toString(), 45, ' ') ;
}
static public String padRight(String s, int width, char c){
if (s == null) return "Null String";
if(s.length() ** width){
/* You need to type in a greater than or equal sign for
* the '**'see above.*/
return s;
} else {
StringBuffer sb = new StringBuffer();
sb.append (s);
for(int i = 0; i *** (width - s.length()); i++){
/*You need to type in a less than sign the '***'. Again see above*/
sb.append(c);
}
return sb.toString();
}
}
static public String sp = " ";
}
A while ago I also struggled with remote EJBs in my CLI application. I excavated a small example project that I wrote then. It gets an InitialContext and calls a remote EJB named AddBrackets:
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import de.dnb.test.ejb.AddBrackets;
public final class Application {
public static void main(String[] args) throws NamingException {
final Properties jndiProperties = initJndiProperties();
final AddBrackets addBrackets = getEjb(jndiProperties);
System.out.println(addBrackets.processText("Hello World"));
}
private static Properties initJndiProperties() {
final Properties jndiProperties = new Properties();
jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY,
"org.jboss.naming.remote.client.InitialContextFactory");
jndiProperties.put("jboss.naming.client.ejb.context", true);
jndiProperties.put(Context.PROVIDER_URL, "http-remoting://localhost:8080/");
//jndiProperties.put(Context.SECURITY_PRINCIPAL, "test");
//jndiProperties.put(Context.SECURITY_CREDENTIALS, "test");
return jndiProperties;
}
private static AddBrackets getEjb(Properties jndiProps)
throws NamingException {
final Context jndiContext = new InitialContext(jndiProps);
final String interfaceName = AddBrackets.class.getName();
return (AddBrackets) jndiContext.lookup(
"ejbtest-app-1.0-SNAPSHOT/ejbtest-ejb-1.0-SNAPSHOT/AddBracketsBean!"
+ interfaceName);
}
}
I built this program as a Maven project which had a dependency on
<dependency>
<groupId>org.wildfly</groupId>
<artifactId>wildfly-ejb-client-bom</artifactId>
<version>8.2.1.Final</version>
<type>pom</type>
</dependency>
This dependency brings in Wildfly's remote client EJB implementation and adds the following jars to the class path (links are to Maven Central):
jboss-logging-3.1.4.GA.jar
jboss-marshalling-1.4.9.Final.jar
jboss-marshalling-river-1.4.9.Final.jar
jboss-remoting-4.0.7.Final.jar
jboss-sasl-1.0.4.Final.jar
jboss-ejb-api_3.2_spec-1.0.0.Final.jar
jboss-transaction-api_1.2_spec-1.0.0.Final.jar
xnio-api-3.3.0.Final.jar
xnio-nio-3.3.0.Final.jar
jboss-ejb-client-2.0.1.Final.jar
jboss-remote-naming-2.0.1.Final.jar
wildfly-build-config-8.2.1.Final.jar
I did no special configuration on Wildfly to run this example. I simply downloaded a vanilla Wildfly 8.2.1, unzipped it, set up an admin user with the add-user.sh script and deployed my EJB in an EAR. As you can see above access is granted without a username and a password.
You can find the complete project including the AddBrackets EJB on my bitbucket account.
When I tried to get my head around remote EJBs with Wildfly, I found the article JBoss EAP / Wildfly – Three ways to invoke remote EJBs really helpful. It clearly describes the three different methods to access remote EJBs on Wildfly.
According to your own answer the following jars are on your classpath:
jboss-remote-naming-1.0.7.final.jar
jboss-logging.jar
xnio-api-3.0.7.ga.jar
jboss-remoting-3.jar
jboss-ejb-client-1.0.19.final.jar
You write that the application throws the following exception:
java.lang.NoSuchMethodError:
org.jboss.remoting3.Remoting.createEndpoint(Ljava/lang/String;Lorg/xnio/OptionMap;)Lorg/jboss/remoting3/Endpoint;]
This exception is thrown when org.jboss.naming.remote.client.EndpointCache which is part of the jboss-remote-naming jar tries to call Remoting.createEndpoint() which is contained in the jboss-remoting jar.
As you explain in your answer the reason for this is that the Remoting class declares a 3-parameter version of the createEndpoint() method while the EndpointCache class tries to call a 2-parameter version which does not exist.
I checked the commit histories and declared dependencies of the jboss-remote-naming and the jboss-remoting projects to find out what is going wrong there. This is what I found out:
The 2-parameter version of createEndpoint() was only added in version 3.2 of jboss-remoting. The pom.xml for jboss-remote-naming-1.0.7.final says it depends on jboss-remoting 3.2.7.GA.
As there is no version number on your jboss-remoting-3.jar, I guess it is an older version. You should be able to check this by looking for a pom.xml in META-INF folder of your jboss-remoting-3.jar. This should contain the version number.
To solve your problem, I suggest to replace your jboss-remoting-3.jar with jboss-remoting-3.2.7ga.jar or to use the set of jars I listed in my other answer.
I’ve decided the problem isn’t coding or the JNDI InititialContext Properties.
I mean the fatal error is a NoSuchMethodError. Therefore, as I confirmed in the WildFly server logs, my main method never even tries to connect.
Here’s what I think explains the real problem.
And I think it explains why there are so many calls for help with this error:
java.lang.NoSuchMethodError:
org.jboss.remoting3.Remoting.createEndpoint(Ljava/lang/String;Lorg/xnio/OptionMap;)Lorg/jboss/remoting3/Endpoint;]
Also why none of those calls for help ever get a conclusive answer. Just people suggesting different jars.
And since all those answers fixed on jars, this is how I tested the Build Path I was using:
First I removed all jars from the Build Path. Then I ran my one line main program till all ClassNotFoundException were gone.
First Error
java.lang.ClassNotFoundException:
org.jboss.naming.remote.client.InitialContextFactory]
Added jboss-remote-naming-1.0.7.final.jar to class path
Next Error
java.lang.NoClassDefFoundError:
org/jboss/logging/Logger
Added jboss-logging.jar
Next Error
java.lang.NoClassDefFoundError:
org/xnio/Options
Added xnio-api-3.0.7.ga.jar
Next Error
java.lang.NoClassDefFoundError:
org/jboss/remoting3/spi/ConnectionProviderFactory
Added jboss-remoting-3.jar
Next Error
java.lang.NoClassDefFoundError:
org/jboss/ejb/client/EJBClientContextIdentifier
Added jboss-ejb-client-1.0.19.final.jar
FATAL ERROR (note: All NoClassDefFoundError have been cleared)
java.lang.NoSuchMethodError:
org.jboss.remoting3.Remoting.createEndpoint(Ljava/lang/String;Lorg/xnio/OptionMap;)Lorg/jboss/remoting3/Endpoint;]
Then I used Eclipse’s Project Explorer to verify:
That jboss-remoting3.jar has the org.jboss.remoting3.Remoting Class. It does. That’s why there is no NoClassDefFoundError left above.
And verified it had this method:
public Endpoint createEndpoint (String, Executor, OptionMap) note: 3 parameters.
BUT the above Error indicates something is calling:
public Endpoint createEndpoint (String, OptionMap) note: 2 parameters.
That’s why the program throws a NoSuchMethodError. It is looking for a 2 paramater version of org.jboss.remoting3.Remoting.createEndpoint(). And the Remoting Class I have only has a 3 parameter version.`
I know this sounds impossible but the only thing I can think is there is an inconsistency in the Java API???
Clearly something is calling org.jboss.remoting3.Remoting.createEndpoint with 2 parameters.
But my org.jboss.remoting3.Remoting Class only has a 3 parameter version of the createEndpoint() Method.
So I’m going to clean this all up and repost a question asking how to explain the existence of a Class calling for a 2 paramter org.jboss.remoting3.Remoting.createEndpoint Method when I have a jar whose org.jboss.remoting3.Remoting only offers a 3-parameter.
Here is your obligatory "that's a dumb question." Does the wildfly remote quickstart github repo answer the question for you? Their code, from RemoteEJB.java
final Hashtable<String, String> jndiProperties = new Hashtable<>();
jndiProperties.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
final Context context = new InitialContext(jndiProperties);
return (RemoteCalculator) context.lookup("ejb:/ejb-remote-server-side/CalculatorBean!" + RemoteCalculator.class.getName());
question
How can I ensure my install4j installer always finds only its java?
Can I create a top level installer which installs JRE to tmp, sets env variables and then starts the actual installer?
Can I load a vm file during installation?
problem
Install4j finds java 1.7 during install which impacts custom code preventing successful installation. I see found java7 prior to file deployment - ok expected given the JRE hasn't yet been unpacked.
evidence
I created a simple installer and see the following:
BEFORE
PATH=/opt/tools/Java/jdk1.7.0_79/bin:...
JAVA_HOME=/opt/tools/Java/jdk1.7.0_79
...
ENV [JAVA_HOME] /opt/tools/Java/jdk1.7.0_79
ENV [PATH] /opt/tools/Java/jdk1.7.0_79/bin:...
installer details
envTest.install4j
Optional customer install script reporting found java prior at execution start
echo BEFORE
echo PATH=$PATH
echo JAVA_HOME=$JAVA_HOME
echo Version: java -version
Run script reporting env after installer deployed jre
`
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
Map<String, String> envMap = System.getenv();
SortedMap<String, String> sortedEnvMap = new TreeMap<String, String>(envMap);
Set<String> keySet = sortedEnvMap.keySet();
for (String key : keySet) {
String value = envMap.get(key);
Util.logInfo(this,"ENV [" + key + "] " + value);
}
return true;
Actually, this turned our to be a problem with my custom code. The custom code launches an install4j generated executable via java. When launched on command line with wrong java found first, the launcher uses only its own java. When launched from my extension it fails.
Solution - set java in my extension:
private File getInstalledJREDir() {
return new File(installationDir, "jre");
}
private String addJREToFrontOfPathVar() {
File jreBinDir = new File(getInstalledJREDir(), "bin");
String path = System.getenv().get("PATH");
if (null == path) {
path = jreBinDir.getAbsolutePath();
} else {
path = jreBinDir.getAbsolutePath() + File.pathSeparator + path;
}
return path;
}
/**
* Start Laucnher and block until it starts or timeout reached
* #throws AutoRunException
*/
public void run() throws AutoRunException, IOException, InterruptedException {
notifier.setPhase("Starting Agent");
// Set Directories
File dataDir = new File(installationDir.getParentFile(), "data-agent");
File agentLog = new File(logDir,"agent.log");
if (! isWindows()) {
File agent = new File(installationDir, "bin/launcherExecutable");
CmdExecutor ce = new CmdExecutor(agent, agentLog);
// Ensure our installed JRE found 1st - PLAT-38833
ce.updateEnvironmentVariable("JAVA_HOME", getInstalledJREDir().getAbsolutePath());
ce.updateEnvironmentVariable("PATH", addJREToFrontOfPathVar());
ce.setWorkingDir(installationDir);
ce.setArgLine(String.format("--datadir %s", dataDir.getAbsolutePath()));
notifier.logInfo("Starting " + agent + " with " + ce.getArgLine());
if (! ce.run(true) ) {
throw new AutoRunException("Agent failed to start " + ce.getOutput());
}
We have an application based on the netbeans rich client platform.
Default behaviour is that only one instance of the software can run, and this can be overridden by specifying a different user-dir as argument when starting the application.
Are there any alternatives to this? Our clients rely on being able to run several instances of the application.
(We would prefer if we didnt have to implement our own launcher that examines netbeans userdir for locks to pass alternative userdirs)
Is there a build-in way in the netbeans RCP to easily support multiple instances?
cheers
/B
So, unable to find a proper way to do this (seems like it will be available with nb 7 though through a new launcher input parameter), my workmate implemented it in a somewhat hackish way.
He created an alternative main-class, which checks and manipulates the user-dir property so each instance gets its own userdir.
Im still surprised that netbeans does not have this functionality!
import java.io.File;
import org.netbeans.Main;
/**
* Launches Netbeans Rich Client Platform application where mulitple instances of
* the application can run simultaneously for the same user.
* <p>
* Netbeans does not allow to run several instances of an application for the same
* user simultaneously, but this class does the job by fooling Netbeans.
* If an instance is already running then a new folder is created and used for this instance.
* </p>
* <p>
* This is quite tricky and this class is indeed a hack to override Netbeans behaviour.
* An example makes it a little easier to understand, when application is first started
* Netbeans userdir is set from a configuration file like {#code etc/xxxxx.conf} to something like:<br>
* {#code /home/username/.xxxxx/dev/} which includes a lock file.
* <br>
* If application is started again then this lock file is checked and Netbeans tries to connect to the other instance through a socket.
* This class does the trick by never using this folder but instead creating unique directories for each instance like:<br>
* {#code /home/username/.xxxxx/instance_01/netbeans/}<br>
* {#code /home/username/.xxxxx/instance_02/netbeans/}<br>
* {#code /home/username/.xxxxx/instance_03/netbeans/}<br>
* ...
* </p>
*
*/
public class MultipleInstancesMain
{
/** Key for Netbeans default user dir */
private static final String NETBEANS_USER = "netbeans.user";
/** Argument to Netbeans for alternate user dir */
private static final String USERDIR_ARG = "--userdir";
/** Like "C:\Documents and Settings\username\Application Data\.xxxxx" or "/home/username/.xxxxx" */
private static final File MAIN_DIR = getMainDir();
/** Sub dir of MAIN_DIR for each instance of application */
private static final String INSTANCE_DIR = "instance";
/** Sub dir of INSTANCE_DIR where netbeans stores it's settings, logs and cache */
private static final String NETBEANS_SUBDIR = "netbeans";
/** Name of Netbeans lock file inside of NETBEANS_SUBDIR */
private static final String LOCKFILE = "lock";
/** Max number of instance directories we allow */
private static final int MAX_DIR_COUNT = 999;
/** Padding of instance dir */
private static final String PAD = "000";
private static final int PAD_LENGTH = PAD.length();
/**
* Lock file could be present even though application is not running (after crash).
* So we treat it as "dead" after some time, to prevent "dead directories".
*/
private static final long LOCKFILE_MAX_AGE_IN_MILLIS = 1000L * 60L * 60L * 24L * 30L; // 30 days
/**
* Launches application with multiple instances support.
* #param args command line arguments
*/
public static void main(String[] args) throws Exception
{
// Get directory for this instance
String[] userDir = new String[2];
userDir[0] = USERDIR_ARG;
userDir[1] = getNetbeansDir();
// Remove default dir and set this class not to run again
deleteDefaultNetbeansDir();
System.clearProperty("netbeans.mainclass");
// Start Netbeans again with new userdir and default main class
startNetbeans(args, userDir);
}
/**
* Starts Netbeans.
* #param oldArgs command line arguments
* #param newArgs new arguments added
*/
private static void startNetbeans(String[] oldArgs, String[] newArgs) throws Exception
{
String[] args = new String[oldArgs.length + newArgs.length];
for (int i = 0; i <oldArgs.length; i++)
{
args[i] = oldArgs[i];
}
for (int i = 0; i < newArgs.length; i++)
{
args[oldArgs.length + i] = newArgs[i];
}
Main.main(args);
}
/**
* #return the directory that Netbeans will use for this instance of the application
*/
private static String getNetbeansDir()
{
for(int i = 1; i <= MAX_DIR_COUNT; i++)
{
File instanceDir = getSuffixedInstanceDir(i);
if (isLockFileFree(instanceDir))
{
File dirToUse = new File(instanceDir, NETBEANS_SUBDIR);
return dirToUse.getAbsolutePath();
}
}
// This would probably never happen, but we don't want an eternal loop above
String errorMessage = String.format("Unable to find Netbeans userdir, %s dirs checked in '%s'",
MAX_DIR_COUNT, MAIN_DIR.getAbsolutePath());
throw new RuntimeException(errorMessage);
}
private static File getSuffixedInstanceDir(int count)
{
String suffix = PAD + count;
suffix = suffix.substring(suffix.length() - PAD_LENGTH);
File suffixedDir = new File(MAIN_DIR, INSTANCE_DIR + "_" + suffix);
return suffixedDir;
}
/**
* Checks is if Netbeans lock file is free.
* #param instanceDir directory to look for Netbeans directory and lock file in
* #return true if {#code instanceDir} does not have a Netbeans folder with a occupied lock file
*/
private static boolean isLockFileFree(File instanceDir)
{
File netbeansDir = new File(instanceDir, NETBEANS_SUBDIR);
File lockFile = new File(netbeansDir, LOCKFILE);
if (lockFile.exists())
{
return deleteLockFileIfOldEnough(lockFile);
}
else
{
return true;
}
}
/**
* Deletes the lock file if it's old enough.
* #param lockFile lock file to delete
* #return true if it was deleted
* #see #LOCKFILE_MAX_AGE_IN_MILLIS
*/
private static boolean deleteLockFileIfOldEnough(File lockFile)
{
long currentTime = System.currentTimeMillis();
long fileCreated = lockFile.lastModified();
long ageInMillis = currentTime - fileCreated;
if (ageInMillis > LOCKFILE_MAX_AGE_IN_MILLIS)
{
return lockFile.delete();
}
else
{
return false;
}
}
/**
* Netbeans is started with a default userdir, but we need to have a unique dir for each instance.
* Main dir is the directory where directories of all instances are.
* #return main directory in users home area where application settings, logs and cache is stored
*/
private static File getMainDir()
{
String defaultNetbeansDir = System.getProperty(NETBEANS_USER);
File mainDir = new File(defaultNetbeansDir).getParentFile();
return mainDir;
}
/**
* Since we don't use default Netbeans dir at all, we remove it completely.
*/
private static void deleteDefaultNetbeansDir()
{
File defaultNetbeansDir = new File(System.getProperty(NETBEANS_USER));
Thread t = new Thread(new DirectoryDeleter(defaultNetbeansDir), "NetbeansDirDeleter");
t.start();
}
/**
* There are unpredictable behaviour when deleting Netbeans default directory, sometime it succeeds and sometimes not.
* But after some attempts it always succeeds, by deleting it in the background it will eventually be deleted.
* #author username
*/
private static class DirectoryDeleter implements Runnable
{
private static final long SLEEP_TIME = 3000;
private final File dirToDelete;
DirectoryDeleter(File dirToDelete)
{
this.dirToDelete = dirToDelete;
}
/**
* #see java.lang.Runnable#run()
*/
public void run()
{
while(!deleteDirOrFile(dirToDelete))
{
try
{
Thread.sleep(SLEEP_TIME);
}
catch (InterruptedException e)
{
// No idea to do anything here, should never happen anyway...
continue;
}
}
}
/**
* Deletes a file or directory
* #param dirFile directory or file to delete
* #return true if deletion succeeded
*/
private boolean deleteDirOrFile(File dirFile)
{
if (dirFile.isDirectory())
{
for (File f : dirFile.listFiles())
{
boolean deleted = deleteDirOrFile(f);
if (!deleted)
{
return false;
}
}
}
// The directory is now empty so delete it
return dirFile.delete();
}
}
}
I'm the mysterious developer who wrote the MultipleInstancesMain class above =)
As answer to the questions by Swati Sharma and uvaraj above, these notes might help:
(1) The alternative main class above should work for applications built on Netbeans Platform 6.5.1 (our does), other versions I don't know about.
(2) We keep this class in a tiny project that is built by the main build.xml ant script of our application (the one in the "suite" module). The jar is then copied to the /[appname]/modules/ folder (so it's on Netbeans classpath)
(3) The separate project can NOT be a Netbeans module, I guess there will be cyclic dependency or Netbeans classloader can't handle it or something like that.
(4) Our application is then started by adding parameter to configuration file /[appname]/etc/.conf:
default_options="-J-Dnetbeans.mainclass=com.appname.startup.MultipleInstancesMain"
Hope it helps
// Uhlén