Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 317ab66a31a2299587efad4b8fe84cd182b2dd33 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
/*******************************************************************************
 * Copyright (c) 2011 Wind River Systems, Inc. and others. All rights reserved.
 * This program and the accompanying materials are made available under the terms
 * of the Eclipse Public License v1.0 which accompanies this distribution, and is
 * available at http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.tcf.te.tcf.terminals.core.launcher;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.PlatformObject;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.util.NLS;
import org.eclipse.tcf.protocol.IChannel;
import org.eclipse.tcf.protocol.IChannel.IChannelListener;
import org.eclipse.tcf.protocol.IPeer;
import org.eclipse.tcf.protocol.IToken;
import org.eclipse.tcf.protocol.Protocol;
import org.eclipse.tcf.services.IStreams;
import org.eclipse.tcf.services.ITerminals;
import org.eclipse.tcf.services.ITerminals.TerminalContext;
import org.eclipse.tcf.te.core.async.AsyncCallbackCollector;
import org.eclipse.tcf.te.runtime.callback.Callback;
import org.eclipse.tcf.te.runtime.events.DisposedEvent;
import org.eclipse.tcf.te.runtime.events.EventManager;
import org.eclipse.tcf.te.runtime.interfaces.callback.ICallback;
import org.eclipse.tcf.te.runtime.interfaces.events.IEventListener;
import org.eclipse.tcf.te.runtime.interfaces.properties.IPropertiesContainer;
import org.eclipse.tcf.te.runtime.properties.PropertiesContainer;
import org.eclipse.tcf.te.runtime.services.ServiceManager;
import org.eclipse.tcf.te.runtime.services.interfaces.ITerminalService;
import org.eclipse.tcf.te.runtime.services.interfaces.constants.ITerminalsConnectorConstants;
import org.eclipse.tcf.te.tcf.core.Tcf;
import org.eclipse.tcf.te.tcf.core.async.CallbackInvocationDelegate;
import org.eclipse.tcf.te.tcf.core.interfaces.IChannelManager;
import org.eclipse.tcf.te.tcf.core.streams.StreamsDataProvider;
import org.eclipse.tcf.te.tcf.core.streams.StreamsDataReceiver;
import org.eclipse.tcf.te.tcf.terminals.core.activator.CoreBundleActivator;
import org.eclipse.tcf.te.tcf.terminals.core.interfaces.launcher.ITerminalsContextAwareListener;
import org.eclipse.tcf.te.tcf.terminals.core.interfaces.launcher.ITerminalsLauncher;
import org.eclipse.tcf.te.tcf.terminals.core.interfaces.tracing.ITraceIds;
import org.eclipse.tcf.te.tcf.terminals.core.nls.Messages;

/**
 * Remote terminals launcher.
 * <p>
 * The terminals launcher is implemented fully asynchronous.
 */
public class TerminalsLauncher extends PlatformObject implements ITerminalsLauncher {
	// The channel instance
	/* default */ IChannel channel;
	// The terminals properties instance
	private IPropertiesContainer properties;

	// The terminals service instance
	/* default */ ITerminals svcTerminals;
	// The streams service instance
	/* default */ IStreams svcStreams;
	// The remote terminals context
	/* default */ ITerminals.TerminalContext terminalContext;

	// The callback instance
	private ICallback callback;

	// The streams listener instance
	private IStreams.StreamsListener streamsListener = null;
	// The terminals listener instance
	private ITerminals.TerminalsListener terminalsListener = null;
	// The event listener instance
	private IEventListener eventListener = null;

	/**
	 * Constructor.
	 */
	public TerminalsLauncher() {
		super();
	}

