Skip to content

Commit d094b7b

Browse files
committed
feat(devSrv): Make VS also use csproj.user file for dev-server port config/discovery
1 parent 6e6b701 commit d094b7b

File tree

3 files changed

+191
-153
lines changed

3 files changed

+191
-153
lines changed

src/Uno.UI.RemoteControl.VS/EntryPoint.ActiveProfileSync.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ private async Task WriteProjectUserSettingsAsync(string targetFramework)
419419

420420
if (hierarchy is IVsBuildPropertyStorage propertyStorage)
421421
{
422-
WriteUserProperty(propertyStorage, UnoSelectedTargetFrameworkProperty, targetFramework);
422+
propertyStorage.SetUserProperty(UnoSelectedTargetFrameworkProperty, targetFramework);
423423
}
424424
else
425425
{
@@ -431,14 +431,4 @@ private async Task WriteProjectUserSettingsAsync(string targetFramework)
431431
_debugAction?.Invoke("Could not write .user file (1)");
432432
}
433433
}
434-
435-
private static void WriteUserProperty(IVsBuildPropertyStorage propertyStorage, string propertyName, string propertyValue)
436-
{
437-
propertyStorage.SetPropertyValue(
438-
propertyName, // Property name
439-
null, // Configuration name, null applies to all configurations
440-
(uint)_PersistStorageType.PST_USER_FILE, // Specifies that this is a user-specific property
441-
propertyValue // Property value
442-
);
443-
}
444434
}

src/Uno.UI.RemoteControl.VS/EntryPoint.cs

Lines changed: 54 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ public partial class EntryPoint : IDisposable
3737
{
3838
private const string UnoPlatformOutputPane = "Uno Platform";
3939
private const string RemoteControlServerPortProperty = "UnoRemoteControlPort";
40-
private const string UnoRemoteControlConfigCookieProperty = "UnoRemoteControlConfigCookie";
4140
private const string UnoVSExtensionLoadedProperty = "_UnoVSExtensionLoaded";
4241

4342
private readonly CancellationTokenSource _ct = new();
@@ -51,12 +50,10 @@ public partial class EntryPoint : IDisposable
5150
private Action<string>? _warningAction;
5251
private Action<string>? _errorAction;
5352
private int _msBuildLogLevel;
54-
private System.Diagnostics.Process? _process;
55-
private SemaphoreSlim _processGate = new(1);
53+
private (System.Diagnostics.Process process, int port)? _devServer;
54+
private SemaphoreSlim _devServerGate = new(1);
5655
private IServiceProvider? _visualStudioServiceProvider;
5756

58-
private int _remoteControlServerPort;
59-
private string? _remoteControlConfigCookie;
6057
private bool _closing;
6158
private bool _isDisposed;
6259
private IdeChannelClient? _ideChannelClient;
@@ -139,16 +136,6 @@ private Task<Dictionary<string, string>> OnProvideGlobalPropertiesAsync()
139136
[UnoVSExtensionLoadedProperty] = "true"
140137
};
141138

142-
if (_remoteControlServerPort is not 0)
143-
{
144-
properties.Add(RemoteControlServerPortProperty, _remoteControlServerPort.ToString(CultureInfo.InvariantCulture));
145-
}
146-
147-
if (_remoteControlConfigCookie is not null)
148-
{
149-
properties.Add(UnoRemoteControlConfigCookieProperty, _remoteControlConfigCookie);
150-
}
151-
152139
return Task.FromResult(properties);
153140
}
154141

@@ -173,8 +160,8 @@ private void SetupOutputWindow()
173160
if (owPane == null)
174161
{
175162
owPane = ow
176-
.OutputWindowPanes
177-
.Add(UnoPlatformOutputPane);
163+
.OutputWindowPanes
164+
.Add(UnoPlatformOutputPane);
178165
}
179166

180167
_debugAction = s =>
@@ -252,24 +239,24 @@ private void SolutionEvents_BeforeClosing()
252239
_dte.Events.SolutionEvents.BeforeClosing -= _closeHandler;
253240

