Skip to content

Commit 47c7fdb

Browse files
committed
feat: add agent status to tray app
- Adds agent and stopped workspace statuses to the tray app - The Vpn.Service.Manager now tracks the current list of workspaces and agents from the tunnel - Deletes remnants of the old Package - Moves App to be completely unpackaged
1 parent 88a4a97 commit 47c7fdb

20 files changed

+839
-560
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -403,3 +403,5 @@ FodyWeavers.xsd
403403
.idea/**/shelf
404404

405405
publish
406+
WindowsAppRuntimeInstall-x64.exe
407+
wintun.dll

App/App.csproj

+2-38
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,13 @@
1010
<PublishProfile>Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfile>
1111
<UseWinUI>true</UseWinUI>
1212
<Nullable>enable</Nullable>
13-
<EnableMsixTooling>true</EnableMsixTooling>
13+
<EnableMsixTooling>false</EnableMsixTooling>
14+
<WindowsPackageType>None</WindowsPackageType>
1415
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
1516
<!-- To use CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute: -->
1617
<LangVersion>preview</LangVersion>
1718
</PropertyGroup>
1819

19-
<ItemGroup>
20-
<AppxManifest Include="Package.appxmanifest">
21-
<SubType>Designer</SubType>
22-
</AppxManifest>
23-
</ItemGroup>
24-
2520
<ItemGroup>
2621
<Manifest Include="$(ApplicationManifest)" />
2722
</ItemGroup>
@@ -40,43 +35,12 @@
4035
</PackageReference>
4136
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.2.0" />
4237
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
43-
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1742" />
4438
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.250108002" />
4539
</ItemGroup>
4640

47-
<!--
48-
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
49-
Tools extension to be activated for this project even if the Windows App SDK Nuget
50-
package has not yet been restored.
51-
-->
52-
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
53-
<ProjectCapability Include="Msix" />
54-
</ItemGroup>
5541
<ItemGroup>
5642
<ProjectReference Include="..\CoderSdk\CoderSdk.csproj" />
5743
<ProjectReference Include="..\Vpn.Proto\Vpn.Proto.csproj" />
5844
<ProjectReference Include="..\Vpn\Vpn.csproj" />
5945
</ItemGroup>
60-
61-
<!--
62-
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
63-
Explorer "Package and Publish" context menu entry to be enabled for this project even if
64-
the Windows App SDK Nuget package has not yet been restored.
65-
-->
66-
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
67-
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
68-
</PropertyGroup>
69-
70-
<!-- Publish Properties -->
71-
<PropertyGroup>
72-
<!--
73-
This does not work in CI at the moment, so we need to set it to false
74-
Error: C:\Program Files\dotnet\sdk\9.0.102\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Publish.targets(400,5): error NETSDK1094: Unable to optimize assemblies for performance: a valid runtime package was not found. Either set the PublishReadyToRun property to false, or use a supported runtime identifier when publishing. When targeting .NET 6 or higher, make sure to restore packages with the PublishReadyToRun property set to true. [D:\a\coder-desktop-windows\coder-desktop-windows\App\App.csproj]
75-
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
76-
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
77-
-->
78-
<PublishReadyToRun>False</PublishReadyToRun>
79-
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
80-
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
81-
</PropertyGroup>
8246
</Project>

App/App.xaml.cs

+4-7
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ public partial class App : Application
1313
{
1414
private readonly IServiceProvider _services;
1515
private TrayWindow? _trayWindow;
16-
private readonly bool _handleClosedEvents = true;
1716

1817
public App()
1918
{
2019
var services = new ServiceCollection();
2120
services.AddSingleton<ICredentialManager, CredentialManager>();
2221
services.AddSingleton<IRpcController, RpcController>();
2322

23+
// TrayWindow pages and view models
2424
services.AddTransient<TrayWindowDisconnectedViewModel>();
2525
services.AddTransient<TrayWindowDisconnectedPage>();
2626
services.AddTransient<TrayWindowLoginRequiredViewModel>();
@@ -43,14 +43,11 @@ public App()
4343
protected override void OnLaunched(LaunchActivatedEventArgs args)
4444
{
4545
_trayWindow = _services.GetRequiredService<TrayWindow>();
46+
// Just hide the window rather than closing it.
4647
_trayWindow.Closed += (sender, args) =>
4748
{
48-
// TODO: wire up HandleClosedEvents properly
49-
if (_handleClosedEvents)
50-
{
51-
args.Handled = true;
52-
_trayWindow.AppWindow.Hide();
53-
}
49+
args.Handled = true;
50+
_trayWindow.AppWindow.Hide();
5451
};
5552
}
5653
}

App/Models/RpcModel.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Collections.Generic;
2+
using System.Linq;
3+
using Coder.Desktop.Vpn.Proto;
24

35
namespace Coder.Desktop.App.Models;
46

@@ -23,15 +25,18 @@ public class RpcModel
2325

2426
public VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Stopped;
2527

26-
public List<object> Agents { get; set; } = [];
28+
public List<Workspace> Workspaces { get; set; } = [];
29+
30+
public List<Agent> Agents { get; set; } = [];
2731

2832
public RpcModel Clone()
2933
{
3034
return new RpcModel
3135
{
3236
RpcLifecycle = RpcLifecycle,
3337
VpnLifecycle = VpnLifecycle,
34-
Agents = Agents,
38+
Workspaces = Workspaces.ToList(),
39+
Agents = Agents.ToList(),
3540
};
3641
}
3742
}

App/Properties/launchSettings.json

