Skip to content

Commit c2791f5

Browse files
authored
feat: add agent status to tray app (#21)
Closes #5
1 parent 641f1bc commit c2791f5

23 files changed

+724
-446
lines changed

.github/workflows/ci.yaml

+7-7
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ jobs:
5151
cache-dependency-path: '**/packages.lock.json'
5252
- name: dotnet restore
5353
run: dotnet restore --locked-mode
54-
- name: dotnet publish
55-
run: dotnet publish --no-restore --configuration Release --output .\publish
56-
- name: Upload artifact
57-
uses: actions/upload-artifact@v4
58-
with:
59-
name: publish
60-
path: .\publish\
54+
#- name: dotnet publish
55+
# run: dotnet publish --no-restore --configuration Release --output .\publish
56+
#- name: Upload artifact
57+
# uses: actions/upload-artifact@v4
58+
# with:
59+
# name: publish
60+
# path: .\publish\

.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

+2-7
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ namespace Coder.Desktop.App;
1212
public partial class App : Application
1313
{
1414
private readonly IServiceProvider _services;
15-
private readonly bool _handleClosedEvents = true;
1615

1716
public App()
1817
{
@@ -49,12 +48,8 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
4948
var trayWindow = _services.GetRequiredService<TrayWindow>();
5049
trayWindow.Closed += (sender, args) =>
5150
{
52-
// TODO: wire up HandleClosedEvents properly
53-
if (_handleClosedEvents)
54-
{
55-
args.Handled = true;
56-
trayWindow.AppWindow.Hide();
57-
}
51+
args.Handled = true;
52+
trayWindow.AppWindow.Hide();
5853
};
5954
}
6055
}

App/Converters/VpnLifecycleToBoolConverter.cs

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
namespace Coder.Desktop.App.Converters;
88

9+
[DependencyProperty<bool>("Unknown", DefaultValue = false)]
910
[DependencyProperty<bool>("Starting", DefaultValue = false)]
1011
[DependencyProperty<bool>("Started", DefaultValue = false)]
1112
[DependencyProperty<bool>("Stopping", DefaultValue = false)]
@@ -18,6 +19,7 @@ public object Convert(object value, Type targetType, object parameter, string la
1819

1920
return lifecycle switch
2021
{
22+
VpnLifecycle.Unknown => Unknown,
2123
VpnLifecycle.Starting => Starting,
2224
VpnLifecycle.Started => Started,
2325
VpnLifecycle.Stopping => Stopping,

App/Models/RpcModel.cs

+9-3
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

@@ -11,6 +13,7 @@ public enum RpcLifecycle
1113

1214
public enum VpnLifecycle
1315
{
16+
Unknown,
1417
Stopped,
1518
Starting,
1619
Started,
@@ -21,17 +24,20 @@ public class RpcModel
2124
{
2225
public RpcLifecycle RpcLifecycle { get; set; } = RpcLifecycle.Disconnected;
2326

24-
public VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Stopped;
27+
public VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown;
2528

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

2833
public RpcModel Clone()
2934
{
3035
return new RpcModel
3136
{
3237
RpcLifecycle = RpcLifecycle,
3338
VpnLifecycle = VpnLifecycle,
34-
Agents = Agents,
39+
Workspaces = Workspaces.ToList(),
40+
Agents = Agents.ToList(),
3541
};
3642
}
3743
}

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,14 @@ public async Task SetCredentials(string coderUrl, string apiToken, CancellationT
6666

6767
try
6868
{
69+
var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
70+
cts.CancelAfter(TimeSpan.FromSeconds(15));
6971
var sdkClient = new CoderApiClient(uri);
7072
sdkClient.SetSessionToken(apiToken);
7173
// TODO: we should probably perform a version check here too,
7274
// rather than letting the service do it on Start
73-
_ = await sdkClient.GetBuildInfo(ct);
74-
_ = await sdkClient.GetUser(User.Me, ct);
75+
_ = await sdkClient.GetBuildInfo(cts.Token);
76+
_ = await sdkClient.GetUser(User.Me, cts.Token);
7577
}
7678
catch (Exception e)
7779
{

App/Services/RpcController.cs

+53-5
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

@@ -125,7 +126,8 @@ public async Task Reconnect(CancellationToken ct = default)
125126
MutateState(state =>
126127
{
127128
state.RpcLifecycle = RpcLifecycle.Disconnected;
128-
state.VpnLifecycle = VpnLifecycle.Stopped;
129+
state.VpnLifecycle = VpnLifecycle.Unknown;
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.Unknown;
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.Unknown,
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)