254241
_closing = true;
255-
if (_process is not null)
242+
if (_devServer is { process: var devServer })
256243
{
257244
try
258245
{
259-
_debugAction?.Invoke($"Terminating Remote Control server (pid: {_process.Id})");
260-
_process.Kill();
261-
_debugAction?.Invoke($"Terminated Remote Control server (pid: {_process.Id})");
246+
_debugAction?.Invoke($"Terminating Remote Control server (pid: {devServer.Id})");
247+
devServer.Kill();
248+
_debugAction?.Invoke($"Terminated Remote Control server (pid: {devServer.Id})");
262249

263250
_ideChannelClient?.Dispose();
264251
_ideChannelClient = null;
265252
}
266253
catch (Exception e)
267254
{
268-
_debugAction?.Invoke($"Failed to terminate Remote Control server (pid: {_process.Id}): {e}");
255+
_debugAction?.Invoke($"Failed to terminate Remote Control server (pid: {devServer.Id}): {e}");
269256
}
270257
finally
271258
{
272-
_process = null;
259+
_devServer = null;
273260

274261
// Invoke Dispose to make sure other event handlers are detached
275262
Dispose();
@@ -301,27 +288,45 @@ private async Task EnsureServerAsync()
301288
{
302289
_debugAction?.Invoke($"Starting server (tid:{Environment.CurrentManagedThreadId})");
303290

304-
if (_process is { HasExited: false })
291+
var persistedPortsStr = await _dte.GetProjectUserSettingsAsync(_asyncPackage, RemoteControlServerPortProperty, ProjectAttribute.Application);
292+
var persistedPorts = persistedPortsStr
293+
.Distinct(StringComparer.OrdinalIgnoreCase)
294+
.Select(str => int.TryParse(str, out var p) ? p : -1)
295+
.ToArray();
296+
var persistedPort = persistedPorts.FirstOrDefault(p => p > 0);
297+
298+
var port = _devServer?.port ?? persistedPort;
299+
var portMissConfigured = persistedPorts is { Length: 0 or > 1 } || port != persistedPort;
300+
301+
if (_devServer is { process.HasExited: false })
305302
{
306-
_debugAction?.Invoke("Server already running");
303+
if (portMissConfigured)
304+
{
305+
_debugAction?.Invoke($"Server already running on port {_devServer?.port}, but port is not configured properly on all projects. Updating it ...");
306+
307+
// The dev-server is already running, but at least one project is not configured properly
308+
// (This can happen when a project is being added to the solution while opened - Reminder: This EnsureServerAsync is invoked each time a project is built)
309+
// We make sure to set the current port for **all** projects.
310+
await _dte.SetProjectUserSettingsAsync(_asyncPackage, RemoteControlServerPortProperty, port.ToString(CultureInfo.InvariantCulture), ProjectAttribute.Application);
311+
}
312+
else
313+
{
314+
_debugAction?.Invoke($"Server already running on port {_devServer?.port}");
315+
}
316+
307317
return;
308318
}
309319

310-
await _processGate.WaitAsync();
320+
await _devServerGate.WaitAsync();
311321
try
312322
{
313-
314-
if (EnsureTcpPort(ref _remoteControlServerPort))
323+
if (EnsureTcpPort(ref port) || portMissConfigured)
315324
{
316-
// Update the cookie file, so a rebuild will be triggered
317-
_remoteControlConfigCookie ??= Path.GetTempFileName();
318-
File.WriteAllText(_remoteControlConfigCookie, _remoteControlServerPort.ToString(CultureInfo.InvariantCulture));
319-
320-
// Push the new port to the project using global properties
321-
_ = _globalPropertiesChanged();
325+
// The port has changed, or all application projects does not have the same port number (or is not configured), we update port in *all* user files
326+
await _dte.SetProjectUserSettingsAsync(_asyncPackage, RemoteControlServerPortProperty, port.ToString(CultureInfo.InvariantCulture), ProjectAttribute.Application);
322327
}
323328

324-
_debugAction?.Invoke($"Using available port {_remoteControlServerPort}");
329+
_debugAction?.Invoke($"Using available port {port}");
325330

326331
var version = GetDotnetMajorVersion();
327332
if (version < 7)
@@ -332,7 +337,7 @@ private async Task EnsureServerAsync()
332337
var pipeGuid = Guid.NewGuid();
333338

334339
var hostBinPath = Path.Combine(_toolsPath, "host", $"net{version}.0", "Uno.UI.RemoteControl.Host.dll");
335-
var arguments = $"\"{hostBinPath}\" --httpPort {_remoteControlServerPort} --ppid {System.Diagnostics.Process.GetCurrentProcess().Id} --ideChannel \"{pipeGuid}\" --solution \"{_dte.Solution.FullName}\"";
340+
var arguments = $"\"{hostBinPath}\" --httpPort {port} --ppid {System.Diagnostics.Process.GetCurrentProcess().Id} --ideChannel \"{pipeGuid}\" --solution \"{_dte.Solution.FullName}\"";
336341
var pi = new ProcessStartInfo("dotnet", arguments)
337342
{
338343
UseShellExecute = false,
@@ -345,55 +350,29 @@ private async Task EnsureServerAsync()
345350
RedirectStandardError = true
346351
};
347352

348-
_process = new System.Diagnostics.Process { EnableRaisingEvents = true };
353+
var devServer = new System.Diagnostics.Process { EnableRaisingEvents = true };
354+
_devServer = (devServer, port);
349355

350356
// hookup the event handlers to capture the data that is received
351-
_process.OutputDataReceived += (sender, args) => _debugAction?.Invoke(args.Data);
352-
_process.ErrorDataReceived += (sender, args) => _errorAction?.Invoke(args.Data);
357+
devServer.OutputDataReceived += (sender, args) => _debugAction?.Invoke(args.Data);
358+
devServer.ErrorDataReceived += (sender, args) => _errorAction?.Invoke(args.Data);
353359

354-
_process.StartInfo = pi;
355-
_process.Exited += (sender, args) => _ = RestartAsync();
360+
devServer.StartInfo = pi;
361+
devServer.Exited += (sender, args) => _ = RestartAsync();
356362

357-
if (_process.Start())
363+
if (devServer.Start())
358364
{
359365
// start our event pumps
360-
_process.BeginOutputReadLine();
361-
_process.BeginErrorReadLine();
366+
devServer.BeginOutputReadLine();
367+
devServer.BeginErrorReadLine();
362368

363369
_ideChannelClient = new IdeChannelClient(pipeGuid, new Logger(this));
364370
_ideChannelClient.OnMessageReceived += OnMessageReceivedAsync;
365371
_ideChannelClient.ConnectToHost();
366-
367-
// Set the port to the projects
368-
// This LEGACY as port should be set through the global properties (cf. OnProvideGlobalPropertiesAsync)
369-
var portString = _remoteControlServerPort.ToString(CultureInfo.InvariantCulture);
370-
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
371-
var projects = (await _dte.GetProjectsAsync()).ToArray(); // EnumerateProjects must be called on the UI thread.
372-
await TaskScheduler.Default;
373-
foreach (var project in projects)
374-
{
375-
var filename = string.Empty;
376-
try
377-
{
378-
filename = project.FileName;
379-
}
380-
catch (Exception ex)
381-
{
382-
_debugAction?.Invoke($"Exception on retrieving {project.UniqueName} details. Err: {ex}.");
383-
_warningAction?.Invoke($"Cannot read {project.UniqueName} project details (It may be unloaded).");
384-
}
385-
if (string.IsNullOrWhiteSpace(filename) == false
386-
&& GetMsbuildProject(filename) is Microsoft.Build.Evaluation.Project msbProject
387-
&& IsApplication(msbProject))
388-
{
389-
SetGlobalProperty(filename, RemoteControlServerPortProperty, portString);
390-
}
391-
}
392372
}
393373
else
394374
{
395-
_process = null;
396-
_remoteControlServerPort = 0;
375+
_devServer = null;
397376
}
398377
}
399378
catch (Exception e)
@@ -402,33 +381,23 @@ private async Task EnsureServerAsync()
402381
}
403382
finally
404383
{
405-
_processGate.Release();
384+
_devServerGate.Release();
406385
}
407386

408387
async Task RestartAsync()
409388
{
410389
if (_closing || _ct.IsCancellationRequested)
411390
{
412-
if (_remoteControlConfigCookie is not null)
413-
{
414-
File.WriteAllText(_remoteControlConfigCookie, "--closed--"); // Make sure VS will re-build on next start
415-
}
416-
417-
_debugAction?.Invoke($"Remote Control server exited ({_process?.ExitCode}) and won't be restarted as solution is closing.");
391+
_debugAction?.Invoke($"Remote Control server exited ({_devServer?.process.ExitCode}) and won't be restarted as solution is closing.");
418392
return;
419393
}
420394

421-
_debugAction?.Invoke($"Remote Control server exited ({_process?.ExitCode}). It will restart in 5sec.");
395+
_debugAction?.Invoke($"Remote Control server exited ({_devServer?.process.ExitCode}). It will restart in 5sec.");
422396

423397
await Task.Delay(5000, _ct.Token);
424398

425399
if (_closing || _ct.IsCancellationRequested)
426400
{
427-
if (_remoteControlConfigCookie is not null)
428-
{
429-
File.WriteAllText(_remoteControlConfigCookie, "--closed--"); // Make sure VS will re-build on next start
430-
}
431-
432401
_debugAction?.Invoke($"Remote Control server will not be restarted as solution is closing.");
433402
return;
434403
}
@@ -653,51 +622,6 @@ private bool EnsureTcpPort(ref int port)
653622
return true; // HasChanged
654623
}
655624

656-
public void SetGlobalProperty(string projectFullName, string propertyName, string propertyValue)
657-
{
658-
var msbuildProject = GetMsbuildProject(projectFullName);
659-
if (msbuildProject == null)
660-
{
661-
_debugAction?.Invoke($"Failed to find project {projectFullName}, cannot provide listen port to the app.");
662-
}
663-
else
664-
{
665-
SetGlobalProperty(msbuildProject, propertyName, propertyValue);
666-
}
667-
}
668-
669-
private static Microsoft.Build.Evaluation.Project? GetMsbuildProject(string projectFullName)
670-
=> ProjectCollection.GlobalProjectCollection.GetLoadedProjects(projectFullName).FirstOrDefault();
671-
672-
public void SetGlobalProperties(string projectFullName, IDictionary<string, string> properties)
673-
{
674-
var msbuildProject = ProjectCollection.GlobalProjectCollection.GetLoadedProjects(projectFullName).FirstOrDefault();
675-
if (msbuildProject == null)
676-
{
677-
_debugAction?.Invoke($"Failed to find project {projectFullName}, cannot provide listen port to the app.");
678-
}
679-
else
680-
{
681-
foreach (var property in properties)
682-
{
683-
SetGlobalProperty(msbuildProject, property.Key, property.Value);
684-
}
685-
}
686-
}
687-
688-
private void SetGlobalProperty(Microsoft.Build.Evaluation.Project msbuildProject, string propertyName, string propertyValue)
689-
{
690-
msbuildProject.SetGlobalProperty(propertyName, propertyValue);
691-
692-
}
693-
694-
private bool IsApplication(Microsoft.Build.Evaluation.Project project)
695-
{
696-
var outputType = project.GetPropertyValue("OutputType");
697-
return outputType is not null &&
698-
(outputType.Equals("Exe", StringComparison.OrdinalIgnoreCase) || outputType.Equals("WinExe", StringComparison.OrdinalIgnoreCase));
699-
}
700-
701625
public void Dispose()
702626
{
703627
if (_isDisposed)

0 commit comments

Comments
 (0)