/*******************************************************************************
 * Copyright (c) 2000, 2014 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.team.internal.ccvs.core;
 
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;

import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jsch.core.IJSchService;
import org.eclipse.osgi.service.debug.DebugOptions;
import org.eclipse.osgi.service.debug.DebugOptionsListener;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.Team;
import org.eclipse.team.core.TeamException;
import org.eclipse.team.internal.ccvs.core.client.*;
import org.eclipse.team.internal.ccvs.core.client.Command.KSubstOption;
import org.eclipse.team.internal.ccvs.core.client.Command.QuietOption;
import org.eclipse.team.internal.ccvs.core.client.listeners.IConsoleListener;
import org.eclipse.team.internal.ccvs.core.mapping.CVSActiveChangeSetCollector;
import org.eclipse.team.internal.ccvs.core.resources.FileModificationManager;
import org.eclipse.team.internal.ccvs.core.util.*;
import org.eclipse.team.internal.core.subscribers.ActiveChangeSetManager;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.util.tracker.ServiceTracker;

public class CVSProviderPlugin extends Plugin {
	
	// preference names
	public static final String READ_ONLY = "cvs.read.only"; //$NON-NLS-1$
	public static final String ENABLE_WATCH_ON_EDIT = "cvs.watch.on.edit"; //$NON-NLS-1$

	// external command to run for ext connection method
	public static final String DEFAULT_CVS_RSH = "ssh"; //$NON-NLS-1$
	// external command parameters
	public static final String DEFAULT_CVS_RSH_PARAMETERS = "-l {user} {host}"; //$NON-NLS-1$
	// remote command to run for ext connection method
	public static final String DEFAULT_CVS_SERVER = "cvs"; //$NON-NLS-1$
	// determines if empty directories received from the server should be pruned.
	public static final boolean DEFAULT_PRUNE = true;
	// determines if the user is prompted for confirmation before moving tags during a tag operation.
	public static final boolean DEFAULT_CONFIRM_MOVE_TAG = true;
	// determines if new directories should be discovered during update.
	public static final boolean DEFAULT_FETCH = true;
	// communication timeout with the server
	public static final int DEFAULT_TIMEOUT = 60;
	// file transfer compression level (0 - 9)
	public static final int DEFAULT_COMPRESSION_LEVEL = 0;
	// default text keyword substitution mode
	public static final KSubstOption DEFAULT_TEXT_KSUBST_OPTION = Command.KSUBST_TEXT_EXPAND;

	// cvs plugin extension points and ids
	public static final String ID = "org.eclipse.team.cvs.core"; //$NON-NLS-1$
	
	public static final QualifiedName CVS_WORKSPACE_SUBSCRIBER_ID = new QualifiedName("org.eclipse.team.cvs.ui.cvsworkspace-participant", "syncparticipant"); //$NON-NLS-1$ //$NON-NLS-2$
	public static final String PT_AUTHENTICATOR = "authenticator"; //$NON-NLS-1$
	public static final String PT_CONNECTIONMETHODS = "connectionmethods"; //$NON-NLS-1$
	public static final String PT_FILE_MODIFICATION_VALIDATOR = "filemodificationvalidator"; //$NON-NLS-1$
	
	private QuietOption quietness;
	private int compressionLevel = DEFAULT_COMPRESSION_LEVEL;
	private KSubstOption defaultTextKSubstOption = DEFAULT_TEXT_KSUBST_OPTION;
	private boolean usePlatformLineend = true;
	private int communicationsTimeout = DEFAULT_TIMEOUT;
	private boolean pruneEmptyDirectories = DEFAULT_PRUNE;
	private boolean fetchAbsentDirectories = DEFAULT_FETCH;
	private boolean replaceUnmanaged = true;
	private boolean repositoriesAreBinary = false;
	private String cvsRshCommand = DEFAULT_CVS_RSH;
	private String cvsRshParameters = DEFAULT_CVS_RSH_PARAMETERS;
	private String cvsServer = DEFAULT_CVS_SERVER;
	private boolean determineVersionEnabled = true;
	
	private static volatile CVSProviderPlugin instance;
	
	// CVS specific resource delta listeners
	private BuildCleanupListener addDeleteMoveListener;
	private FileModificationManager fileModificationManager;
	private SyncFileChangeListener metaFileSyncListener;

	private static final String REPOSITORIES_STATE_FILE = ".cvsProviderState"; //$NON-NLS-1$
	// version numbers for the state file (a positive number indicates version 1)
	private static final int REPOSITORIES_STATE_FILE_VERSION_2 = -1;
	private static List<ICVSDecoratorEnablementListener> decoratorEnablementListeners = new ArrayList<>();
	
	private CVSWorkspaceSubscriber cvsWorkspaceSubscriber;
	
	/**
	 * The identifier for the CVS nature
	 * (value <code>"org.eclipse.team.cvs.core.nature"</code>).
	 * The presence of this nature on a project indicates that it is 
	 * CVS-capable.
	 *
	 * @see org.eclipse.core.resources.IProject#hasNature
	 */
	private static final String NATURE_ID = ID + ".cvsnature"; //$NON-NLS-1$

	// File used to idicate if the workbench was shut down properly or not
	private static final String CRASH_INDICATION_FILE  = ".running"; //$NON-NLS-1$
	private boolean crash;

	private boolean autoShareOnImport;
	private boolean useProxy;

	public static final String PROXY_TYPE_HTTP = "HTTP"; //$NON-NLS-1$
	public static final String PROXY_TYPE_SOCKS5 = "SOCKS5"; //$NON-NLS-1$
	public static final String HTTP_DEFAULT_PORT = "80"; //$NON-NLS-1$
	public static final String SOCKS5_DEFAULT_PORT = "1080"; //$NON-NLS-1$
	
	private String proxyType;
	private String proxyHost;
	private String proxyPort;
	private boolean useProxyAuth;
	
	private CVSActiveChangeSetCollector changeSetManager;
	private ServiceRegistration debugRegistration;
	private ServiceTracker tracker;

	private static final String INFO_PROXY_USER = "org.eclipse.team.cvs.core.proxy.user"; //$NON-NLS-1$ 
	private static final String INFO_PROXY_PASS = "org.eclipse.team.cvs.core.proxy.pass"; //$NON-NLS-1$ 

	private static final URL FAKE_URL;
	static {
		URL temp = null;
		try {
			temp = new URL("http://org.eclipse.team.cvs.proxy.auth");//$NON-NLS-1$ 
		} catch (MalformedURLException e) {
			// Should never fail
		}
		FAKE_URL = temp;
	}
	
	
	public synchronized CVSWorkspaceSubscriber getCVSWorkspaceSubscriber() {
		if (cvsWorkspaceSubscriber == null) {
			cvsWorkspaceSubscriber = new CVSWorkspaceSubscriber(
					CVS_WORKSPACE_SUBSCRIBER_ID, 
					CVSMessages.CVSProviderPlugin_20); 
		}
		return cvsWorkspaceSubscriber;
	}

	/**
	 * Constructor for CVSProviderPlugin.
	 * @param descriptor
	 */
	public CVSProviderPlugin() {
		super();
		instance = this;
	}
	
	/**
	 * Convenience method for logging CoreExceptions to the plugin log
	 */
	public static void log(CoreException e) {
		log(e.getStatus().getSeverity(), e.getMessage(), e);
	}
	
	/**
	 * Log the given exception along with the provided message and severity indicator
	 */
	public static void log(int severity, String message, Throwable e) {
		log(new Status(severity, ID, 0, message, e));
	}
	
	/**
	 * Log the given status. Do not use this method for the IStatus from a CoreException.
	 * Use<code>log(CoreException)</code> instead so the stack trace is not lost.
	 */
	public static void log(IStatus status) {
		getPlugin().getLog().log(status);
	}

	/**
	 * Returns the singleton plug-in instance.
	 * 
	 * @return the plugin instance
	 */
	public static CVSProviderPlugin getPlugin() {
		return instance;
	}
	
	/**
	 * Answers the repository provider type id for the cvs plugin
	 */
	public static String getTypeId() {
		return NATURE_ID;
	}
	
	/**
	 * Sets the file transfer compression level. (if supported)
	 * Valid levels are: 0 (disabled), 1 (worst/fastest) - 9 (best/slowest)
	 */
	public void setCompressionLevel(int level) {
		compressionLevel = level;
	}

	/**
	 * Gets the file transfer compression level.
	 */
	public int getCompressionLevel() {
		return compressionLevel;
	}
	
	/**
	 * Sets the default keyword substitution mode for text files.
	 */
	public void setDefaultTextKSubstOption(KSubstOption ksubst) {
		defaultTextKSubstOption = ksubst;
	}


	/**
	 * Gets the default keyword substitution mode for text files.
	 */
	public KSubstOption getDefaultTextKSubstOption() {
		return defaultTextKSubstOption;
	}

	/**
	 * Should the CVS adapter prune empty directories
	 */
	public boolean getPruneEmptyDirectories() {
		return pruneEmptyDirectories;
	}

	/**
	 * Set whether the CVS adapter should prune empty directories
	 */
	public void setPruneEmptyDirectories(boolean prune) {
		pruneEmptyDirectories = prune;
	}

	/**
	 * Get the communications timeout value in seconds
	 */
	public int getTimeout() {
		return communicationsTimeout;
	}
	
	/**
	 * Set the timeout value for communications to a value in seconds.
	 * The value must be greater than or equal 0. If is it 0, there is no timeout.
	 */
	public void setTimeout(int timeout) {
		this.communicationsTimeout = Math.max(0, timeout);
	}
	
	/**
	 * Set the quietness option to use with cvs commands.
	 * Can be "", "-q" or "-Q"
	 */
	public void setQuietness(QuietOption option) {
			this.quietness = option;
	}

	/**
	 * Get the quietness option for commands
	 */
	public QuietOption getQuietness() {
		return quietness;
	}
	
	/**
	 * Set the console listener for commands.
	 * @param consoleListener the listener
	 */
	public void setConsoleListener(IConsoleListener consoleListener) {
		ConsoleListeners.getInstance().addListener(consoleListener);
	}
	
	@Override
	public void start(BundleContext context) throws Exception {
		super.start(context);

		// register debug options listener
		Hashtable<String,String> properties = new Hashtable<>(2);
		properties.put(DebugOptions.LISTENER_SYMBOLICNAME, ID);
		debugRegistration = context.registerService(DebugOptionsListener.class, Policy.DEBUG_OPTIONS_LISTENER, properties);

		// load the state which includes the known repositories
		loadOldState();
		crash = createCrashFile();
		
		// Initialize CVS change listeners. Note that the report type is important.
		IWorkspace workspace = ResourcesPlugin.getWorkspace();
		addDeleteMoveListener = new BuildCleanupListener();
		fileModificationManager = new FileModificationManager();
		metaFileSyncListener = new SyncFileChangeListener();
		workspace.addResourceChangeListener(addDeleteMoveListener, IResourceChangeEvent.POST_BUILD);
		workspace.addResourceChangeListener(metaFileSyncListener, IResourceChangeEvent.POST_CHANGE);
		workspace.addResourceChangeListener(fileModificationManager, IResourceChangeEvent.POST_CHANGE);
		
		getCVSWorkspaceSubscriber();
		
		// Must load the change set manager on startup since it listens to deltas
		getChangeSetManager();
		
		tracker = new ServiceTracker<>(getBundle().getBundleContext(), IJSchService.class.getName(), null);
		tracker.open();
	}
	
	@Override
	public void stop(BundleContext context) throws Exception {
		try {
			// unregister debug options listener
			debugRegistration.unregister();
			debugRegistration = null;

			savePluginPreferences();
			
			// remove listeners
			IWorkspace workspace = ResourcesPlugin.getWorkspace();
			workspace.removeResourceChangeListener(metaFileSyncListener);
			workspace.removeResourceChangeListener(fileModificationManager);
			workspace.removeResourceChangeListener(addDeleteMoveListener);
			
			// remove all of this plugin's save participants. This is easier than having
			// each class that added itself as a participant to have to listen to shutdown.
			workspace.removeSaveParticipant(this);
			
			getChangeSetManager().dispose();
			
			tracker.close();
			
			deleteCrashFile();
		} finally {
			super.stop(context);
		}
	}
		
	@Override
	protected void initializeDefaultPluginPreferences(){
		Preferences store = getPluginPreferences();
		store.setDefault(READ_ONLY, false);
		store.setDefault(ENABLE_WATCH_ON_EDIT, false);
	}
	
	/**
	 * Gets the cvsRshCommand.
	 * @return Returns a String
	 */
	public String getCvsRshCommand() {
		return cvsRshCommand;
	}

	/**
	 * Sets the cvsRshCommand.
	 * @param cvsRshCommand The cvsRshCommand to set
	 */
	public void setCvsRshCommand(String cvsRshCommand) {
		this.cvsRshCommand = cvsRshCommand;
	}

	/**
	 * Returns the cvsRshParameters.
	 * @return String
	 */
	public String getCvsRshParameters() {
		return cvsRshParameters;
	}

	/**
	 * Sets the cvsRshParameters.
	 * @param cvsRshParameters The cvsRshParameters to set
	 */
	public void setCvsRshParameters(String cvsRshParameters) {
		this.cvsRshParameters = cvsRshParameters;
	}
	
	/**
	 * Gets the cvsServer.
	 * @return Returns a String
	 */
	public String getCvsServer() {
		return cvsServer;
	}

	/**
	 * Sets the cvsServer.
	 * @param cvsServer The cvsServer to set
	 */
	public void setCvsServer(String cvsServer) {
		this.cvsServer = cvsServer;
	}

	/**
	 * Gets the etchAbsentDirectories.
	 * @return Returns a boolean
	 */
	public boolean getFetchAbsentDirectories() {
		return fetchAbsentDirectories;
	}

	public boolean getRepositoriesAreBinary() {
		return repositoriesAreBinary;
	}
	
	/**
	 * Sets the fetchAbsentDirectories.
	 * @param etchAbsentDirectories The etchAbsentDirectories to set
	 */
	public void setFetchAbsentDirectories(boolean fetchAbsentDirectories) {
		this.fetchAbsentDirectories = fetchAbsentDirectories;
	}
	
	public void setRepositoriesAreBinary(boolean binary) {
		repositoriesAreBinary = binary;
	}
	
	public static void broadcastDecoratorEnablementChanged(final boolean enabled) {
		ICVSDecoratorEnablementListener[] listeners;
		synchronized(decoratorEnablementListeners) {
			listeners = decoratorEnablementListeners.toArray(new ICVSDecoratorEnablementListener[decoratorEnablementListeners.size()]);
		}
		for (ICVSDecoratorEnablementListener listener : listeners) {
			ISafeRunnable code = new ISafeRunnable() {
				public void run() throws Exception {
					listener.decoratorEnablementChanged(enabled);
				}
				public void handleException(Throwable e) {
					// don't log the exception....it is already being logged in Platform#run
				}
			};
			Platform.run(code);
		}
	}
	
	/**
	 * Gets the replaceUnmanaged.
	 * @return Returns a boolean
	 */
	public boolean isReplaceUnmanaged() {
		return replaceUnmanaged;
	}

	/**
	 * Sets the replaceUnmanaged.
	 * @param replaceUnmanaged The replaceUnmanaged to set
	 */
	public void setReplaceUnmanaged(boolean replaceUnmanaged) {
		this.replaceUnmanaged = replaceUnmanaged;
	}

		
	/**
	 * Register to receive notification of repository creation and disposal
	 */
	public void addRepositoryListener(ICVSListener listener) {
		KnownRepositories.getInstance().addRepositoryListener(listener);
	}
	
	/**
	 * Register to receive notification of enablement of sync info decoration requirements. This
	 * can be useful for providing lazy initialization of caches that are only required for decorating
	 * resource with CVS information.
	 */
	public void addDecoratorEnablementListener(ICVSDecoratorEnablementListener listener) {
		synchronized(decoratorEnablementListeners) {
			decoratorEnablementListeners.add(listener);
		}
	}
	
	/**
	 * De-register a listener
	 */
	public void removeRepositoryListener(ICVSListener listener) {
		KnownRepositories.getInstance().removeRepositoryListener(listener);
	}
	
	/**
	 * De-register the decorator enablement listener. 
	 */
	public void removeDecoratorEnablementListener(ICVSDecoratorEnablementListener listener) {
		synchronized(decoratorEnablementListeners) {
			decoratorEnablementListeners.remove(listener);
		}
	}
	
	/** 
	 * Return a list of the know repository locations. This is left
	 * here to isolate the RelEng tools plugin from changes in CVS core.
	 */
	public ICVSRepositoryLocation[] getKnownRepositories() {
		return KnownRepositories.getInstance().getRepositories();
	}

	private void loadOldState() {
		try {
			IPath pluginStateLocation = CVSProviderPlugin.getPlugin().getStateLocation().append(REPOSITORIES_STATE_FILE);
			File file = pluginStateLocation.toFile();
			if (file.exists()) {
				try {
					try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) {
						readOldState(dis);
					}
					// The file is no longer needed as the state is
					// persisted in the user settings
					file.delete();
				} catch (IOException e) {
					throw new TeamException(new Status(IStatus.ERROR, CVSProviderPlugin.ID, TeamException.UNABLE, CVSMessages.CVSProvider_ioException, e));  
				}
			}
		} catch (TeamException e) {
			Util.logError(CVSMessages.CVSProvider_errorLoading, e);
		}
	}
	
	private void readOldState(DataInputStream dis) throws IOException, CVSException {
		KnownRepositories instance = KnownRepositories.getInstance();
		int count = dis.readInt();
		if (count >= 0) {
			// this is the version 1 format of the state file
			for (int i = 0; i < count; i++) {
				ICVSRepositoryLocation location = instance.getRepository(dis.readUTF());
				instance.addRepository(location, false /* no need to broadcast on startup */);
			}
		} else if (count == REPOSITORIES_STATE_FILE_VERSION_2) {
			count = dis.readInt();
			for (int i = 0; i < count; i++) {
				ICVSRepositoryLocation location = instance.getRepository(dis.readUTF());
				instance.addRepository(location, false /* no need to broadcast on startup */);
				// Read the next field which is no longer used
				dis.readUTF();
			}
		} else {
			Util.logError(NLS.bind(CVSMessages.CVSProviderPlugin_unknownStateFileVersion, new String[] { Integer.valueOf(count).toString() }), null); 
		}
	}
		
	public static boolean isText(IFile file) {
		if (CVSProviderPlugin.getPlugin().getRepositoriesAreBinary()) return false;
		return Team.getFileContentManager().getType(file) == Team.TEXT;
	}
	
	/**
	 * Gets the determineVersionEnabled.
	 * @return boolean
	 */
	public boolean isDetermineVersionEnabled() {
		return determineVersionEnabled;
	}

	/**
	 * Sets the determineVersionEnabled.
	 * @param determineVersionEnabled The determineVersionEnabled to set
	 */
	public void setDetermineVersionEnabled(boolean determineVersionEnabled) {
		this.determineVersionEnabled = determineVersionEnabled;
	}
	
	/**
	 * Returns the fileModificationManager.
	 * @return FileModificationManager
	 */
	public FileModificationManager getFileModificationManager() {
		return fileModificationManager;
	}

	/**
	 * @return boolean
	 */
	public boolean isWatchEditEnabled() {
		return getPluginPreferences().getBoolean(CVSProviderPlugin.READ_ONLY);
	}

	public void setDebugProtocol(boolean value) {
		Policy.DEBUG_CVS_PROTOCOL = value;		
	}
	
	public boolean isDebugProtocol() {
		return Policy.DEBUG_CVS_PROTOCOL;
	}

	/*
	 * Create the crash indicator file. This method returns true if the file
	 * already existed, indicating that a crash occurred on the last invokation of
	 * the platform.
	 */
	private boolean createCrashFile() {
		IPath pluginStateLocation = CVSProviderPlugin.getPlugin().getStateLocation();
		File crashFile = pluginStateLocation.append(CRASH_INDICATION_FILE).toFile();
		if (crashFile.exists()) {
			return true;
		}
		try {
			crashFile.createNewFile();
		} catch (IOException e) {
			CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
		}
		return false;
	}
	
	private void deleteCrashFile() {
		IPath pluginStateLocation = CVSProviderPlugin.getPlugin().getStateLocation();
		File crashFile = pluginStateLocation.append(CRASH_INDICATION_FILE).toFile();
		crashFile.delete();
	}
	
	public boolean crashOnLastRun() {
		return crash;
	}
	
	/**
	 * Return the CVS preferences node in the instance scope
	 */
	public org.osgi.service.prefs.Preferences getInstancePreferences() {
		return InstanceScope.INSTANCE.getNode(getBundle().getSymbolicName());
	}
	
	/**
	 * @return Returns the usePlatformLineend.
	 */
	public boolean isUsePlatformLineend() {
		return usePlatformLineend;
	}
	/**
	 * @param usePlatformLineend The usePlatformLineend to set.
	 */
	public void setUsePlatformLineend(boolean usePlatformLineend) {
		this.usePlatformLineend = usePlatformLineend;
	}

	public void setAutoshareOnImport(boolean autoShareOnImport) {
		this.autoShareOnImport = autoShareOnImport;
	}

	public boolean isAutoshareOnImport() {
		return autoShareOnImport;
	}

	/**
	 * @return Returns the watchOnEdit.
	 */
	public boolean isWatchOnEdit() {
		return getPluginPreferences().getBoolean(CVSProviderPlugin.ENABLE_WATCH_ON_EDIT);
	}
	
	// proxy configuration
	
	public void setUseProxy(boolean useProxy) {
		this.useProxy = useProxy;
	}

	public boolean isUseProxy() {
		return this.useProxy;
	}

	public void setProxyType(String proxyType) {
		this.proxyType = proxyType;
	}
	
	public String getProxyType() {
		return this.proxyType;
	}

	public void setProxyHost(String proxyHost) {
		this.proxyHost = proxyHost;
	}
	
	public String getProxyHost() {
		return this.proxyHost;
	}

	public void setProxyPort(String proxyPort) {
		this.proxyPort = proxyPort;
	}
	
	public String getProxyPort() {
		return this.proxyPort;
	}

	public void setUseProxyAuth(boolean useProxyAuth) {
		this.useProxyAuth = useProxyAuth;
	}

	public boolean isUseProxyAuth() {
		return this.useProxyAuth;
	}
	
	public String getProxyUser() {
		Object user = getAuthInfo().get(INFO_PROXY_USER);
		return user==null ? "" : (String) user; //$NON-NLS-1$
	}
	
	public String getProxyPassword() {
		Object pass = getAuthInfo().get(INFO_PROXY_PASS);
		return pass==null ? "" : (String) pass; //$NON-NLS-1$
	}
	
	private Map<String,String> getAuthInfo() {
		// Retrieve username and password from keyring.
		Map<String,String> authInfo = Platform.getAuthorizationInfo(FAKE_URL, "proxy", ""); //$NON-NLS-1$ //$NON-NLS-2$
		return authInfo!=null ? authInfo : Collections.emptyMap();
	}

	public void setProxyAuth(String proxyUser, String proxyPass) {
		Map<String,String> authInfo = getAuthInfo();
		if (authInfo.isEmpty()) {
			authInfo = new java.util.HashMap<>(4);
		}
		if (proxyUser != null) {
			authInfo.put(INFO_PROXY_USER, proxyUser);
		}
		if (proxyPass != null) {
			authInfo.put(INFO_PROXY_PASS, proxyPass);
		}
		try {
			Platform.addAuthorizationInfo(FAKE_URL, "proxy", "", authInfo);  //$NON-NLS-1$ //$NON-NLS-2$
		} catch (CoreException e) {
			// We should probably wrap the CoreException here!
			CVSProviderPlugin.log(e);
		}
	}
	
	public synchronized ActiveChangeSetManager getChangeSetManager() {
		if (changeSetManager == null) {
			changeSetManager = new CVSActiveChangeSetCollector(CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber());
		}
		return changeSetManager;
	}
	
	public IJSchService getJSchService() {
		return (IJSchService)tracker.getService();
	}
	
}
