/*
* TerminalList.java
*
* Copyright (C) 2009-17 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.studio.client.workbench.views.terminal;
import java.util.Iterator;
import java.util.LinkedHashMap;
import org.rstudio.core.client.StringUtil;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.console.ConsoleProcess.ConsoleProcessFactory;
import org.rstudio.studio.client.common.console.ConsoleProcessInfo;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.views.terminal.events.TerminalBusyEvent;
import org.rstudio.studio.client.workbench.views.terminal.events.TerminalSubprocEvent;
import com.google.inject.Inject;
import com.google.inject.Provider;
/**
* List of terminals, with sufficient metadata to display a list of
* available terminals and reconnect to them.
*/
public class TerminalList implements Iterable<String>,
TerminalSubprocEvent.Handler
{
private static class TerminalMetadata
{
/**
* Create a TerminalMetadata object
* @param handle terminal handle, unique key
* @param caption terminal caption, shown in terminal picker
* @param title terminal title, shown in toolbar above active terminal
* @param sequence terminal sequence number
* @param childProcs is terminal busy
* @param cols number of columns in pseudoterminal
* @param rows number of rows in pseudoterminal
* @param shellType type of shell to run
*/
private TerminalMetadata(String handle,
String caption,
String title,
int sequence,
boolean childProcs,
int cols,
int rows,
int shellType)
{
handle_ = StringUtil.notNull(handle);
caption_ = StringUtil.notNull(caption);
title_ = StringUtil.notNull(title);
sequence_ = sequence;
childProcs_ = childProcs;
cols_ = cols;
rows_ = rows;
shellType_ = shellType;
}
private TerminalMetadata(TerminalMetadata original,
String newTitle)
{
this(original.handle_,
original.caption_,
newTitle,
original.sequence_,
original.childProcs_,
original.cols_,
original.rows_,
original.shellType_);
}
private TerminalMetadata(ConsoleProcessInfo procInfo)
{
this(procInfo.getHandle(),
procInfo.getCaption(),
procInfo.getTitle(),
procInfo.getTerminalSequence(),
procInfo.getHasChildProcs(),
ConsoleProcessInfo.DEFAULT_COLS,
ConsoleProcessInfo.DEFAULT_ROWS,
procInfo.getShellType()
);
}
private TerminalMetadata(TerminalSession term)
{
this(term.getHandle(),
term.getCaption(),
term.getTitle(),
term.getSequence(),
term.getHasChildProcs(),
term.getCols(),
term.getRows(),
term.getShellType());
}
/**
* @return unique identifier for terminal
*/
public String getHandle() { return handle_; }
/**
* @return caption for terminal, shown in terminal picker
*/
public String getCaption() { return caption_; }
/**
* @return title for terminal, shown above the terminal pane
*/
public String getTitle() { return title_; }
/**
* @return relative order of terminal creation, used to pick number for
* unique default terminal caption, e.g. "Terminal 3"
*/
public int getSequence() { return sequence_; }
/**
* @return true if terminal shell has child processes
*/
public boolean getChildProcs() { return childProcs_; }
public int getCols() { return cols_; }
public int getRows() { return rows_; }
public int getShellType() { return shellType_; }
private String handle_;
private String caption_;
private String title_;
private int sequence_;
private boolean childProcs_;
private int cols_;
private int rows_;
private int shellType_;
}
protected TerminalList()
{
RStudioGinjector.INSTANCE.injectMembers(this);
eventBus_.addHandler(TerminalSubprocEvent.TYPE, this);
}
@Inject
private void initialize(Provider<ConsoleProcessFactory> pConsoleProcessFactory,
EventBus events,
UIPrefs uiPrefs)
{
pConsoleProcessFactory_ = pConsoleProcessFactory;
eventBus_ = events;
uiPrefs_ = uiPrefs;
}
/**
* Add metadata from supplied TerminalSession
* @param terminal terminal to add
*/
public void addTerminal(TerminalSession terminal)
{
addTerminal(new TerminalMetadata(terminal));
}
/**
* Add metadata from supplied ConsoleProcessInfo
* @param procInfo metadata to add
*/
public void addTerminal(ConsoleProcessInfo procInfo)
{
addTerminal(new TerminalMetadata(procInfo));
}
/**
* Change terminal title.
* @param handle handle of terminal
* @param title new title
* @return true if title was changed, false if it was unchanged
*/
public boolean retitleTerminal(String handle, String title)
{
TerminalMetadata current = getMetadataForHandle(handle);
if (current == null)
{
return false;
}
if (!current.getTitle().equals(title))
{
addTerminal(new TerminalMetadata(current, title));
return true;
}
return false;
}
/**
* update has subprocesses flag
* @param handle terminal handle
* @param childProcs new subprocesses flag value
* @return true if changed, false if unchanged
*/
private boolean setChildProcs(String handle, boolean childProcs)
{
TerminalMetadata current = getMetadataForHandle(handle);
if (current == null)
{
return false;
}
if (current.getChildProcs() != childProcs)
{
addTerminal(new TerminalMetadata(
current.handle_,
current.caption_,
current.title_,
current.sequence_,
childProcs,
current.cols_,
current.rows_,
current.shellType_));
return true;
}
return false;
}
/**
* Remove given terminal from the list
* @param handle terminal handle
*/
void removeTerminal(String handle)
{
terminals_.remove(handle);
updateTerminalBusyStatus();
}
/**
* Kill all known terminal server processes, remove them from the server-
* side list, and from the client-side list.
*/
void terminateAll()
{
for (final java.util.Map.Entry<String, TerminalMetadata> item : terminals_.entrySet())
{
pConsoleProcessFactory_.get().interruptAndReap(item.getValue().getHandle());
}
terminals_.clear();
updateTerminalBusyStatus();
}
/**
* Number of terminals in cache.
* @return number of terminals tracked by this object
*/
public int terminalCount()
{
return terminals_.size();
}
/**
* Return 0-based index of a terminal in the list.
* @param handle terminal to find
* @return 0-based index of terminal, -1 if not found
*/
public int indexOfTerminal(String handle)
{
int i = 0;
for (final java.util.Map.Entry<String, TerminalMetadata> item : terminals_.entrySet())
{
if (item.getValue().getHandle().equals(handle))
{
return i;
}
i++;
}
return -1;
}
/**
* Return terminal handle at given 0-based index
* @param i zero-based index
* @return handle of terminal at index, or null if invalid index
*/
public String terminalHandleAtIndex(int i)
{
int j = 0;
for (final java.util.Map.Entry<String, TerminalMetadata> item : terminals_.entrySet())
{
if (i == j)
{
return item.getValue().getHandle();
}
j++;
}
return null;
}
/**
* Determine if a caption is already in use
* @param caption to check
* @return true if caption is not in use (i.e. a new terminal can use it)
*/
public boolean isCaptionAvailable(String caption)
{
for (final java.util.Map.Entry<String, TerminalMetadata> item : terminals_.entrySet())
{
if (item.getValue().getCaption().equals(caption))
{
return false;
}
}
return true;
}
/**
* Obtain handle for given caption.
* @param caption to find
* @return handle if found, or null
*/
public String handleForCaption(String caption)
{
for (final java.util.Map.Entry<String, TerminalMetadata> item : terminals_.entrySet())
{
if (item.getValue().getCaption().equals(caption))
{
return item.getValue().getHandle();
}
}
return null;
}
/**
* Get metadata for terminal with given handle.
* @param handle handle of terminal of interest
* @return terminal metadata or null if not found
*/
private TerminalMetadata getMetadataForHandle(String handle)
{
return terminals_.get(handle);
}
/**
* Initiate startup of a new terminal
*/
public void createNewTerminal()
{
startTerminal(nextTerminalSequence(),
null, // handle
null, // caption
null, // title
true, // childProcs
ConsoleProcessInfo.DEFAULT_COLS,
ConsoleProcessInfo.DEFAULT_ROWS,
TerminalShellInfo.SHELL_DEFAULT);
}
/**
* Initiate startup of a new terminal with specified caption.
* @param caption desired caption; if null or empty creates standard caption
* @return true if caption available, false if name already in use
*/
public boolean createNamedTerminal(String caption)
{
if (StringUtil.isNullOrEmpty(caption))
{
createNewTerminal();
return true;
}
// is this terminal name available?
if (!isCaptionAvailable(caption))
{
return false;
}
startTerminal(nextTerminalSequence(),
null, // handle
caption, // caption
null, // title
true, // childProcs
ConsoleProcessInfo.DEFAULT_COLS,
ConsoleProcessInfo.DEFAULT_ROWS,
TerminalShellInfo.SHELL_DEFAULT);
return true;
}
/**
* Reconnect to a known terminal.
* @param handle
* @return true if terminal was known and reconnect initiated
*/
public boolean reconnectTerminal(String handle)
{
TerminalMetadata existing = getMetadataForHandle(handle);
if (existing == null)
{
return false;
}
startTerminal(existing.getSequence(),
handle,
existing.getCaption(),
existing.getTitle(),
existing.getChildProcs(),
existing.getCols(),
existing.getRows(),
existing.getShellType());
return true;
}
/**
* @param handle handle to find
* @return caption for that handle or null if no such handle
*/
public String getCaption(String handle)
{
TerminalMetadata data = getMetadataForHandle(handle);
if (data == null)
{
return null;
}
return data.caption_;
}
/**
* @param handle handle to find
* @return does terminal have subprocesses
*/
public boolean getHasSubprocs(String handle)
{
TerminalMetadata data = getMetadataForHandle(handle);
if (data == null)
{
return true;
}
return data.childProcs_;
}
/**
* @return true if any of the terminal shells have subprocesses
*/
public boolean haveSubprocs()
{
for (final TerminalMetadata item : terminals_.values())
{
if (item.childProcs_)
{
return true;
}
}
return false;
}
/**
* Choose a 1-based sequence number one higher than the highest currently
* known terminal number. We don't try to fill gaps if terminals are closed
* in the middle of the opened tabs.
* @return Highest currently known terminal plus one
*/
private int nextTerminalSequence()
{
int maxNum = ConsoleProcessInfo.SEQUENCE_NO_TERMINAL;
for (final java.util.Map.Entry<String, TerminalMetadata> item : terminals_.entrySet())
{
maxNum = Math.max(maxNum, item.getValue().getSequence());
}
return maxNum + 1;
}
private void startTerminal(int sequence,
String terminalHandle,
String caption,
String title,
boolean hasChildProcs,
int cols,
int rows,
int shellType)
{
TerminalSession newSession = new TerminalSession(
sequence, terminalHandle, caption, title, hasChildProcs,
cols, rows, uiPrefs_.blinkingCursor().getValue(), true /*focus*/, shellType);
newSession.connect();
updateTerminalBusyStatus();
}
private void addTerminal(TerminalMetadata terminal)
{
terminals_.put(terminal.getHandle(), terminal);
updateTerminalBusyStatus();
}
private void updateTerminalBusyStatus()
{
eventBus_.fireEvent(new TerminalBusyEvent(haveSubprocs()));
}
@Override
public Iterator<String> iterator()
{
return terminals_.keySet().iterator();
}
@Override
public void onTerminalSubprocs(TerminalSubprocEvent event)
{
setChildProcs(event.getHandle(), event.hasSubprocs());
updateTerminalBusyStatus();
}
/**
* Map of terminal handles to terminal metadata; order they are added
* is the order they will be iterated.
*/
private LinkedHashMap<String, TerminalMetadata> terminals_ =
new LinkedHashMap<String, TerminalMetadata>();
// Injected ----
private Provider<ConsoleProcessFactory> pConsoleProcessFactory_;
private EventBus eventBus_;
private UIPrefs uiPrefs_;
}