	/* (non-Javadoc)
	 * @see org.eclipse.tcf.te.tcf.terminals.core.interfaces.launcher.ITerminalsLauncher#dispose()
	 */
	@Override
	public void dispose() {
		// Unlink the terminal context
		terminalContext = null;

		// Store a final reference to the channel instance
		final IChannel finChannel = channel;

		// Remove the notification listener
		if (eventListener != null) {
			EventManager.getInstance().removeEventListener(eventListener);
			eventListener = null;
		}

		// Create the callback collector
		final AsyncCallbackCollector collector = new AsyncCallbackCollector(new Callback() {
			@Override
			protected void internalDone(Object caller, IStatus status) {
				Assert.isTrue(Protocol.isDispatchThread(), "Illegal Thread Access"); //$NON-NLS-1$
				// Close the channel as all disposal is done
				if (finChannel != null) Tcf.getChannelManager().closeChannel(finChannel);
			}
		}, new CallbackInvocationDelegate());

		if (streamsListener != null) {
			// Dispose the streams listener
			if (streamsListener instanceof TerminalsStreamsListener) {
				((TerminalsStreamsListener)streamsListener).dispose(new AsyncCallbackCollector.SimpleCollectorCallback(collector));
			}
			streamsListener = null;
		}

		// Dispose the terminals listener if created
		if (terminalsListener != null) {
			// Dispose the terminals listener
			if (terminalsListener instanceof TerminalsListener) {
				((TerminalsListener)terminalsListener).dispose(new AsyncCallbackCollector.SimpleCollectorCallback(collector));
			}
			terminalsListener = null;
			// Remove the terminals listener from the terminals service
			getSvcTerminals().removeListener(terminalsListener);
		}

		// Mark the collector initialization as done
		collector.initDone();

		// Dissociate the channel
		channel = null;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.tcf.te.tcf.terminals.core.interfaces.launcher.ITerminalsLauncher#exit()
	 */
	@Override
	public void exit() {
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				if (terminalContext != null) {
					// Exit the terminal
					terminalContext.exit(new ITerminals.DoneCommand() {
						@Override
						public void doneCommand(IToken token, Exception error) {
							onExitDone(terminalContext, error);
						}
					});
				}

			}
		};

