Skip to content

Commit 6ac1b6a

Browse files
Add --isolated for per-launch process isolation (#48)
Adds `--isolated`: that launch gets its own process, never forwards to a running instance, and never becomes the broker for later ones. RDM needs this to swap its bundled wt-distro for dt. It starts one terminal per entry, each with its own env vars (secrets included), `WT_BASE_SETTINGS_PATH` and `WT_PARENT_WINDOW_HANDLE`. With the broker, the second launch forwards to the first process and exits, so entry B's shell gets entry A's env and settings, and the parent HWND is dropped. RDM gets around this today with WT's `compatibility.isolatedMode` ([WindowManager.cpp](https://github.com/microsoft/terminal/blob/v1.20.11271.0/src/cascadia/Remoting/WindowManager.cpp#L118-L193)), but WT removed that setting in 1.23 (microsoft/terminal#18215). DHowett floated bringing it back as `wt --isolated` in microsoft/terminal#19468. Why a flag and not the old setting: a dt that doesn't know the setting silently ignores it and leaks, while an unknown flag fails the launch (`dt: Unknown command '--isolated'.`). It's also per launch, and nothing has to be read from `settings.json` before the election. With `--isolated`: - the host and `dt.exe` skip forwarding, election and the endpoint file, so a normal launch later can't land in an isolated process either - persisted layouts aren't restored or saved, same as WT's [`ShouldUsePersistedLayout`](https://github.com/microsoft/terminal/blob/v1.20.11271.0/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp#L252-L255) - `-w use-existing` / window ids exit with 3 **Testing** - Cli, App, Settings, Broker, Compatibility, Settings.Editor and UI tests pass; NativeAOT win-x64 publish has no warnings - Repro: start A, then B, each with a different `RDM_SECRET` and settings dir. Without the flag, B's shell is a child of A and prints A's secret. With `--isolated`, each launch keeps its own process, secret and settings dir. Checked on Debug and AOT builds, through both the host and `dt.exe`. A later launch without the flag also stays out of A. Not tested yet: HWND embedding inside RDM, macOS/Linux.
1 parent c2d4cc3 commit 6ac1b6a

11 files changed

Lines changed: 98 additions & 26 deletions

File tree

‎README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ settings file. On Windows, `WT_PARENT_WINDOW_HANDLE` embeds the window as a
8888
child of that HWND. `alwaysShowTabs: false` hides the tab row when only one
8989
tab is open.
9090

91+
`dt --isolated` (or `Devolutions.Terminal.exe --isolated`) runs the launch in
92+
its own process: it neither forwards to a running instance nor becomes the
93+
broker for later launches, so every window keeps its own environment,
94+
`WT_PARENT_WINDOW_HANDLE`, and settings directory. `-w` targeting of other
95+
windows is unavailable, and persisted layouts are neither restored nor saved.
96+
It is the per-launch equivalent of Windows Terminal's
97+
`compatibility.isolatedMode` setting, which Windows Terminal 1.23 removed.
98+
9199
Set `"experimental.terminalEngine": "ghostty"` to use the pinned
92100
`libghostty-vt` engine globally. A profile can override it with `"builtin"` or
93101
`"ghostty"`. ConPTY remains the Windows process transport for both engines.

‎src/Devolutions.Terminal.App/Models/TerminalLayoutStateStore.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ preference is not null &&
1010
preference.Equals("persistedWindowLayout", StringComparison.OrdinalIgnoreCase) ||
1111
preference.Equals("persistedLayoutAndContent", StringComparison.OrdinalIgnoreCase));
1212

13+
// Matches Windows Terminal: an isolated process never restores or saves the shared window layouts.
14+
public static bool ShouldUsePersistedLayout(string? firstWindowPreference, bool isolated) =>
15+
!isolated && IsPersistedLayoutPreference(firstWindowPreference);
16+
1317
public static WindowLayoutState? ReadWindowState(ApplicationStateStore store, int windowId)
1418
{
1519
ArgumentNullException.ThrowIfNull(store);

‎src/Devolutions.Terminal.App/Views/MainWindow.axaml.cs‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public partial class MainWindow :
8080
private readonly Func<GlobalSummonArgs, ValueTask<WindowActionResult>>? _summonRequested;
8181
private readonly Action<AppSettings>? _settingsChanged;
8282
private readonly ISystemMenuService _systemMenuService;
83+
private readonly bool _isolated;
8384
private readonly TaskCompletionSource<TerminalWindowActivationResult> _initialActivationCompletion =
8485
new(TaskCreationOptions.RunContinuationsAsynchronously);
8586
private readonly DispatcherTimer _notificationTimer;
@@ -108,10 +109,12 @@ public MainWindow(
108109
Action<string>? workspaceRequested = null,
109110
Func<GlobalSummonArgs, ValueTask<WindowActionResult>>? summonRequested = null,
110111
Action<AppSettings>? settingsChanged = null,
111-
ISystemMenuService? systemMenuService = null)
112+
ISystemMenuService? systemMenuService = null,
113+
bool isolated = false)
112114
{
113115
WindowId = windowId;
114116
WindowName = windowName;
117+
_isolated = isolated;
115118
_initialActivation = initialActivation;
116119
_newWindowRequested = newWindowRequested;
117120
_tabTearOffRequested = tabTearOffRequested;
@@ -4359,8 +4362,7 @@ private void PersistWorkspace(TerminalWindowLayoutDescriptor layout)
43594362
};
43604363

43614364
private bool UsesPersistedLayout =>
4362-
TerminalLayoutStateStore.IsPersistedLayoutPreference(
4363-
_settings.FirstWindowPreference);
4365+
TerminalLayoutStateStore.ShouldUsePersistedLayout(_settings.FirstWindowPreference, _isolated);
43644366

43654367
private void TryPersistCurrentLayout(TerminalWindowLayoutDescriptor layout)
43664368
{

‎src/Devolutions.Terminal.Cli/CliContracts.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public sealed record CliInvocation(
2424
CliLaunchMode LaunchMode,
2525
int? SavedLayout,
2626
IReadOnlyList<ActionAndArgs> Actions,
27-
CliSaveRequest? SaveRequest = null);
27+
CliSaveRequest? SaveRequest = null,
28+
bool Isolated = false);
2829

2930
public sealed record CliParseResult(
3031
int ExitCode,

‎src/Devolutions.Terminal.Cli/CliParser.cs‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ private static CliParseResult ParseCore(
118118
int? rows = null;
119119
int? savedLayout = null;
120120
var launchMode = CliLaunchMode.Default;
121+
var isolated = false;
121122
CliSaveRequest? save = null;
122123

123124
for (var segmentIndex = 0; segmentIndex < segments.Count; segmentIndex++)
@@ -135,7 +136,8 @@ private static CliParseResult ParseCore(
135136
ref columns,
136137
ref rows,
137138
ref savedLayout,
138-
ref launchMode);
139+
ref launchMode,
140+
ref isolated);
139141
}
140142

141143
var command = index < segment.Count && Commands.Contains(segment[index])
@@ -215,7 +217,8 @@ save is null &&
215217
launchMode,
216218
savedLayout,
217219
actions,
218-
save);
220+
save,
221+
isolated);
219222
return new(0, string.Empty, false, invocation);
220223
}
221224

