11using System . Runtime . InteropServices ;
22using Coder . Desktop . Vpn . Proto ;
3+ using Coder . Desktop . Vpn . Utilities ;
34using CoderSdk ;
45using Microsoft . Extensions . Logging ;
56using Microsoft . Extensions . Options ;
@@ -27,6 +28,10 @@ public class Manager : IManager
2728 private readonly IDownloader _downloader ;
2829 private readonly ILogger < Manager > _logger ;
2930 private readonly ITunnelSupervisor _tunnelSupervisor ;
31+
32+ // TunnelSupervisor already has protections against concurrent operations,
33+ // but all the other stuff before starting the tunnel does not.
34+ private readonly RaiiSemaphoreSlim _tunnelOperationLock = new ( 1 , 1 ) ;
3035 private SemVersion ? _lastServerVersion ;
3136 private StartRequest ? _lastStartRequest ;
3237
@@ -58,10 +63,18 @@ public async Task HandleClientRpcMessage(ReplyableRpcMessage<ServiceMessage, Cli
5863 {
5964 case ClientMessage . MsgOneofCase . Start :
6065 // TODO: these sub-methods should be managed by some Task list and cancelled/awaited on stop
61- await HandleClientMessageStart ( message , ct ) ;
66+ var startResponse = await HandleClientMessageStart ( message . Message , ct ) ;
67+ await message . SendReply ( new ServiceMessage
68+ {
69+ Start = startResponse ,
70+ } , ct ) ;
6271 break ;
6372 case ClientMessage . MsgOneofCase . Stop :
64- await HandleClientMessageStop ( message , ct ) ;
73+ var stopResponse = await HandleClientMessageStop ( message . Message , ct ) ;
74+ await message . SendReply ( new ServiceMessage
75+ {
76+ Stop = stopResponse ,
77+ } , ct ) ;
6578 break ;
6679 case ClientMessage . MsgOneofCase . None :
6780 default :
@@ -75,92 +88,105 @@ public async Task StopAsync(CancellationToken ct = default)
7588 await _tunnelSupervisor . StopAsync ( ct ) ;
7689 }
7790
78- private async Task HandleClientMessageStart ( ReplyableRpcMessage < ServiceMessage , ClientMessage > message ,
91+ private async ValueTask < StartResponse > HandleClientMessageStart ( ClientMessage message ,
7992 CancellationToken ct )
8093 {
81- try
94+ var opLock = await _tunnelOperationLock . LockAsync ( TimeSpan . FromMilliseconds ( 500 ) , ct ) ;
95+ if ( opLock == null )
8296 {
83- var serverVersion =
84- await CheckServerVersionAndCredentials ( message . Message . Start . CoderUrl , message . Message . Start . ApiToken ,
85- ct ) ;
86- if ( _tunnelSupervisor . IsRunning && _lastStartRequest != null &&
87- _lastStartRequest . Equals ( message . Message . Start ) && _lastServerVersion == serverVersion )
97+ _logger . LogWarning ( "ClientMessage.Start: Tunnel operation lock timed out" ) ;
98+ return new StartResponse
8899 {
89- // The client is requesting to start an identical tunnel while
90- // we're already running it.
91- _logger . LogInformation ( "ClientMessage.Start: Ignoring duplicate start request" ) ;
92- await message . SendReply ( new ServiceMessage
100+ Success = false ,
101+ ErrorMessage = "Could not acquire tunnel operation lock, another operation is in progress" ,
102+ } ;
103+ }
104+
105+ using ( opLock )
106+ {
107+ try
108+ {
109+ var serverVersion =
110+ await CheckServerVersionAndCredentials ( message . Start . CoderUrl , message . Start . ApiToken ,
111+ ct ) ;
112+ if ( _tunnelSupervisor . IsRunning && _lastStartRequest != null &&
113+ _lastStartRequest . Equals ( message . Start ) && _lastServerVersion == serverVersion )
93114 {
94- Start = new StartResponse
115+ // The client is requesting to start an identical tunnel while
116+ // we're already running it.
117+ _logger . LogInformation ( "ClientMessage.Start: Ignoring duplicate start request" ) ;
118+ return new StartResponse
95119 {
96120 Success = true ,
97- } ,
98- } , ct ) ;
99- return ;
100- }
101-
102- _lastStartRequest = message . Message . Start ;
103- _lastServerVersion = serverVersion ;
121+ } ;
122+ }
104123
105- // Stop the tunnel if it's running so we don't have to worry about
106- // permissions issues when replacing the binary.
107- await _tunnelSupervisor . StopAsync ( ct ) ;
108- await DownloadTunnelBinaryAsync ( message . Message . Start . CoderUrl , serverVersion , ct ) ;
109- await _tunnelSupervisor . StartAsync ( _config . TunnelBinaryPath , HandleTunnelRpcMessage , HandleTunnelRpcError ,
110- ct ) ;
124+ _lastStartRequest = message . Start ;
125+ _lastServerVersion = serverVersion ;
111126
112- var reply = await _tunnelSupervisor . SendRequestAwaitReply ( new ManagerMessage
113- {
114- Start = message . Message . Start ,
115- } , ct ) ;
116- if ( reply . MsgCase != TunnelMessage . MsgOneofCase . Start )
117- throw new InvalidOperationException ( "Tunnel did not reply with a Start response" ) ;
127+ // TODO: each section of this operation needs a timeout
128+ // Stop the tunnel if it's running so we don't have to worry about
129+ // permissions issues when replacing the binary.
130+ await _tunnelSupervisor . StopAsync ( ct ) ;
131+ await DownloadTunnelBinaryAsync ( message . Start . CoderUrl , serverVersion , ct ) ;
132+ await _tunnelSupervisor . StartAsync ( _config . TunnelBinaryPath , HandleTunnelRpcMessage ,
133+ HandleTunnelRpcError ,
134+ ct ) ;
118135
119- await message . SendReply ( new ServiceMessage
120- {
121- Start = reply . Start ,
122- } , ct ) ;
123- }
124- catch ( Exception e )
125- {
126- _logger . LogWarning ( e , "ClientMessage.Start: Failed to start VPN client" ) ;
127- await message . SendReply ( new ServiceMessage
136+ var reply = await _tunnelSupervisor . SendRequestAwaitReply ( new ManagerMessage
137+ {
138+ Start = message . Start ,
139+ } , ct ) ;
140+ if ( reply . MsgCase != TunnelMessage . MsgOneofCase . Start )
141+ throw new InvalidOperationException ( "Tunnel did not reply with a Start response" ) ;
142+ return reply . Start ;
143+ }
144+ catch ( Exception e )
128145 {
129- Start = new StartResponse
146+ _logger . LogWarning ( e , "ClientMessage.Start: Failed to start VPN client" ) ;
147+ return new StartResponse
130148 {
131149 Success = false ,
132150 ErrorMessage = e . ToString ( ) ,
133- } ,
134- } , ct ) ;
151+ } ;
152+ }
135153 }
136154 }
137155
138- private async Task HandleClientMessageStop ( ReplyableRpcMessage < ServiceMessage , ClientMessage > message ,
156+ private async ValueTask < StopResponse > HandleClientMessageStop ( ClientMessage message ,
139157 CancellationToken ct )
140158 {
141- try
159+ var opLock = await _tunnelOperationLock . LockAsync ( TimeSpan . FromMilliseconds ( 500 ) , ct ) ;
160+ if ( opLock == null )
142161 {
143- // This will handle sending the Stop message to the tunnel for us.
144- await _tunnelSupervisor . StopAsync ( ct ) ;
145- await message . SendReply ( new ServiceMessage
162+ _logger . LogWarning ( "ClientMessage.Stop: Tunnel operation lock timed out" ) ;
163+ return new StopResponse
146164 {
147- Stop = new StopResponse
148- {
149- Success = true ,
150- } ,
151- } , ct ) ;
165+ Success = false ,
166+ ErrorMessage = "Could not acquire tunnel operation lock, another operation is in progress" ,
167+ } ;
152168 }
153- catch ( Exception e )
169+
170+ using ( opLock )
154171 {
155- _logger . LogWarning ( e , "ClientMessage.Stop: Failed to stop VPN client" ) ;
156- await message . SendReply ( new ServiceMessage
172+ try
157173 {
158- Stop = new StopResponse
174+ // This will handle sending the Stop message to the tunnel for us.
175+ await _tunnelSupervisor . StopAsync ( ct ) ;
176+ return new StopResponse
177+ {
178+ Success = true ,
179+ } ;
180+ }
181+ catch ( Exception e )
182+ {
183+ _logger . LogWarning ( e , "ClientMessage.Stop: Failed to stop VPN client" ) ;
184+ return new StopResponse
159185 {
160186 Success = false ,
161- ErrorMessage = e . Message ,
162- } ,
163- } , ct ) ;
187+ ErrorMessage = e . ToString ( ) ,
188+ } ;
189+ }
164190 }
165191 }
166192
0 commit comments