		if (Protocol.isDispatchThread()) runnable.run();
		else Protocol.invokeAndWait(runnable);
	}

	/**
	 * Check if the terminal context really exited.
	 * <p>
	 * Called from {@link #exit()}.
	 *
	 * @param context The terminal context. Must not be <code>null</code>.
	 * @param error The exception in case {@link #exit()} returned with an error or <code>null</code>.
	 */
	protected void onExitDone(ITerminals.TerminalContext context, Exception error) {
		Assert.isNotNull(context);

		// If the exit of the remote terminal context failed, give a warning to the user
		if (error != null) {
			String message = NLS.bind(Messages.TerminalsLauncher_error_terminalExitFailed, context.getProcessID());
			message += NLS.bind(Messages.TerminalsLauncher_error_possibleCause, error.getLocalizedMessage());

			IStatus status = new Status(IStatus.WARNING, CoreBundleActivator.getUniqueIdentifier(), message, error);
			Platform.getLog(CoreBundleActivator.getContext().getBundle()).log(status);

			// Dispose the launcher directly
			dispose();
		}
		// No error from exit -> double-check.
		else {
			final ITerminals.TerminalContext finContext = context;
			// Let's see if we can still get information about the context
			getSvcTerminals().getContext(context.getID(), new ITerminals.DoneGetContext() {
				@Override
				public void doneGetContext(IToken token, Exception error, TerminalContext context) {
					// In case there is no error and we do get back an terminal context,
					// the terminal must be still running, having ignored the exit.
					if (error == null && context != null && context.getID().equals(finContext.getID())) {
						String message = NLS.bind(Messages.TerminalsLauncher_error_terminalExitFailed, context.getProcessID());
						message += Messages.TerminalsLauncher_error_possibleCauseUnknown;

						IStatus status = new Status(IStatus.WARNING, CoreBundleActivator.getUniqueIdentifier(), message, error);
						Platform.getLog(CoreBundleActivator.getContext().getBundle()).log(status);

						// Dispose the launcher directly
						dispose();
					}
				}
			});
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.tcf.te.tcf.terminals.core.interfaces.launcher.ITerminalsLauncher#launch(org.eclipse.tcf.protocol.IPeer, org.eclipse.tcf.te.runtime.interfaces.properties.IPropertiesContainer, org.eclipse.tcf.te.runtime.interfaces.callback.ICallback)
	 */
	@Override
	public void launch(final IPeer peer, final IPropertiesContainer properties, final ICallback callback) {
		Assert.isNotNull(peer);
		Assert.isNotNull(properties);

		// Normalize the callback
		if (callback == null) {
			this.callback = new Callback() {
				/* (non-Javadoc)
				 * @see org.eclipse.tcf.te.runtime.callback.Callback#internalDone(java.lang.Object, org.eclipse.core.runtime.IStatus)
				 */
				@Override
				public void internalDone(Object caller, IStatus status) {
				}
			};
		}
		else {
			this.callback = callback;
		}

		// Remember the terminal properties
		this.properties = properties;

		// Open a dedicated channel to the given peer
		Tcf.getChannelManager().openChannel(peer, true, new IChannelManager.DoneOpenChannel() {
			/* (non-Javadoc)
			 * @see org.eclipse.tcf.te.tcf.core.interfaces.IChannelManager.DoneOpenChannel#doneOpenChannel(java.lang.Throwable, org.eclipse.tcf.protocol.IChannel)
			 */
			@Override
			public void doneOpenChannel(Throwable error, IChannel channel) {
				if (error == null) {
					TerminalsLauncher.this.channel = channel;

					// Attach a channel listener so we can dispose ourself if the channel
					// is closed from the remote side.
					channel.addChannelListener(new IChannelListener() {
						/* (non-Javadoc)
						 * @see org.eclipse.tcf.protocol.IChannel.IChannelListener#onChannelOpened()
						 */
						@Override
						public void onChannelOpened() {
						}
						/* (non-Javadoc)
						 * @see org.eclipse.tcf.protocol.IChannel.IChannelListener#onChannelClosed(java.lang.Throwable)
						 */
						@Override
						public void onChannelClosed(Throwable error) {
							if (error != null) {
								IStatus status = new Status(IStatus.ERROR, CoreBundleActivator.getUniqueIdentifier(),
												NLS.bind(Messages.TerminalsLauncher_error_channelConnectFailed, peer.getID(), error.getLocalizedMessage()),
												error);
								invokeCallback(status, null);
							}
						}
						/* (non-Javadoc)
						 * @see org.eclipse.tcf.protocol.IChannel.IChannelListener#congestionLevel(int)
						 */
						@Override
						public void congestionLevel(int level) {
						}
					});


					// Check if the channel is in connected state
					if (channel.getState() != IChannel.STATE_OPEN) {
						IStatus status = new Status(IStatus.ERROR, CoreBundleActivator.getUniqueIdentifier(),
										Messages.TerminalsLauncher_error_channelNotConnected,
										new IllegalStateException());
						invokeCallback(status, null);
						return;
					}

					// Get the terminals and streams services
					svcTerminals = channel.getRemoteService(ITerminals.class);
					if (svcTerminals == null) {
						IStatus status = new Status(IStatus.ERROR, CoreBundleActivator.getUniqueIdentifier(),
										NLS.bind(Messages.TerminalsLauncher_error_missingRequiredService, ITerminals.class.getName()),
										null);

						invokeCallback(status, null);
						return;
					}

					svcStreams = channel.getRemoteService(IStreams.class);
					if (svcStreams == null) {
						IStatus status = new Status(IStatus.ERROR, CoreBundleActivator.getUniqueIdentifier(),
										NLS.bind(Messages.TerminalsLauncher_error_missingRequiredService, IStreams.class.getName()),
										null);
						invokeCallback(status, null);
						return;
					}

					// Execute the launch now
					executeLaunch();
				} else {
					IStatus status = new Status(IStatus.ERROR, CoreBundleActivator.getUniqueIdentifier(),
									NLS.bind(Messages.TerminalsLauncher_error_channelConnectFailed, peer.getID(), error.getLocalizedMessage()),
									error);
					invokeCallback(status, null);
				}
			}
		});
	}

	/**
	 * Executes the launch of the remote terminal.
	 */
	protected void executeLaunch() {
		// Get the properties container
		final IPropertiesContainer properties = getProperties();
		if (properties == null) {
			// This is an illegal argument. Properties must be set
			IStatus status = new Status(IStatus.ERROR, CoreBundleActivator.getUniqueIdentifier(),
							NLS.bind(Messages.TerminalsLauncher_error_illegalNullArgument, "properties"), //$NON-NLS-1$
							new IllegalArgumentException());
			invokeCallback(status, null);
			return;
		}

		// Create the streams listener
		streamsListener = createStreamsListener();
		// If available, we need to subscribe to the streams.
		if (streamsListener != null) {
			getSvcStreams().subscribe(ITerminals.NAME, streamsListener, new IStreams.DoneSubscribe() {
				@Override
				public void doneSubscribe(IToken token, Exception error) {
					// In case the subscribe to the stream fails, we pass on
					// the error to the user and stop the launch
					if (error != null) {
						// Construct the error message to show to the user
						String message = NLS.bind(Messages.TerminalsLauncher_error_terminalLaunchFailed,
												  properties.getStringProperty(ITerminalsLauncher.PROP_CONNECTION_NAME));
						message += NLS.bind(Messages.TerminalsLauncher_error_possibleCause, Messages.TerminalsLauncher_cause_subscribeFailed);

						// Construct the status object
						IStatus status = new Status(IStatus.ERROR, CoreBundleActivator.getUniqueIdentifier(), message, error);
						invokeCallback(status, null);
					} else {
						// Initialize the console or output file
						onSubscribeStreamsDone();
					}
				}
			});
		} else {
			// No streams to attach to -> go directly to the terminals launch
			onAttachStreamsDone();
		}
	}

	/**
	 * Initialize and attach the output console and/or the output file.
	 * <p>
	 * Called from {@link IStreams#subscribe(String, org.eclipse.tcf.services.IStreams.StreamsListener, org.eclipse.tcf.services.IStreams.DoneSubscribe)}.
	 */
	protected void onSubscribeStreamsDone() {
		// Get the properties container
		IPropertiesContainer properties = getProperties();
		if (properties == null) {
			// This is an illegal argument. Properties must be set
			IStatus status = new Status(IStatus.ERROR, CoreBundleActivator.getUniqueIdentifier(),
							NLS.bind(Messages.TerminalsLauncher_error_illegalNullArgument, "properties"), //$NON-NLS-1$
							new IllegalArgumentException());
			invokeCallback(status, null);
			return;
		}

		// Register the notification listener to listen to the console disposal
		eventListener = new TerminalsLauncherEventListener(this);
		EventManager.getInstance().addEventListener(eventListener, DisposedEvent.class);

		// Get the terminal service
		ITerminalService terminal = ServiceManager.getInstance().getService(ITerminalService.class);
		// If not available, we cannot fulfill this request
		if (terminal != null) {
			// Create the terminal streams settings
			PropertiesContainer props = new PropertiesContainer();
			props.setProperty(ITerminalsConnectorConstants.PROP_CONNECTOR_TYPE_ID, "org.eclipse.tcf.te.ui.terminals.type.terminals"); //$NON-NLS-1$
			props.setProperty(ITerminalsConnectorConstants.PROP_ID, "org.eclipse.tcf.te.ui.terminals.TerminalsView"); //$NON-NLS-1$
			// Set the terminal tab title
			String terminalTitle = getTerminalTitle();
			if (terminalTitle != null) {
				props.setProperty(ITerminalsConnectorConstants.PROP_TITLE, terminalTitle);
			}

			// Create and store the streams which will be connected to the terminals stdin
			props.setProperty(ITerminalsConnectorConstants.PROP_STREAMS_STDIN, connectRemoteOutputStream(getStreamsListener(), new String[] { ITerminals.PROP_STDIN_ID }));
			// Create and store the streams the terminal will see as stdout
			props.setProperty(ITerminalsConnectorConstants.PROP_STREAMS_STDOUT, connectRemoteInputStream(getStreamsListener(), new String[] { ITerminals.PROP_STDOUT_ID }));
			// Create and store the streams the terminal will see as stderr
			props.setProperty(ITerminalsConnectorConstants.PROP_STREAMS_STDERR, connectRemoteInputStream(getStreamsListener(), new String[] { ITerminals.PROP_STDERR_ID }));

			// Copy the terminal properties
			props.setProperty(ITerminalsConnectorConstants.PROP_LOCAL_ECHO, properties.getBooleanProperty(ITerminalsConnectorConstants.PROP_LOCAL_ECHO));
			props.setProperty(ITerminalsConnectorConstants.PROP_LINE_SEPARATOR, properties.getStringProperty(ITerminalsConnectorConstants.PROP_LINE_SEPARATOR));

			// The custom data object is the terminal launcher itself
			props.setProperty(ITerminalsConnectorConstants.PROP_DATA, this);

			// Open the console
			terminal.openConsole(props, null);
		}

		// The streams got subscribed, check if we shall configure the output redirection to a file
		if (properties.getStringProperty(ITerminalsLauncher.PROP_TERMINAL_OUTPUT_REDIRECT_TO_FILE) != null) {
			// Get the file name where to redirect the terminal output to
			String filename = properties.getStringProperty(ITerminalsLauncher.PROP_TERMINAL_OUTPUT_REDIRECT_TO_FILE);
			try {
				// Create the receiver instance. If the file already exist, we
				// overwrite the file content.
				StreamsDataReceiver receiver = new StreamsDataReceiver(new BufferedWriter(new FileWriter(filename)),
																 	   new String[] { ITerminals.PROP_STDOUT_ID, ITerminals.PROP_STDERR_ID });
				// Register the receiver to the streams listener
				if (getStreamsListener() instanceof TerminalsStreamsListener) {
					((TerminalsStreamsListener)getStreamsListener()).registerDataReceiver(receiver);
				}
			} catch (IOException e) {
				// Construct the error message to show to the user
				String message = NLS.bind(Messages.TerminalsLauncher_error_terminalLaunchFailed,
								  		  properties.getStringProperty(ITerminalsLauncher.PROP_CONNECTION_NAME));
				message += NLS.bind(Messages.TerminalsLauncher_error_possibleCause,
								e.getLocalizedMessage() != null ? e.getLocalizedMessage() : Messages.TerminalsLauncher_cause_ioexception);

				// Construct the status object
				IStatus status = new Status(IStatus.ERROR, CoreBundleActivator.getUniqueIdentifier(), message, e);
				invokeCallback(status, null);
			}
		}

		// Launch the terminal
		onAttachStreamsDone();
	}

	/**
	 * Returns the terminal title string.
	 * <p>
	 * The default implementation constructs a title like &quot;[peer name] (Start time) &quot;.
	 *
	 * @return The terminal title string or <code>null</code>.
	 */
	protected String getTerminalTitle() {
		if (properties == null) {
			return null;
		}

		StringBuilder title = new StringBuilder();

		// Get the peer name
		final AtomicReference<String> peerName = new AtomicReference<String>(getProperties().getStringProperty(ITerminalsLauncher.PROP_CONNECTION_NAME));
		if (peerName.get() == null) {
			// Query the peer from the open channel
			Runnable runnable = new Runnable() {
				@Override
				public void run() {
					if (channel != null) {
						peerName.set(channel.getRemotePeer().getName());
					}
				}
			};

			if (Protocol.isDispatchThread()) runnable.run();
			else Protocol.invokeAndWait(runnable);
		}

		if (peerName.get() != null) {
			title.append("[").append(peerName.get()).append("]"); //$NON-NLS-1$ //$NON-NLS-2$
		}

		DateFormat format = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
		String date = format.format(new Date(System.currentTimeMillis()));
		title.append(" (").append(date).append(")"); //$NON-NLS-1$ //$NON-NLS-2$

		return title.toString();
	}

	/**
	 * Connects the given stream id's to a local {@link InputStream} instance.
	 *
	 * @param streamsListener The streams listener. Must not be <code>null</code>.
	 * @param streamIds The stream id's. Must not be <code>null</code>.
	 *
	 * @return The local input stream instance or <code>null</code>.
	 */
	protected InputStream connectRemoteInputStream(IStreams.StreamsListener streamsListener, String[] streamIds) {
		Assert.isNotNull(streamsListener);
		Assert.isNotNull(streamIds);

		InputStream stream = null;

		// Create the output stream receiving the data from remote
		PipedOutputStream remoteStreamDataReceiverStream = new PipedOutputStream();
		// Create the piped input stream instance
		try { stream = new PipedInputStream(remoteStreamDataReceiverStream); } catch (IOException e) { /* ignored on purpose */ }

		// If the input stream creation succeeded, connect the data receiver
		if (stream != null) {
			StreamsDataReceiver receiver = new StreamsDataReceiver(new OutputStreamWriter(remoteStreamDataReceiverStream), streamIds);
			// Register the data receiver to the streams listener
			if (getStreamsListener() instanceof TerminalsStreamsListener) {
				((TerminalsStreamsListener)getStreamsListener()).registerDataReceiver(receiver);
			}
		}

		return stream;
	}

	/**
	 * Connects the given stream id's to a local {@link OutputStream} instance.
	 *
	 * @param streamsListener The streams listener. Must not be <code>null</code>.
	 * @param streamIds The stream id's. Must not be <code>null</code>.
	 *
	 * @return The local output stream instance or <code>null</code>.
	 */
	protected OutputStream connectRemoteOutputStream(IStreams.StreamsListener streamsListener, String[] streamIds) {
		Assert.isNotNull(streamsListener);
		Assert.isNotNull(streamIds);

		PipedInputStream inStream = null;

		// Create the output stream receiving the data from local
		PipedOutputStream stream = new PipedOutputStream();
		// Create the piped input stream instance
		try { inStream = new PipedInputStream(stream); } catch (IOException e) { stream = null; }

		// If the stream creation succeeded, connect the data provider
		if (stream != null && inStream != null) {
			StreamsDataProvider provider = new StreamsDataProvider(new InputStreamReader(inStream), streamIds);
			// Register the data provider to the streams listener
			if (getStreamsListener() instanceof TerminalsStreamsListener) {
				((TerminalsStreamsListener)getStreamsListener()).setDataProvider(provider);
			}
		}

		return stream;
	}

	/**
	 * Initiate the terminal launch.
	 * <p>
	 * Called from {@link #executeLaunch()} or {@link #onAttachStreamsDone()}.
	 */
	protected void onAttachStreamsDone() {
		// Get the properties container
		final IPropertiesContainer properties = getProperties();
		if (properties == null) {
			// This is an illegal argument. Properties must be set
			IStatus status = new Status(IStatus.ERROR, CoreBundleActivator.getUniqueIdentifier(),
							NLS.bind(Messages.TerminalsLauncher_error_illegalNullArgument, "properties"), //$NON-NLS-1$
							new IllegalArgumentException());
			invokeCallback(status, null);
			return;
		}

		// Create the terminal listener
		terminalsListener = createTerminalsListener();
		if (terminalsListener != null) {
			getSvcTerminals().addListener(terminalsListener);
		}

		// Get the terminal attributes

		// Terminal Type: Default to "ansi" if not explicitly specified
		String type = properties.getStringProperty(ITerminalsLauncher.PROP_TERMINAL_TYPE);
		if (type == null || "".equals(type.trim())) type = "ansi"; //$NON-NLS-1$ //$NON-NLS-2$

		// Terminal Encoding: Default to "null" if not explicitly specified
		String encoding = properties.getStringProperty(ITerminalsLauncher.PROP_TERMINAL_ENCODING);

		// Environment: Default to "null" if not explicitly specified
		Map<String, String> env = (Map<String, String>)properties.getProperty(ITerminalsLauncher.PROP_TERMINAL_ENV);

		// Launch the remote terminal
		getSvcTerminals().launch(type, encoding, makeEnvironmentArray(env), new ITerminals.DoneLaunch() {
			@Override
			public void doneLaunch(IToken token, Exception error, ITerminals.TerminalContext terminal) {
				if (error != null) {
					// Construct the error message to show to the user
					String message = NLS.bind(Messages.TerminalsLauncher_error_terminalLaunchFailed,
							  		  properties.getStringProperty(ITerminalsLauncher.PROP_CONNECTION_NAME));
					message += NLS.bind(Messages.TerminalsLauncher_error_possibleCause,
									error.getLocalizedMessage() != null ? error.getLocalizedMessage() : Messages.TerminalsLauncher_cause_startFailed);

					// Construct the status object
					IStatus status = new Status(IStatus.ERROR, CoreBundleActivator.getUniqueIdentifier(), message, error);
					invokeCallback(status, null);
				} else {
					// Register the terminal context to the listener
					onTerminalLaunchDone(terminal);
				}
			}
		});
	}

	/**
	 * Register the terminals context to the listeners.
	 *
	 * @param terminal The terminals context or <code>null</code>.
	 */
	protected void onTerminalLaunchDone(ITerminals.TerminalContext terminal) {
		// Register the terminal context with the listeners
		if (terminal != null) {
			if (CoreBundleActivator.getTraceHandler().isSlotEnabled(0, ITraceIds.TRACE_TERMINALS_LAUNCHER)) {
				CoreBundleActivator.getTraceHandler().trace("Terminal context created: id='" + terminal.getID() + "', PTY type='" + terminal.getPtyType() + "'", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
								0, ITraceIds.TRACE_TERMINALS_LAUNCHER, IStatus.INFO, getClass());
			}

			// Remember the terminal context
			terminalContext = terminal;

			// Push the terminals context to the listeners
			if (getStreamsListener() instanceof ITerminalsContextAwareListener) {
				((ITerminalsContextAwareListener)getStreamsListener()).setTerminalsContext(terminal);
			}
			if (getTerminalsListener() instanceof ITerminalsContextAwareListener) {
				((ITerminalsContextAwareListener)getTerminalsListener()).setTerminalsContext(terminal);
			}

			// Send a notification
			TerminalsStateChangeEvent event = createRemoteTerminalsStateChangeEvent(terminal);
			if (event != null) EventManager.getInstance().fireEvent(event);
		}

		// Invoke the callback to signal that we are done
		invokeCallback(Status.OK_STATUS, terminal);
	}

	/**
	 * Creates a new remote terminals state change event instance.
	 *
	 * @param context The terminals context. Must not be <code>null</code>.
	 * @return The event instance or <code>null</code>.
	 */
	protected TerminalsStateChangeEvent createRemoteTerminalsStateChangeEvent(ITerminals.TerminalContext context) {
		Assert.isNotNull(context);
		return new TerminalsStateChangeEvent(context, TerminalsStateChangeEvent.EVENT_TERMINAL_CREATED, Boolean.FALSE, Boolean.TRUE, -1);
	}

	/**
	 * Invoke the callback with the given parameters. If the given status severity
	 * is {@link IStatus#ERROR}, the terminals launcher object is disposed automatically.
	 *
	 * @param status The status. Must not be <code>null</code>.
	 * @param result The result object or <code>null</code>.
	 */
	protected void invokeCallback(IStatus status, Object result) {
		// Dispose the terminal launcher if we report an error
		if (status.getSeverity() == IStatus.ERROR) {
			dispose();
		}

		// Invoke the callback
		ICallback callback = getCallback();
		if (callback != null) {
			callback.setResult(result);
			callback.done(this, status);
		}
	}

	/**
	 * Returns the channel instance.
	 *
	 * @return The channel instance or <code>null</code> if none.
	 */
	public final IChannel getChannel() {
		return channel;
	}

	/**
	 * Returns the terminals properties container.
	 *
	 * @return The terminals properties container or <code>null</code> if none.
	 */
	public final IPropertiesContainer getProperties() {
		return properties;
	}

	/**
	 * Returns the terminals service instance.
	 *
	 * @return The terminals service instance or <code>null</code> if none.
	 */
	public final ITerminals getSvcTerminals() {
		return svcTerminals;
	}

	/**
	 * Returns the streams service instance.
	 *
	 * @return The streams service instance or <code>null</code> if none.
	 */
	public final IStreams getSvcStreams() {
		return svcStreams;
	}

	/**
	 * Returns the callback instance.
	 *
	 * @return The callback instance or <code>null</code> if none.
	 */
	protected final ICallback getCallback() {
		return callback;
	}

	/**
	 * Create the streams listener instance.
	 *
	 * @return The streams listener instance or <code>null</code> if none.
	 */
	protected IStreams.StreamsListener createStreamsListener() {
		return new TerminalsStreamsListener(this);
	}

	/**
	 * Returns the streams listener instance.
	 *
	 * @return The streams listener instance or <code>null</code>.
	 */
	protected final IStreams.StreamsListener getStreamsListener() {
		return streamsListener;
	}

	/**
	 * Create the terminals listener instance.
	 *
	 * @return The terminals listener instance or <code>null</code> if none.
	 */
	protected ITerminals.TerminalsListener createTerminalsListener() {
		return new TerminalsListener(this);
	}

	/**
	 * Returns the terminals listener instance.
	 *
	 * @return The terminals listener instance or <code>null</code> if none.
	 */
	protected final ITerminals.TerminalsListener getTerminalsListener() {
		return terminalsListener;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.runtime.PlatformObject#getAdapter(java.lang.Class)
	 */
	@Override
	public Object getAdapter(Class adapter) {
		if (adapter.isAssignableFrom(ITerminals.TerminalsListener.class)) {
			return terminalsListener;
		}
		else if (adapter.isAssignableFrom(IStreams.StreamsListener.class)) {
			return streamsListener;
		}
		else if (adapter.isAssignableFrom(IStreams.class)) {
			return svcStreams;
		}
		else if (adapter.isAssignableFrom(ITerminals.class)) {
			return svcTerminals;
		}
		else if (adapter.isAssignableFrom(IChannel.class)) {
			return channel;
		}
		else if (adapter.isAssignableFrom(IPropertiesContainer.class)) {
			return properties;
		}
		else if (adapter.isAssignableFrom(ITerminals.TerminalContext.class)) {
			return terminalContext;
		}
		else if (adapter.isAssignableFrom(this.getClass())) {
			return this;
		}


		return super.getAdapter(adapter);
	}

	/**
	 * Makes an environment array out of the given map.
	 *
	 * @param env The environment map or <code>null</code>.
	 * @return The string.
	 */
	private String[] makeEnvironmentArray(Map<String, String> env) {
		if (env == null) return null;

		List<String> envList = new ArrayList<String>();
		for (String key : env.keySet()) {
			String entry = key.trim() + "=" + env.get(key).trim(); //$NON-NLS-1$
			envList.add(entry);
		}

		return envList.toArray(new String[envList.size()]);
	}
}

Back to the top