-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
{
22
"profiles": {
3-
"App (Package)": {
4-
"commandName": "MsixPackage"
5-
},
63
"App (Unpackaged)": {
74
"commandName": "Project"
85
}

App/Services/CredentialManager.cs

+7-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Threading.Tasks;
77
using Coder.Desktop.App.Models;
88
using Coder.Desktop.Vpn.Utilities;
9-
using CoderSdk;
109

1110
namespace Coder.Desktop.App.Services;
1211

@@ -64,18 +63,23 @@ public async Task SetCredentials(string coderUrl, string apiToken, CancellationT
6463
if (apiToken.Length != 33)
6564
throw new ArgumentOutOfRangeException(nameof(apiToken), "API token must be 33 characters long");
6665

66+
// TODO: this code seems to hang?
67+
/*
6768
try
6869
{
70+
var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
71+
cts.CancelAfter(TimeSpan.FromSeconds(5));
6972
var sdkClient = new CoderApiClient(uri);
7073
// TODO: we should probably perform a version check here too,
7174
// rather than letting the service do it on Start
72-
_ = await sdkClient.GetBuildInfo(ct);
73-
_ = await sdkClient.GetUser(User.Me, ct);
75+
_ = await sdkClient.GetBuildInfo(cts.Token);
76+
_ = await sdkClient.GetUser(User.Me, cts.Token);
7477
}
7578
catch (Exception e)
7679
{
7780
throw new InvalidOperationException("Could not connect to or verify Coder server", e);
7881
}
82+
*/
7983

8084
WriteCredentials(new RawCredentials
8185
{

App/Services/RpcController.cs

+52-4
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ public async Task Reconnect(CancellationToken ct = default)
9696
{
9797
state.RpcLifecycle = RpcLifecycle.Connecting;
9898
state.VpnLifecycle = VpnLifecycle.Stopped;
99+
state.Workspaces.Clear();
99100
state.Agents.Clear();
100101
});
101102

@@ -126,6 +127,7 @@ public async Task Reconnect(CancellationToken ct = default)
126127
{
127128
state.RpcLifecycle = RpcLifecycle.Disconnected;
128129
state.VpnLifecycle = VpnLifecycle.Stopped;
130+
state.Workspaces.Clear();
129131
state.Agents.Clear();
130132
});
131133
throw new RpcOperationException("Failed to reconnect to the RPC server", e);
@@ -134,10 +136,18 @@ public async Task Reconnect(CancellationToken ct = default)
134136
MutateState(state =>
135137
{
136138
state.RpcLifecycle = RpcLifecycle.Connected;
137-
// TODO: fetch current state
138-
state.VpnLifecycle = VpnLifecycle.Stopped;
139+
state.VpnLifecycle = VpnLifecycle.Stopping; // prevents clicking the toggle
140+
state.Workspaces.Clear();
139141
state.Agents.Clear();
140142
});
143+
144+
var statusReply = await _speaker.SendRequestAwaitReply(new ClientMessage
145+
{
146+
Status = new StatusRequest(),
147+
}, ct);
148+
if (statusReply.MsgCase != ServiceMessage.MsgOneofCase.Status)
149+
throw new InvalidOperationException($"Unexpected reply message type: {statusReply.MsgCase}");
150+
ApplyStatusUpdate(statusReply.Status);
141151
}
142152

143153
public async Task StartVpn(CancellationToken ct = default)
@@ -234,9 +244,40 @@ private async Task<IDisposable> AcquireOperationLockNowAsync()
234244
return locker;
235245
}
236246

247+
private void ApplyStatusUpdate(Status status)
248+
{
249+
MutateState(state =>
250+
{
251+
state.VpnLifecycle = status.Lifecycle switch
252+
{
253+
Status.Types.Lifecycle.Unknown => VpnLifecycle.Stopping, // disables the switch
254+
Status.Types.Lifecycle.Starting => VpnLifecycle.Starting,
255+
Status.Types.Lifecycle.Started => VpnLifecycle.Started,
256+
Status.Types.Lifecycle.Stopping => VpnLifecycle.Stopping,
257+
Status.Types.Lifecycle.Stopped => VpnLifecycle.Stopped,
258+
_ => VpnLifecycle.Stopped,
259+
};
260+
state.Workspaces.Clear();
261+
state.Workspaces.AddRange(status.PeerUpdate.UpsertedWorkspaces);
262+
state.Agents.Clear();
263+
state.Agents.AddRange(status.PeerUpdate.UpsertedAgents);
264+
});
265+
}
266+
237267
private void SpeakerOnReceive(ReplyableRpcMessage<ClientMessage, ServiceMessage> message)
238268
{
239-
// TODO: this
269+
switch (message.Message.MsgCase)
270+
{
271+
case ServiceMessage.MsgOneofCase.Status:
272+
ApplyStatusUpdate(message.Message.Status);
273+
break;
274+
case ServiceMessage.MsgOneofCase.Start:
275+
case ServiceMessage.MsgOneofCase.Stop:
276+
case ServiceMessage.MsgOneofCase.None:
277+
default:
278+
// TODO: log unexpected message
279+
break;
280+
}
240281
}
241282

242283
private async Task DisposeSpeaker()
@@ -251,7 +292,14 @@ private async Task DisposeSpeaker()
251292
private void SpeakerOnError(Exception e)
252293
{
253294
Debug.WriteLine($"Error: {e}");
254-
Reconnect(CancellationToken.None).Wait();
295+
try
296+
{
297+
Reconnect(CancellationToken.None).Wait();
298+
}
299+
catch
300+
{
301+
// best effort to immediately reconnect
302+
}
255303
}
256304

257305
private void AssertRpcConnected()

0 commit comments

Comments
 (0)