/* * Created on May 1, 2007 * Created by Paul Gardner * Copyright (C) 2007 Aelitis, All Rights Reserved. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * AELITIS, SAS au capital de 63.529,40 euros * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France. * */ package com.aelitis.azureus.core.networkmanager.admin.impl; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.lang.reflect.Field; import java.net.URL; import java.net.URLClassLoader; import java.net.URLEncoder; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.gudy.azureus2.core3.config.COConfigurationManager; import org.gudy.azureus2.core3.config.ParameterListener; import org.gudy.azureus2.core3.config.impl.TransferSpeedValidator; import org.gudy.azureus2.core3.torrent.TOTorrent; import org.gudy.azureus2.core3.torrent.TOTorrentFactory; import org.gudy.azureus2.core3.util.AEThread; import org.gudy.azureus2.core3.util.BDecoder; import org.gudy.azureus2.core3.util.BEncoder; import org.gudy.azureus2.core3.util.Constants; import org.gudy.azureus2.core3.util.Debug; import org.gudy.azureus2.core3.util.FileUtil; import org.gudy.azureus2.core3.util.SystemProperties; import org.gudy.azureus2.core3.internat.MessageText; import org.gudy.azureus2.plugins.PluginInterface; import org.gudy.azureus2.plugins.download.Download; import org.gudy.azureus2.plugins.download.DownloadManagerListener; import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloader; import org.gudy.azureus2.pluginsimpl.local.PluginConfigImpl; import org.gudy.azureus2.pluginsimpl.local.utils.resourcedownloader.ResourceDownloaderFactoryImpl; import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTestScheduledTest; import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTestScheduledTestListener; import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTestScheduler; import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTester; import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTesterListener; import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTesterResult; import com.aelitis.azureus.core.util.CopyOnWriteList; import com.aelitis.azureus.plugins.upnp.UPnPPlugin; public class NetworkAdminSpeedTestScheduledTestImpl implements NetworkAdminSpeedTestScheduledTest { //Types of requests sent to SpeedTest scheduler. private static final long REQUEST_TEST = 0; private static final long CHALLENGE_REPLY = 1; private static final long TEST_RESULT = 2; private static int ZERO_DOWNLOAD_SETTING = -1; private PluginInterface plugin; private NetworkAdminSpeedTesterImpl tester; private String detectedRouter; private SpeedTestDownloadState preTestSettings; private byte[] challenge_id; private long delay_millis; private long max_speed; private TOTorrent test_torrent; private volatile boolean aborted; private CopyOnWriteList listeners = new CopyOnWriteList(); protected NetworkAdminSpeedTestScheduledTestImpl( PluginInterface _plugin, NetworkAdminSpeedTesterImpl _tester ) { plugin = _plugin; tester = _tester; //detect the router. PluginInterface upnp = plugin.getPluginManager().getPluginInterfaceByClass( UPnPPlugin.class ); if( upnp != null ){ detectedRouter = upnp.getPluginconfig().getPluginStringParameter("plugin.info"); } tester.addListener( new NetworkAdminSpeedTesterListener() { public void complete( NetworkAdminSpeedTester tester, NetworkAdminSpeedTesterResult result ) { try{ sendResult( result ); }finally{ reportComplete(); } } public void stage( NetworkAdminSpeedTester tester, String step ) { } }); } public NetworkAdminSpeedTester getTester() { return( tester ); } public long getMaxUpBytePerSec() { return( max_speed ); } public long getMaxDownBytePerSec() { return( max_speed ); } public boolean start() { if ( schedule()){ new AEThread( "NetworkAdminSpeedTestScheduledTest:delay", true ) { public void runSupport() { long delay_ticks = delay_millis/1000; for (int i=0;i<delay_ticks;i++){ if ( aborted ){ break; } String testScheduledIn = MessageText.getString( "SpeedTestWizard.abort.message.scheduled.in" , new String[]{""+(delay_ticks - i)} ); reportStage( testScheduledIn ); try{ Thread.sleep(1000); }catch( InterruptedException e ){ e.printStackTrace(); } } if ( !aborted ){ setSpeedLimits(); if ( tester.getTestType() == NetworkAdminSpeedTestScheduler.TEST_TYPE_BT ){ ((NetworkAdminSpeedTesterBTImpl)tester).start( test_torrent ); }else{ String unsupportedType = MessageText.getString("SpeedTestWizard.abort.message.unsupported.type"); tester.abort(unsupportedType); } } } }.start(); return( true ); }else{ return( false ); } } public void abort() { abort( MessageText.getString("SpeedTestWizard.abort.message.manual.abort") ); } public void abort( String reason ) { if ( !aborted ){ aborted = true; tester.abort( reason ); } } /** * Request a test from the speed testing service, handle the "challenge" if request and then get * the id for the test. * * Per spec all request are BEncoded maps. * * @return boolean - true if the test has been reserved with the service. */ private boolean schedule() { try{ //lookup UPnP devices found. One might be a router. //Send "schedule test" request. Map request = new HashMap(); request.put("request_type", new Long(REQUEST_TEST) ); String id = COConfigurationManager.getStringParameter("ID","unknown"); // get jar file its version for the test File jar_file = null; String jar_version = null; String explicit_path = System.getProperty( "azureus.speed.test.challenge.jar.path", null ); if ( explicit_path != null ){ File f = new File( explicit_path ); if ( f.exists()){ String v = getVersionFromJAR( f ); if ( v != null ){ jar_file = f; jar_version = v; System.out.println( "SpeedTest: using explicit challenge jar " + jar_file.getAbsolutePath() + ", version " + jar_version ); } } } if ( jar_file == null ){ String debug = System.getProperty("debug.speed.test.challenge","n"); if( !debug.equals( "n" )){ //over-ride the jar version, and location for debugging. File f = new File( "C:\\test\\azureus\\Azureus3.0.1.2.jar" ); //ToDo: make this a -D option with this default. if ( f.exists()){ jar_file = f; jar_version = "3.0.1.2"; System.out.println( "SpeedTest: using old spec challenge jar " + jar_file.getAbsolutePath() + ", version " + jar_version ); } } } if ( jar_file == null ){ jar_file = FileUtil.getJarFileFromClass( getClass()); if ( jar_file != null ){ jar_version = Constants.AZUREUS_VERSION; // System.out.println( "SpeedTest: using class-based challenge jar " + jar_file.getAbsolutePath() + ", version " + jar_version ); }else{ File f = new File( SystemProperties.getAzureusJarPath()); if ( f.exists()){ jar_version = Constants.AZUREUS_VERSION; jar_file = f; // System.out.println( "SpeedTest: using config-based challenge jar " + jar_file.getAbsolutePath() + ", version " + jar_version ); } } } if ( jar_file == null ){ throw( new Exception( "Failed to locate an 'Azureus2.jar' to use for the challenge protocol" )); } //ToDo: remove once challenge testing is done. request.put("az-id",id); //Where to I get the AZ-ID and client version from the Configuration? request.put("type","both"); request.put("jar_ver",jar_version); if ( detectedRouter != null ){ request.put( "router", detectedRouter ); } Map result = sendRequest( request ); challenge_id = (byte[]) result.get("challenge_id"); if( challenge_id == null ){ throw new IllegalStateException("No challenge returned from speed test scheduling service"); } Long responseType = (Long) result.get("reply_type"); if( responseType.intValue()==1 ){ //a challenge has occured. result = handleChallengeFromSpeedTestService( jar_file, result ); responseType = (Long) result.get("reply_type"); } if( responseType == null ){ throw new IllegalStateException("No challenge response returned from speed test scheduling service"); } if( responseType.intValue()==0 ){ //a test has been scheduled. //set the Map properly. Long time = (Long) result.get("time"); Long limit = (Long) result.get("limit"); if( time==null || limit==null ){ throw new IllegalArgumentException("Returned time or limit parameter is null"); } delay_millis = time.longValue(); max_speed = limit.longValue(); // this is test-specific data Map torrentMap = (Map)result.get("torrent"); test_torrent = TOTorrentFactory.deserialiseFromMap(torrentMap); return( true ); }else{ throw new IllegalStateException( "Unrecognized response from speed test scheduling servcie." ); } }catch( Throwable t ){ Debug.printStackTrace(t); tester.abort( MessageText.getString("SpeedTestWizard.abort.message.scheduling.failed"), t ); return( false ); } } private String getVersionFromJAR( File jar_file ) { try{ // force the URLClassLoader to load from the URL and not delegate and find the currently // jar's Constants ClassLoader parent = new ClassLoader() { protected synchronized Class loadClass( String name, boolean resolve ) throws ClassNotFoundException { if ( name.equals( "org.gudy.azureus2.core3.util.Constants")){ throw( new ClassNotFoundException()); } return( super.loadClass( name, resolve )); } }; ClassLoader cl = new URLClassLoader(new URL[]{jar_file.toURI().toURL()}, parent); Class c = cl.loadClass( "org.gudy.azureus2.core3.util.Constants"); Field field = c.getField( "AZUREUS_VERSION" ); return((String)field.get( null )); }catch( Throwable e){ return( null ); } } /** * * @param jar_file - File Azureus jar used to load classes. * @param result - Map from the previous response * @return Map - from the current response. */ private Map handleChallengeFromSpeedTestService( File jar_file, Map result ) throws IOException { //verify the following items are in the response. //size (in bytes) //offset (in bytes) //challenge_id Map retVal = new HashMap(); RandomAccessFile raf=null; try{ Long size = (Long) result.get("size"); Long offset = (Long) result.get("offset"); if( size==null || offset==null ) throw new IllegalStateException("scheduleTestWithSpeedTestService had a null parameter."); //read the bytes raf = new RandomAccessFile( jar_file, "r" ); byte[] jarBytes = new byte[size.intValue()]; raf.seek(offset.intValue()); raf.read( jarBytes ); //Build the URL. Map request = new HashMap(); request.put("request_type", new Long(CHALLENGE_REPLY) ); request.put("challenge_id", challenge_id ); request.put("data",jarBytes); retVal = sendRequest( request ); }finally{ //close try{ if(raf!=null) raf.close(); }catch(Throwable t){ Debug.printStackTrace(t); } } return retVal; } private void sendResult( NetworkAdminSpeedTesterResult result ) { try{ if ( challenge_id != null ){ Map request = new HashMap(); request.put("request_type", new Long(TEST_RESULT) ); request.put("challenge_id", challenge_id ); request.put( "type", new Long( tester.getTestType())); request.put( "mode", new Long( tester.getMode())); request.put( "crypto", new Long( tester.getUseCrypto()?1:0)); if ( result.hadError()){ request.put( "result", new Long(0)); request.put( "error", result.getLastError()); }else{ request.put( "result", new Long(1)); request.put( "maxup", new Long(result.getUploadSpeed())); request.put( "maxdown", new Long(result.getDownloadSpeed())); } sendRequest( request ); } }catch( Throwable e ){ Debug.printStackTrace(e); } } private Map sendRequest( Map request ) throws IOException { request.put( "ver", new Long(1) );//request version request.put( "locale", MessageText.getCurrentLocale().toString()); String speedTestServiceName = System.getProperty( "speedtest.service.ip.address", Constants.SPEED_TEST_SERVER ); URL urlRequestTest = new URL("http://"+speedTestServiceName+":60000/scheduletest?request=" + URLEncoder.encode( new String(BEncoder.encode(request),"ISO-8859-1"),"ISO-8859-1")); return( getBEncodedMapFromRequest( urlRequestTest )); } /** * Read from URL and return byte array. * @param url - * @return byte[] of the results. Max size currently 100k. * @throws java.io.IOException - */ private Map getBEncodedMapFromRequest(URL url) throws IOException { ResourceDownloader rd = ResourceDownloaderFactoryImpl.getSingleton().create( url ); InputStream is=null; Map reply = new HashMap(); try { is = rd.download(); reply = BDecoder.decode( new BufferedInputStream(is) ); //all replys of this type contains a "result" Long res = (Long) reply.get("result"); if(res==null) throw new IllegalStateException("No result parameter in the response!! reply="+reply); if(res.intValue()==0){ StringBuffer msg = new StringBuffer("Server failed. "); String error = new String( (byte[]) reply.get("error") ); String errDetail = new String( (byte[]) reply.get("error_detail") ); msg.append("Error: ").append(error); // detail is of no interest to the user // msg.append(" ,error detail: ").append(errDetail); Debug.outNoStack( "SpeedCheck server returned an error: " + error + ", details=" + errDetail ); throw new IOException( msg.toString() ); } }catch(IOException ise){ //rethrow this type of exception. throw ise; }catch(Throwable t){ Debug.out(t); Debug.printStackTrace(t); }finally{ try{ if(is!=null) is.close(); }catch(Throwable e){ Debug.printStackTrace(e); } } return reply; }//getBytesFromRequest /** * Restore all the downloads the state before the speed test started. */ protected synchronized void resetSpeedLimits() { if ( preTestSettings != null ){ preTestSettings.restoreLimits(); preTestSettings = null; } } /** * Preserve all the data about the downloads while the test is running. */ protected synchronized void setSpeedLimits(){ // in case we've already saved limits resetSpeedLimits(); preTestSettings = new SpeedTestDownloadState(); preTestSettings.saveLimits(); } // --------------- HELPER CLASSES BELOW HERE ---------------- // /** * Preservers the state of all the downloads before the speed test started. */ class SpeedTestDownloadState implements ParameterListener, DownloadManagerListener { private Map torrentLimits = new HashMap(); //Map <Download , Map<String,Integer> > public static final String TORRENT_UPLOAD_LIMIT = "u"; public static final String TORRENT_DOWNLOAD_LIMIT = "d"; //global limits. int maxUploadKbs; int maxUploadSeedingKbs; int maxDownloadKbs; boolean autoSpeedEnabled; boolean autoSpeedSeedingEnabled; boolean LANSpeedEnabled; public SpeedTestDownloadState() { } public void parameterChanged( String name ) { // add some trace so we have some clue as to what has made the change! String trace = Debug.getCompressedStackTrace(); abort( "Configuration parameter '" + name + "' changed (new value=" + COConfigurationManager.getParameter( name ) + ") during test (" + trace + ")" ); } public void downloadAdded( Download download ) { if ( test_torrent != null ){ try{ if ( Arrays.equals( download.getTorrent().getHash(), test_torrent.getHash())){ return; } }catch( Throwable e ){ Debug.printStackTrace(e); } } String downloadAdded = MessageText.getString("SpeedTestWizard.abort.message.download.added" , new String[]{download.getName()}); abort(downloadAdded); } public void downloadRemoved( Download download ) { } public void saveLimits() { // a bunch of plugins mess with limits (AutoSpeed, Shaper, SpeedScheduler...) - disable their // ability to mess with config during the test PluginConfigImpl.setEnablePluginCoreConfigChange( false ); plugin.getDownloadManager().addListener( this, false ); //preserve the limits for all the downloads and set each to zero. Download[] d = plugin.getDownloadManager().getDownloads(); if(d!=null){ int len = d.length; for(int i=0;i<len;i++){ plugin.getDownloadManager().getStats(); int downloadLimit = d[i].getDownloadRateLimitBytesPerSecond(); int uploadLimit = d[i].getUploadRateLimitBytesPerSecond(); setDownloadDetails(d[i],uploadLimit,downloadLimit); d[i].setUploadRateLimitBytesPerSecond(ZERO_DOWNLOAD_SETTING); d[i].setDownloadRateLimitBytesPerSecond( ZERO_DOWNLOAD_SETTING ); } } //preserve the global limits saveGlobalLimits(); COConfigurationManager.setParameter( "LAN Speed Enabled", false ); COConfigurationManager.setParameter( TransferSpeedValidator.AUTO_UPLOAD_ENABLED_CONFIGKEY,false); COConfigurationManager.setParameter( TransferSpeedValidator.AUTO_UPLOAD_SEEDING_ENABLED_CONFIGKEY,false); COConfigurationManager.setParameter( TransferSpeedValidator.UPLOAD_CONFIGKEY, max_speed); COConfigurationManager.setParameter( TransferSpeedValidator.UPLOAD_SEEDING_CONFIGKEY, max_speed); COConfigurationManager.setParameter( TransferSpeedValidator.DOWNLOAD_CONFIGKEY, max_speed); String[] params = TransferSpeedValidator.CONFIG_PARAMS; for (int i=0;i<params.length;i++){ COConfigurationManager.addParameterListener( params[i], this ); } } public void restoreLimits() { String[] params = TransferSpeedValidator.CONFIG_PARAMS; for (int i=0;i<params.length;i++){ COConfigurationManager.removeParameterListener( params[i], this ); } plugin.getDownloadManager().removeListener( this ); restoreGlobalLimits(); restoreIndividualLimits(); PluginConfigImpl.setEnablePluginCoreConfigChange( true ); } /** * Get the global limits from the TransferSpeedValidator class. Call before starting a speed test. */ private void saveGlobalLimits(){ //int settings. maxUploadKbs = COConfigurationManager.getIntParameter( TransferSpeedValidator.UPLOAD_CONFIGKEY ); maxUploadSeedingKbs = COConfigurationManager.getIntParameter( TransferSpeedValidator.UPLOAD_SEEDING_CONFIGKEY ); maxDownloadKbs = COConfigurationManager.getIntParameter( TransferSpeedValidator.DOWNLOAD_CONFIGKEY ); //boolean setting. autoSpeedEnabled = COConfigurationManager.getBooleanParameter( TransferSpeedValidator.AUTO_UPLOAD_ENABLED_CONFIGKEY ); autoSpeedSeedingEnabled = COConfigurationManager.getBooleanParameter( TransferSpeedValidator.AUTO_UPLOAD_SEEDING_ENABLED_CONFIGKEY ); LANSpeedEnabled = COConfigurationManager.getBooleanParameter( "LAN Speed Enabled" ); }//saveGlobalLimits /** * Call this method after a speed test completes to restore the global limits. */ private void restoreGlobalLimits(){ COConfigurationManager.setParameter( "LAN Speed Enabled", LANSpeedEnabled ); COConfigurationManager.setParameter(TransferSpeedValidator.AUTO_UPLOAD_ENABLED_CONFIGKEY,autoSpeedEnabled); COConfigurationManager.setParameter(TransferSpeedValidator.AUTO_UPLOAD_SEEDING_ENABLED_CONFIGKEY,autoSpeedSeedingEnabled); COConfigurationManager.setParameter(TransferSpeedValidator.UPLOAD_CONFIGKEY,maxUploadKbs); COConfigurationManager.setParameter(TransferSpeedValidator.UPLOAD_SEEDING_CONFIGKEY,maxUploadSeedingKbs); COConfigurationManager.setParameter(TransferSpeedValidator.DOWNLOAD_CONFIGKEY,maxDownloadKbs); }//restoreGlobalLimits /** * Call this method after the speed test is completed to restore the individual download limits * before the test started. */ private void restoreIndividualLimits(){ Download[] downloads = getAllDownloads(); if(downloads!=null){ int nDownloads = downloads.length; for(int i=0;i<nDownloads;i++){ int uploadLimit = getDownloadDetails(downloads[i], TORRENT_UPLOAD_LIMIT); int downLimit = getDownloadDetails(downloads[i], TORRENT_DOWNLOAD_LIMIT); downloads[i].setDownloadRateLimitBytesPerSecond(downLimit); downloads[i].setUploadRateLimitBytesPerSecond(uploadLimit); } } } /** * Save the upload/download limits of this Download object before the test started. * @param d - Download * @param uploadLimit - int * @param downloadLimit - int */ private void setDownloadDetails(Download d, int uploadLimit, int downloadLimit){ if(d==null) throw new IllegalArgumentException("Download should not be null."); Map props = new HashMap();//Map<String,Integer> props.put(TORRENT_UPLOAD_LIMIT, new Integer(uploadLimit) ); props.put(TORRENT_DOWNLOAD_LIMIT, new Integer(downloadLimit) ); torrentLimits.put(d,props); } /** * Get the upload or download limit for this Download object before the test started. * @param d - Download * @param param - String * @return - limit as int. */ private int getDownloadDetails(Download d, String param){ if(d==null || param==null ) throw new IllegalArgumentException("null inputs."); if(!param.equals(TORRENT_UPLOAD_LIMIT) && !param.equals(TORRENT_DOWNLOAD_LIMIT)) throw new IllegalArgumentException("invalid param. param="+param); Map out = (Map) torrentLimits.get(d); Integer limit = (Integer) out.get(param); return limit.intValue(); } /** * Get all the Download keys in this Map. * @return - Download[] */ private Download[] getAllDownloads(){ Download[] a = new Download[0]; return (Download[]) torrentLimits.keySet().toArray(a); } } protected void reportStage( String str ) { Iterator it = listeners.iterator(); while( it.hasNext()){ try{ ((NetworkAdminSpeedTestScheduledTestListener)it.next()).stage( this, str ); }catch( Throwable e ){ Debug.printStackTrace( e ); } } } protected void reportComplete() { resetSpeedLimits(); Iterator it = listeners.iterator(); while( it.hasNext()){ try{ ((NetworkAdminSpeedTestScheduledTestListener)it.next()).complete( this ); }catch( Throwable e ){ Debug.printStackTrace( e ); } } } public void addListener( NetworkAdminSpeedTestScheduledTestListener listener ) { listeners.add( listener ); } public void removeListener( NetworkAdminSpeedTestScheduledTestListener listener ) { listeners.remove( listener ); } }