/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.jmeter.protocol.http.control;
import java.io.InterruptedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.commons.cli.avalon.CLArgsParser;
import org.apache.commons.cli.avalon.CLOption;
import org.apache.commons.cli.avalon.CLOptionDescriptor;
import org.apache.commons.cli.avalon.CLUtil;
import org.apache.jmeter.gui.Stoppable;
import org.apache.jorphan.util.JOrphanUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.config.Configurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Server daemon thread.
* Creates main socket and listens on it.
* For each client request, creates a thread to handle the request.
*
*/
public class HttpMirrorServer extends Thread implements Stoppable {
private static final int OPTIONS_OPT = '?';// $NON-NLS-1$
private static final int PORT_OPT = 'P';// $NON-NLS-1$
private static final int LOGLEVEL_OPT = 'L';// $NON-NLS-1$
/**
* Define the understood options.
*/
private static final CLOptionDescriptor D_OPTIONS_OPT =
new CLOptionDescriptor("?", CLOptionDescriptor.ARGUMENT_DISALLOWED, OPTIONS_OPT,
"print command line options and exit");
private static final CLOptionDescriptor D_PORT_OPT =
new CLOptionDescriptor("port", CLOptionDescriptor.ARGUMENT_REQUIRED, PORT_OPT,
"Set server port for HttpMirrorServer to use");
private static final CLOptionDescriptor D_LOGLEVEL_OPT =
new CLOptionDescriptor("loglevel", CLOptionDescriptor.DUPLICATES_ALLOWED
| CLOptionDescriptor.ARGUMENTS_REQUIRED_2, LOGLEVEL_OPT,
"[category=]level e.g. INFO or DEBUG");
private static final CLOptionDescriptor[] options = new CLOptionDescriptor[] {
D_OPTIONS_OPT,
D_PORT_OPT,
D_LOGLEVEL_OPT,
};
/**
* The time (in milliseconds) to wait when accepting a client connection.
* The accept will be retried until the Daemon is told to stop. So this
* interval is the longest time that the Daemon will have to wait after
* being told to stop.
*/
private static final int ACCEPT_TIMEOUT = 1000;
private static final long KEEP_ALIVE_TIME = 10;
/**
* Initialization On Demand Holder pattern
*/
private static class LazyHolder {
public static final Logger LOGGER = LoggerFactory.getLogger(HttpMirrorServer.class);
}
/** The port to listen on. */
private final int daemonPort;
/** True if the Daemon is currently running. */
private volatile boolean running;
// Saves the error if one occurs
private volatile Exception except;
/**
* Max Executor Pool size
*/
private int maxThreadPoolSize;
/**
* Max Queue size
*/
private int maxQueueSize;
/**
* Create a new Daemon with the specified port and target.
*
* @param port
* the port to listen on.
*/
public HttpMirrorServer(int port) {
this(port, HttpMirrorControl.DEFAULT_MAX_POOL_SIZE, HttpMirrorControl.DEFAULT_MAX_QUEUE_SIZE);
}
/**
* Create a new Daemon with the specified port and target.
*
* @param port
* the port to listen on.
* @param maxThreadPoolSize Max Thread pool size
* @param maxQueueSize Max Queue size
*/
public HttpMirrorServer(int port, int maxThreadPoolSize, int maxQueueSize) {
super("HttpMirrorServer");
this.daemonPort = port;
this.maxThreadPoolSize = maxThreadPoolSize;
this.maxQueueSize = maxQueueSize;
}
/**
* Listen on the daemon port and handle incoming requests. This method will
* not exit until {@link #stopServer()} is called or an error occurs.
*/
@Override
public void run() {
except = null;
running = true;
ServerSocket mainSocket = null;
ThreadPoolExecutor threadPoolExecutor = null;
if(maxThreadPoolSize>0) {
final ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(
maxQueueSize);
threadPoolExecutor = new ThreadPoolExecutor(
maxThreadPoolSize/2,
maxThreadPoolSize, KEEP_ALIVE_TIME, TimeUnit.SECONDS, queue);
threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
}
try {
getLogger().info("Creating HttpMirror ... on port " + daemonPort);
mainSocket = new ServerSocket(daemonPort);
mainSocket.setSoTimeout(ACCEPT_TIMEOUT);
getLogger().info("HttpMirror up and running!");
while (running) {
try {
// Listen on main socket
Socket clientSocket = mainSocket.accept();
if (running) {
// Pass request to new thread
if(threadPoolExecutor != null) {
threadPoolExecutor.execute(new HttpMirrorThread(clientSocket));
} else {
Thread thd = new Thread(new HttpMirrorThread(clientSocket));
getLogger().debug("Starting new Mirror thread");
thd.start();
}
} else {
getLogger().warn("Server not running");
JOrphanUtils.closeQuietly(clientSocket);
}
} catch (InterruptedIOException e) {
// Timeout occurred. Ignore, and keep looping until we're
// told to stop running.
}
}
getLogger().info("HttpMirror Server stopped");
} catch (Exception e) {
except = e;
getLogger().warn("HttpMirror Server stopped", e);
} finally {
if(threadPoolExecutor != null) {
threadPoolExecutor.shutdownNow();
}
JOrphanUtils.closeQuietly(mainSocket);
}
}
@Override
public void stopServer() {
running = false;
}
public Exception getException(){
return except;
}
public static void main(String[] args) {
CLArgsParser parser = new CLArgsParser(args, options);
String error = parser.getErrorString();
if (error != null) {
System.err.println("Error: " + error);//NOSONAR
System.out.println("Usage");//NOSONAR
System.out.println(CLUtil.describeOptions(options).toString());//NOSONAR
// repeat the error so no need to scroll back past the usage to see it
System.out.println("Error: " + error);//NOSONAR
return;
}
if (parser.getArgumentById(OPTIONS_OPT) != null) {
System.out.println(CLUtil.describeOptions(options).toString());//NOSONAR
return;
}
int port = HttpMirrorControl.DEFAULT_PORT;
if (parser.getArgumentById(PORT_OPT) != null) {
CLOption option = parser.getArgumentById(PORT_OPT);
String value = option.getArgument(0);
try {
port = Integer.parseInt(value);
} catch (NumberFormatException ignored) {
}
} else if (args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException ignored) {
}
}
if (System.getProperty("log4j.configurationFile") == null) {// $NON-NLS-1$
Configurator.setRootLevel(Level.INFO);
}
List<CLOption> clOptions = parser.getArguments();
for (CLOption option : clOptions) {
String name = option.getArgument(0);
String value = option.getArgument(1);
switch (option.getDescriptor().getId()) {
case LOGLEVEL_OPT:
if (!value.isEmpty()) { // Set category
final Level logLevel = Level.getLevel(value);
if (logLevel != null) {
String loggerName = name;
if (name.startsWith("jmeter") || name.startsWith("jorphan")) {
loggerName = "org.apache." + name;// $NON-NLS-1$
}
getLogger().info("Setting log level to " + value + " for " + loggerName);// $NON-NLS-1$ // $NON-NLS-2$
Configurator.setAllLevels(loggerName, logLevel);
} else {
getLogger().warn("Invalid log level, '" + value + "' for '" + name + "'.");// $NON-NLS-1$ // $NON-NLS-2$
}
} else { // Set root level
final Level logLevel = Level.getLevel(name);
if (logLevel != null) {
getLogger().info("Setting root log level to " + name);// $NON-NLS-1$
Configurator.setRootLevel(logLevel);
} else {
getLogger().warn("Invalid log level, '" + name + "' for the root logger.");// $NON-NLS-1$ // $NON-NLS-2$
}
}
break;
default:
break;
}
}
HttpMirrorServer serv = new HttpMirrorServer(port);
serv.start();
}
private static Logger getLogger() {
return LazyHolder.LOGGER;
}
}