1
1
using System . Runtime . InteropServices ;
2
2
using Coder . Desktop . Vpn . Proto ;
3
+ using Coder . Desktop . Vpn . Utilities ;
3
4
using CoderSdk ;
4
5
using Microsoft . Extensions . Logging ;
5
6
using Microsoft . Extensions . Options ;
@@ -27,6 +28,10 @@ public class Manager : IManager
27
28
private readonly IDownloader _downloader ;
28
29
private readonly ILogger < Manager > _logger ;
29
30
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 ) ;
30
35
private SemVersion ? _lastServerVersion ;
31
36
private StartRequest ? _lastStartRequest ;
32
37
@@ -58,10 +63,18 @@ public async Task HandleClientRpcMessage(ReplyableRpcMessage<ServiceMessage, Cli
58
63
{
59
64
case ClientMessage . MsgOneofCase . Start :
60
65
// 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 ) ;
62
71
break ;
63
72
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 ) ;
65
78
break ;
66
79
case ClientMessage . MsgOneofCase . None :
67
80
default :
@@ -75,92 +88,105 @@ public async Task StopAsync(CancellationToken ct = default)
75
88
await _tunnelSupervisor . StopAsync ( ct ) ;
76
89
}
77
90
78
- private async Task HandleClientMessageStart ( ReplyableRpcMessage < ServiceMessage , ClientMessage > message ,
91
+ private async ValueTask < StartResponse > HandleClientMessageStart ( ClientMessage message ,
79
92
CancellationToken ct )
80
93
{
81
- try
94
+ var opLock = await _tunnelOperationLock . LockAsync ( TimeSpan . FromMilliseconds ( 500 ) , ct ) ;
95
+ if ( opLock == null )
82
96
{
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
88
99
{
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 )
93
114
{
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
95
119
{
96
120
Success = true ,
97
- } ,
98
- } , ct ) ;
99
- return ;
100
- }
101
-
102
- _lastStartRequest = message . Message . Start ;
103
- _lastServerVersion = serverVersion ;
121
+ } ;
122
+ }
104
123
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 ;
111
126
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 ) ;
118
135
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 )
128
145
{
129
- Start = new StartResponse
146
+ _logger . LogWarning ( e , "ClientMessage.Start: Failed to start VPN client" ) ;
147
+ return new StartResponse
130
148
{
131
149
Success = false ,
132
150
ErrorMessage = e . ToString ( ) ,
133
- } ,
134
- } , ct ) ;
151
+ } ;
152
+ }
135
153
}
136
154
}
137
155
138
- private async Task HandleClientMessageStop ( ReplyableRpcMessage < ServiceMessage , ClientMessage > message ,
156
+ private async ValueTask < StopResponse > HandleClientMessageStop ( ClientMessage message ,
139
157
CancellationToken ct )
140
158
{
141
- try
159
+ var opLock = await _tunnelOperationLock . LockAsync ( TimeSpan . FromMilliseconds ( 500 ) , ct ) ;
160
+ if ( opLock == null )
142
161
{
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
146
164
{
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
+ } ;
152
168
}
153
- catch ( Exception e )
169
+
170
+ using ( opLock )
154
171
{
155
- _logger . LogWarning ( e , "ClientMessage.Stop: Failed to stop VPN client" ) ;
156
- await message . SendReply ( new ServiceMessage
172
+ try
157
173
{
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
159
185
{
160
186
Success = false ,
161
- ErrorMessage = e . Message ,
162
- } ,
163
- } , ct ) ;
187
+ ErrorMessage = e . ToString ( ) ,
188
+ } ;
189
+ }
164
190
}
165
191
}
166
192
0 commit comments