PageRenderTime 76ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/src/gwt/src/org/rstudio/studio/client/workbench/views/terminal/TerminalList.java

https://github.com/rstudio/rstudio
Java | 482 lines | 306 code | 51 blank | 125 comment | 43 complexity | 680641ce7cfbdb68946093b75a486cfd MD5 | raw file
  1. /*
  2. * TerminalList.java
  3. *
  4. * Copyright (C) 2022 by RStudio, PBC
  5. *
  6. * Unless you have received this program directly from RStudio pursuant
  7. * to the terms of a commercial license agreement with RStudio, then
  8. * this program is licensed to you under the terms of version 3 of the
  9. * GNU Affero General Public License. This program is distributed WITHOUT
  10. * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
  11. * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
  12. * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
  13. *
  14. */
  15. package org.rstudio.studio.client.workbench.views.terminal;
  16. import java.util.Iterator;
  17. import java.util.LinkedHashMap;
  18. import org.rstudio.core.client.BrowseCap;
  19. import org.rstudio.core.client.ResultCallback;
  20. import org.rstudio.core.client.StringUtil;
  21. import org.rstudio.studio.client.RStudioGinjector;
  22. import org.rstudio.studio.client.application.events.EventBus;
  23. import org.rstudio.studio.client.common.console.ConsoleProcess.ConsoleProcessFactory;
  24. import org.rstudio.studio.client.common.console.ConsoleProcessInfo;
  25. import org.rstudio.studio.client.workbench.views.terminal.events.TerminalBusyEvent;
  26. import org.rstudio.studio.client.workbench.views.terminal.events.TerminalCwdEvent;
  27. import org.rstudio.studio.client.workbench.views.terminal.events.TerminalReceivedConsoleProcessInfoEvent;
  28. import org.rstudio.studio.client.workbench.views.terminal.events.TerminalSubprocEvent;
  29. import com.google.inject.Inject;
  30. import com.google.inject.Provider;
  31. /**
  32. * List of terminals, with sufficient metadata to display a list of
  33. * available terminals and reconnect to them.
  34. */
  35. public class TerminalList implements Iterable<String>,
  36. TerminalCwdEvent.Handler,
  37. TerminalReceivedConsoleProcessInfoEvent.Handler
  38. {
  39. private static class TerminalListData
  40. {
  41. TerminalListData(ConsoleProcessInfo cpi, boolean hasSession)
  42. {
  43. cpi_ = cpi;
  44. sessionCreated_ = hasSession;
  45. }
  46. ConsoleProcessInfo getCPI()
  47. {
  48. return cpi_;
  49. }
  50. void setSessionCreated()
  51. {
  52. sessionCreated_ = true;
  53. }
  54. boolean getSessionCreated()
  55. {
  56. return sessionCreated_;
  57. }
  58. private final ConsoleProcessInfo cpi_;
  59. private boolean sessionCreated_;
  60. }
  61. protected TerminalList()
  62. {
  63. RStudioGinjector.INSTANCE.injectMembers(this);
  64. eventBus_.addHandler(TerminalCwdEvent.TYPE, this);
  65. eventBus_.addHandler(TerminalReceivedConsoleProcessInfoEvent.TYPE, this);
  66. }
  67. @Inject
  68. private void initialize(Provider<ConsoleProcessFactory> pConsoleProcessFactory,
  69. EventBus events)
  70. {
  71. pConsoleProcessFactory_ = pConsoleProcessFactory;
  72. eventBus_ = events;
  73. }
  74. /**
  75. * Add metadata from supplied TerminalSession
  76. * @param term terminal to add
  77. */
  78. public void addTerminal(TerminalSession term)
  79. {
  80. addTerminal(term.getProcInfo(), true);
  81. }
  82. /**
  83. * Add metadata from supplied ConsoleProcessInfo
  84. * @param procInfo metadata to add
  85. */
  86. public void addTerminal(ConsoleProcessInfo procInfo, boolean hasSession)
  87. {
  88. terminals_.put(procInfo.getHandle(), new TerminalListData(procInfo, hasSession));
  89. updateTerminalBusyStatus();
  90. }
  91. /**
  92. * Change terminal title.
  93. * @param handle handle of terminal
  94. * @param title new title
  95. * @return true if title was changed, false if it was unchanged
  96. */
  97. public boolean retitleTerminal(String handle, String title)
  98. {
  99. ConsoleProcessInfo current = getMetadataForHandle(handle);
  100. if (current == null)
  101. {
  102. return false;
  103. }
  104. if (!StringUtil.equals(current.getTitle(), title))
  105. {
  106. current.setTitle(title);
  107. return true;
  108. }
  109. return false;
  110. }
  111. /**
  112. * update has subprocesses flag
  113. * @param handle terminal handle
  114. * @param childProcs new subprocesses flag value
  115. */
  116. private void setChildProcs(String handle, boolean childProcs)
  117. {
  118. ConsoleProcessInfo current = getMetadataForHandle(handle);
  119. if (current == null)
  120. return;
  121. if (current.getHasChildProcs() != childProcs)
  122. {
  123. current.setHasChildProcs(childProcs);
  124. }
  125. }
  126. /**
  127. * update current working directory
  128. * @param handle terminal handle
  129. * @param cwd new directory
  130. */
  131. private void setCwd(String handle, String cwd)
  132. {
  133. ConsoleProcessInfo current = getMetadataForHandle(handle);
  134. if (current == null)
  135. return;
  136. if (!StringUtil.equals(current.getCwd(), cwd))
  137. {
  138. current.setCwd(cwd);
  139. }
  140. }
  141. /**
  142. * update zombie flag
  143. * @param handle terminal handle
  144. * @param zombie new zombie flag setting
  145. */
  146. public void setZombie(String handle, boolean zombie)
  147. {
  148. ConsoleProcessInfo current = getMetadataForHandle(handle);
  149. if (current == null)
  150. return;
  151. current.setZombie(zombie);
  152. }
  153. public void setExitCode(String handle, int exitCode)
  154. {
  155. ConsoleProcessInfo current = getMetadataForHandle(handle);
  156. if (current == null)
  157. return;
  158. current.setExitCode(exitCode);
  159. }
  160. /**
  161. * update caption
  162. * @param handle terminal handle
  163. * @param caption new caption
  164. */
  165. public void setCaption(String handle, String caption)
  166. {
  167. ConsoleProcessInfo current = getMetadataForHandle(handle);
  168. if (current == null)
  169. return;
  170. current.setCaption(caption);
  171. }
  172. /**
  173. * Remove given terminal from the list
  174. * @param handle terminal handle
  175. */
  176. void removeTerminal(String handle)
  177. {
  178. terminals_.remove(handle);
  179. updateTerminalBusyStatus();
  180. }
  181. /**
  182. * Kill all known terminal server processes, remove them from the server-
  183. * side list, and from the client-side list.
  184. */
  185. void terminateAll()
  186. {
  187. for (final java.util.Map.Entry<String, TerminalListData> item : terminals_.entrySet())
  188. {
  189. pConsoleProcessFactory_.get().interruptAndReap(item.getValue().getCPI().getHandle());
  190. }
  191. terminals_.clear();
  192. updateTerminalBusyStatus();
  193. }
  194. /**
  195. * Number of terminals in cache.
  196. * @return number of terminals tracked by this object
  197. */
  198. public int terminalCount()
  199. {
  200. return terminals_.size();
  201. }
  202. /**
  203. * Return 0-based index of a terminal in the list.
  204. * @param handle terminal to find
  205. * @return 0-based index of terminal, -1 if not found
  206. */
  207. public int indexOfTerminal(String handle)
  208. {
  209. int i = 0;
  210. for (final java.util.Map.Entry<String, TerminalListData> item : terminals_.entrySet())
  211. {
  212. if (StringUtil.equals(item.getValue().getCPI().getHandle(), handle))
  213. {
  214. return i;
  215. }
  216. i++;
  217. }
  218. return -1;
  219. }
  220. /**
  221. * Return terminal handle at given 0-based index
  222. * @param i zero-based index
  223. * @return handle of terminal at index, or null if invalid index
  224. */
  225. public String terminalHandleAtIndex(int i)
  226. {
  227. int j = 0;
  228. for (final java.util.Map.Entry<String, TerminalListData> item : terminals_.entrySet())
  229. {
  230. if (i == j)
  231. {
  232. return item.getValue().getCPI().getHandle();
  233. }
  234. j++;
  235. }
  236. return null;
  237. }
  238. /**
  239. * Obtain handle for given caption.
  240. * @param caption to find
  241. * @return handle if found, or null
  242. */
  243. public String handleForCaption(String caption)
  244. {
  245. for (final java.util.Map.Entry<String, TerminalListData> item : terminals_.entrySet())
  246. {
  247. if (StringUtil.equals(item.getValue().getCPI().getCaption(), caption))
  248. {
  249. return item.getValue().getCPI().getHandle();
  250. }
  251. }
  252. return null;
  253. }
  254. /**
  255. * Obtain autoclose mode for a given terminal handle.
  256. * @param handle handle to query
  257. * @return autoclose mode; if terminal not in list, returns AUTOCLOSE_DEFAULT
  258. */
  259. public int autoCloseForHandle(String handle)
  260. {
  261. ConsoleProcessInfo meta = getMetadataForHandle(handle);
  262. if (meta == null)
  263. return ConsoleProcessInfo.AUTOCLOSE_DEFAULT;
  264. else
  265. return meta.getAutoCloseMode();
  266. }
  267. /**
  268. * Get ConsoleProcessInfo for terminal with given handle.
  269. * @param handle handle of terminal of interest
  270. * @return terminal metadata or null if not found
  271. */
  272. public ConsoleProcessInfo getMetadataForHandle(String handle)
  273. {
  274. TerminalListData data = getFullMetadataForHandle(handle);
  275. if (data == null)
  276. return null;
  277. return data.getCPI();
  278. }
  279. /**
  280. * Get metadata for terminal with given handle.
  281. * @param handle handle of terminal of interest
  282. * @return terminal metadata or null if not found
  283. */
  284. private TerminalListData getFullMetadataForHandle(String handle)
  285. {
  286. return terminals_.get(handle);
  287. }
  288. /**
  289. * @param handle handle to find
  290. * @return caption for that handle or null if no such handle
  291. */
  292. public String getCaption(String handle)
  293. {
  294. ConsoleProcessInfo data = getMetadataForHandle(handle);
  295. if (data == null)
  296. {
  297. return null;
  298. }
  299. return data.getCaption();
  300. }
  301. /**
  302. * @param handle handle to find
  303. * @return does terminal have subprocesses
  304. */
  305. public boolean getHasSubprocs(String handle)
  306. {
  307. if (BrowseCap.isWindowsDesktop())
  308. return false;
  309. ConsoleProcessInfo data = getMetadataForHandle(handle);
  310. if (data == null)
  311. {
  312. return true;
  313. }
  314. return data.getHasChildProcs();
  315. }
  316. /**
  317. * @return true if any of the terminal shells have subprocesses
  318. */
  319. public boolean haveSubprocs()
  320. {
  321. if (BrowseCap.isWindowsDesktop())
  322. return false;
  323. for (final TerminalListData item : terminals_.values())
  324. {
  325. if (item.getCPI().getHasChildProcs())
  326. {
  327. return true;
  328. }
  329. }
  330. return false;
  331. }
  332. /**
  333. * Orchestrates the creation and connection of a terminal.
  334. *
  335. * @param session TerminalSession widget to contain the terminal emulator. Xterm.js requires
  336. * the Widget's element be visible (have dimensions)
  337. * @param callback done, Boolean if successful, String if failed
  338. */
  339. public void startTerminal(TerminalSession session,
  340. final ResultCallback<Boolean, String> callback)
  341. {
  342. // When terminals are created via the R API, guard against creation of multiple
  343. // TerminalSession objects for the same terminal. For terminals created via the
  344. // UI, we already guard against this via TerminalPane.creatingTerminal_.
  345. TerminalListData existing = getFullMetadataForHandle(session.getProcInfo().getHandle());
  346. if (existing != null && existing.getSessionCreated())
  347. {
  348. callback.onSuccess(false);
  349. return;
  350. }
  351. // initialize xterm.js
  352. session.open(() -> {
  353. if (existing != null)
  354. {
  355. existing.setSessionCreated();
  356. }
  357. // connect the emulator to server-side process
  358. session.connect(new ResultCallback<Boolean, String>()
  359. {
  360. @Override
  361. public void onSuccess(Boolean connected)
  362. {
  363. if (connected)
  364. updateTerminalBusyStatus();
  365. callback.onSuccess(connected);
  366. }
  367. @Override
  368. public void onFailure(String msg)
  369. {
  370. callback.onFailure(msg);
  371. }
  372. });
  373. });
  374. }
  375. /**
  376. * Broadcast event which indicates if any terminals are busy.
  377. */
  378. private void updateTerminalBusyStatus()
  379. {
  380. eventBus_.fireEvent(new TerminalBusyEvent(haveSubprocs()));
  381. }
  382. @Override
  383. public Iterator<String> iterator()
  384. {
  385. return terminals_.keySet().iterator();
  386. }
  387. public void updateTerminalSubprocsStatus(TerminalSubprocEvent event)
  388. {
  389. setChildProcs(event.getHandle(), event.hasSubprocs());
  390. updateTerminalBusyStatus();
  391. }
  392. @Override
  393. public void onTerminalCwd(TerminalCwdEvent event)
  394. {
  395. setCwd(event.getHandle(), event.getCwd());
  396. }
  397. @Override
  398. public void onTerminalReceivedConsoleProcessInfo(TerminalReceivedConsoleProcessInfoEvent event)
  399. {
  400. addTerminal(event.getData(), true);
  401. }
  402. public String debug_dumpTerminalList()
  403. {
  404. StringBuilder dump = new StringBuilder();
  405. dump.append(constants_.terminalListCountText());
  406. dump.append(terminalCount());
  407. dump.append("\n");
  408. for (int i = 0; i < terminalCount(); i++)
  409. {
  410. dump.append(constants_.handleDumpText());
  411. String handle = terminalHandleAtIndex(i);
  412. dump.append(handle);
  413. dump.append(constants_.captionDumpText());
  414. TerminalListData data = getFullMetadataForHandle(handle);
  415. dump.append(data.getCPI().getCaption());
  416. dump.append(constants_.sessionCreatedText());
  417. dump.append(data.getSessionCreated());
  418. dump.append("\n");
  419. }
  420. return dump.toString();
  421. }
  422. /**
  423. * Map of terminal handles to terminal metadata; order they are added
  424. * is the order they will be iterated.
  425. */
  426. private final LinkedHashMap<String, TerminalListData> terminals_ = new LinkedHashMap<>();
  427. // Injected ----
  428. private Provider<ConsoleProcessFactory> pConsoleProcessFactory_;
  429. private EventBus eventBus_;
  430. private static final TerminalConstants constants_ = com.google.gwt.core.client.GWT.create(TerminalConstants.class);
  431. }