@@ -228,7 +231,8 @@ private static void ParseRootOptions(
228231
ref int? columns,
229232
ref int? rows,
230233
ref int? savedLayout,
231-
ref CliLaunchMode launchMode)
234+
ref CliLaunchMode launchMode,
235+
ref bool isolated)
232236
{
233237
while (index < args.Count)
234238
{
@@ -269,6 +273,10 @@ private static void ParseRootOptions(
269273
launchMode |= CliLaunchMode.Focus;
270274
index++;
271275
break;
276+
case "--isolated":
277+
isolated = true;
278+
index++;
279+
break;
272280
default:
273281
if (option.StartsWith('-') &&
274282
option.Length > 2 &&
@@ -696,6 +704,7 @@ internal static class CommandLineSchema
696704
-M, --maximized Launch maximized
697705
-F, --fullscreen Launch fullscreen
698706
-f, --focus Hide the title bar
707+
--isolated Own process: never join or serve other launches
699708
700709
Commands:
701710
new-tab, nt split-pane, sp focus-tab, ft move-focus, mf
@@ -708,6 +717,7 @@ public static RootCommand Create()
708717
root.Options.Add(new Option<string>("--window") { Description = "Target window." });
709718
root.Options.Add(new Option<string>("--pos") { Description = "Window position." });
710719
root.Options.Add(new Option<string>("--size") { Description = "Terminal size." });
720+
root.Options.Add(new Option<bool>("--isolated") { Description = "Run in its own process." });
711721
foreach (var (name, alias) in new[]
712722
{
713723
("new-tab", "nt"),

‎src/Devolutions.Terminal.Cli/Program.cs‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ public static async Task<int> Main(string[] args)
1717
}
1818

1919
var invocation = parsed.Invocation!;
20-
var response = await new BrokerClient().SendAsync(
21-
invocation.TargetWindow,
22-
CliInvocationSerializer.Serialize(invocation)).ConfigureAwait(false);
20+
var response = invocation.Isolated
21+
? BrokerResponse.Unavailable("Isolated launches do not use the broker.")
22+
: await new BrokerClient().SendAsync(
23+
invocation.TargetWindow,
24+
CliInvocationSerializer.Serialize(invocation)).ConfigureAwait(false);
2325
if (response.Status == BrokerStatus.Unavailable)
2426
{
2527
if (RequiresExistingWindow(invocation.TargetWindow))

‎src/Devolutions.Terminal/App.axaml.cs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ public override void OnFrameworkInitializationCompleted()
3030
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
3131
{
3232
_desktop = desktop;
33-
_router = new TerminalWindowRouter(desktop, ConfigureWindow);
33+
_router = new TerminalWindowRouter(
34+
desktop,
35+
ConfigureWindow,
36+
isolated: InitialInvocation?.Isolated ?? false);
3437
BrokerHandler?.SetHandler(_router);
3538
if (OperatingSystem.IsMacOS())
3639
{

‎src/Devolutions.Terminal/Program.cs‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,30 +67,34 @@ public static int Main(string[] args)
6767

6868
var invocation = parsed.Invocation!;
6969
var deferredHandler = new DeferredBrokerHandler();
70-
var broker = BrokerHost.TryCreate(deferredHandler);
71-
if (broker is null)
70+
BrokerHost? broker = null;
71+
if (!invocation.Isolated)
7272
{
73-
var response = ForwardToPrimaryAsync(invocation).AsTask().GetAwaiter().GetResult();
74-
if (!response.IsSuccess)
73+
broker = BrokerHost.TryCreate(deferredHandler);
74+
if (broker is null)
7575
{
76-
Console.Error.WriteLine($"dt: {response.Message}");
77-
return response.Status == BrokerStatus.WindowNotFound ? 3 : 1;
78-
}
76+
var response = ForwardToPrimaryAsync(invocation).AsTask().GetAwaiter().GetResult();
77+
if (!response.IsSuccess)
78+
{
79+
Console.Error.WriteLine($"dt: {response.Message}");
80+
return response.Status == BrokerStatus.WindowNotFound ? 3 : 1;
81+
}
7982

80-
return 0;
83+
return 0;
84+
}
8185
}
8286

8387
if (invocation.TargetWindow.Equals("use-existing", StringComparison.OrdinalIgnoreCase) ||
8488
(int.TryParse(invocation.TargetWindow, out var requestedWindowId) && requestedWindowId > 0))
8589
{
86-
broker.DisposeAsync().AsTask().GetAwaiter().GetResult();
90+
broker?.DisposeAsync().AsTask().GetAwaiter().GetResult();
8791
Console.Error.WriteLine($"dt: terminal window '{invocation.TargetWindow}' was not found.");
8892
return 3;
8993
}
9094

9195
if (invocation.SaveRequest is { Commandline.Length: > 0 } saveRequest)
9296
{
93-
broker.DisposeAsync().AsTask().GetAwaiter().GetResult();
97+
broker?.DisposeAsync().AsTask().GetAwaiter().GetResult();
9498
try
9599
{
96100
var settings = Devolutions.Terminal.Settings.SettingsService.Load();
@@ -135,7 +139,7 @@ IOException or
135139
}
136140
finally
137141
{
138-
broker.DisposeAsync().AsTask().GetAwaiter().GetResult();
142+
broker?.DisposeAsync().AsTask().GetAwaiter().GetResult();
139143
}
140144
}
141145

‎src/Devolutions.Terminal/TerminalWindowRouter.cs‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,19 @@ internal sealed class TerminalWindowRouter : IBrokerRequestHandler, IDisposable
1717
private readonly ApplicationStateStore _stateStore;
1818
private readonly GlobalWindowActionRouter _windowActions;
1919
private readonly GlobalHotkeyManager _globalHotkeys;
20+
private readonly bool _isolated;
2021
private int _nextWindowId = 1;
2122

2223
public TerminalWindowRouter(
2324
IClassicDesktopStyleApplicationLifetime desktop,
2425
Action<MainWindow>? windowCreated = null,
2526
ApplicationStateStore? stateStore = null,
26-
IGlobalHotkeyBackend? globalHotkeyBackend = null)
27+
IGlobalHotkeyBackend? globalHotkeyBackend = null,
28+
bool isolated = false)
2729
{
2830
_desktop = desktop;
2931
_windowCreated = windowCreated;
32+
_isolated = isolated;
3033
_stateStore = stateStore ?? SettingsService.LoadApplicationState();
3134
_windowActions = new GlobalWindowActionRouter(CreateSummonWindow);
3235
_globalHotkeys = new GlobalHotkeyManager(
@@ -146,7 +149,8 @@ private MainWindow CreateWindow(TerminalWindowActivation activation, string name
146149
: $"{window.WindowName} ({window.WindowId})")
147150
.ToArray(),
148151
summonRequested: args => _windowActions.SummonAsync(window, args),
149-
settingsChanged: settings => TraceHotkeyResults(_globalHotkeys.Apply(settings.ActionMap)));
152+
settingsChanged: settings => TraceHotkeyResults(_globalHotkeys.Apply(settings.ActionMap)),
153+
isolated: _isolated);
150154
_windows.Add(window);
151155
_windowActions.Add(window);
152156
_windowCreated?.Invoke(window);
@@ -225,11 +229,12 @@ parsed.Invocation.PositionX is not null ||
225229
parsed.Invocation.PositionY is not null ||
226230
parsed.Invocation.Columns is not null ||
227231
parsed.Invocation.Rows is not null ||
228-
parsed.Invocation.LaunchMode != CliLaunchMode.Default)
232+
parsed.Invocation.LaunchMode != CliLaunchMode.Default ||
233+
parsed.Invocation.Isolated)
229234
{
230235
return new(
231236
false,
232-
"Window routing, position, size, and launch-mode options are not valid inside the current window's command palette.",
237+
"Window routing, position, size, launch-mode, and isolation options are not valid inside the current window's command palette.",
233238
[]);
234239
}
235240

‎tests/Devolutions.Terminal.App.Tests/LayoutDescriptorTests.cs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ public void NonPersistedFirstWindowPreferencesDoNotRestoreLayouts(string? prefer
2828
Assert.False(TerminalLayoutStateStore.IsPersistedLayoutPreference(preference));
2929
}
3030

31+
[Theory]
32+
[InlineData("persistedWindowLayout", false, true)]
33+
[InlineData("persistedWindowLayout", true, false)]
34+
[InlineData("defaultProfile", false, false)]
35+
[InlineData("defaultProfile", true, false)]
36+
public void IsolatedModeNeverUsesPersistedLayouts(string preference, bool isolated, bool expected)
37+
{
38+
Assert.Equal(expected, TerminalLayoutStateStore.ShouldUsePersistedLayout(preference, isolated));
39+
}
40+
3141
[Fact]
3242
public void WindowTabPaneLayoutRoundTripsThroughApplicationState()
3343
{

0 commit comments

Comments
 (0)