public async Task ProjectReferences_Graph()
        {
            var graph = new TestProjectGraph(_tempDir);

            graph.OnCreate(p => p.WithTargetFrameworks("net45"));
            var matches = Regex.Matches(@"
            A->B B->C C->D D->E
                 B->E
            A->F F->G G->E
                 F->E
            W->U
            Y->Z
            Y->B
            Y->F",
                                        @"(\w)->(\w)");

            Assert.Equal(13, matches.Count);
            foreach (Match m in matches)
            {
                var target = graph.GetOrCreate(m.Groups[2].Value);
                graph.GetOrCreate(m.Groups[1].Value).WithProjectReference(target);
            }

            var a = graph.Find("A");

            Assert.NotNull(a);

            a !.WithProjectReference(graph.Find("W"), watch: false);

            var output         = new OutputSink();
            var filesetFactory = new MsBuildFileSetFactory(_reporter, DotNetWatchOptions.Default, graph.GetOrCreate("A").Path, output, trace: true);

            var fileset = await GetFileSet(filesetFactory);

            _reporter.Output(string.Join(
                                 Environment.NewLine,
                                 output.Current.Lines.Select(l => "Sink output: " + l)));

            var includedProjects = new[] { "A", "B", "C", "D", "E", "F", "G" };

            AssertEx.EqualFileList(
                _tempDir.Root,
                includedProjects
                .Select(p => $"{p}/{p}.csproj"),
                fileset
                );

            // ensure unreachable projects exist but where not included
            Assert.NotNull(graph.Find("W"));
            Assert.NotNull(graph.Find("U"));
            Assert.NotNull(graph.Find("Y"));
            Assert.NotNull(graph.Find("Z"));

            // ensure each project is only visited once for collecting watch items
            Assert.All(includedProjects,
                       projectName =>
                       Assert.Single(output.Current.Lines,
                                     line => line.Contains($"Collecting watch items from '{projectName}'"))
                       );
        }
 private async Task <IFileSet> GetFileSet(MsBuildFileSetFactory filesetFactory)
 {
     _tempDir.Create();
     return(await filesetFactory
            .CreateAsync(CancellationToken.None)
            .TimeoutAfter(TimeSpan.FromSeconds(30)));
 }
示例#3
0
        private async Task <IFileSet> GetFileSet(MsBuildFileSetFactory filesetFactory)
        {
            _tempDir.Create();
            var createTask = filesetFactory.CreateAsync(CancellationToken.None);
            var finished   = await Task.WhenAny(createTask, Task.Delay(TimeSpan.FromSeconds(10)));

            Assert.Same(createTask, finished);
            Assert.NotNull(createTask.Result);
            return(createTask.Result);
        }
示例#4
0
        private async Task <int> ListFilesAsync(
            IReporter reporter,
            string project,
            CancellationToken cancellationToken)
        {
            // TODO multiple projects should be easy enough to add here
            string projectFile;

            try
            {
                projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, project);
            }
            catch (FileNotFoundException ex)
            {
                reporter.Error(ex.Message);
                return(1);
            }

            var fileSetFactory = new MsBuildFileSetFactory(reporter,
                                                           DotNetWatchOptions.Default,
                                                           projectFile,
                                                           waitOnError: false,
                                                           trace: false);
            var files = await fileSetFactory.CreateAsync(cancellationToken);

            if (files == null)
            {
                return(1);
            }

            foreach (var group in files.GroupBy(g => g.FileKind).OrderBy(g => g.Key))
            {
                if (group.Key == FileKind.StaticFile)
                {
                    _console.Out.WriteLine("::: Watch Action: Refresh browser :::");
                }

                foreach (var file in group)
                {
                    if (file.FileKind == FileKind.StaticFile)
                    {
                        _console.Out.WriteLine($"{file.FilePath} ~/{file.StaticWebAssetPath}");
                    }
                    else
                    {
                        _console.Out.WriteLine(file.FilePath);
                    }
                }
            }

            return(0);
        }
示例#5
0
        private async Task <int> MainInternalAsync(
            IReporter reporter,
            string project,
            IReadOnlyCollection <string> args,
            CancellationToken cancellationToken)
        {
            // TODO multiple projects should be easy enough to add here
            string projectFile;

            try
            {
                projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, project);
            }
            catch (FileNotFoundException ex)
            {
                reporter.Error(ex.Message);
                return(1);
            }

            var watchOptions = DotNetWatchOptions.Default;

            var fileSetFactory = new MsBuildFileSetFactory(reporter,
                                                           watchOptions,
                                                           projectFile,
                                                           waitOnError: true,
                                                           trace: false);
            var processInfo = new ProcessSpec
            {
                Executable           = new Muxer().MuxerPath,
                WorkingDirectory     = Path.GetDirectoryName(projectFile),
                Arguments            = args,
                EnvironmentVariables =
                {
                    ["DOTNET_WATCH"] = "1"
                },
            };

            if (CommandLineOptions.IsPollingEnabled)
            {
                _reporter.Output("Polling file watcher is enabled");
            }

            await using var watcher = new DotNetWatcher(reporter, fileSetFactory, watchOptions);
            await watcher.WatchAsync(processInfo, cancellationToken);

            return(0);
        }
        public async Task ProjectReferences_Graph()
        {
            // A->B,F,W(Watch=False)
            // B->C,E
            // C->D
            // D->E
            // F->E,G
            // G->E
            // W->U
            // Y->B,F,Z
            var testDirectory = _testAssets.CopyTestAsset("ProjectReferences_Graph")
                                .WithSource()
                                .Path;
            var projectA = Path.Combine(testDirectory, "A", "A.csproj");

            var output         = new OutputSink();
            var options        = GetWatchOptions();
            var filesetFactory = new MsBuildFileSetFactory(options, _reporter, projectA, output, waitOnError: false, trace: true);

            var fileset = await GetFileSet(filesetFactory);

            Assert.NotNull(fileset);

            _reporter.Output(string.Join(
                                 Environment.NewLine,
                                 output.Current.Lines.Select(l => "Sink output: " + l)));

            var includedProjects = new[] { "A", "B", "C", "D", "E", "F", "G" };

            AssertEx.EqualFileList(
                testDirectory,
                includedProjects
                .Select(p => $"{p}/{p}.csproj"),
                fileset
                );

            // ensure each project is only visited once for collecting watch items
            Assert.All(includedProjects,
                       projectName =>
                       Assert.Single(output.Current.Lines,
                                     line => line.Contains($"Collecting watch items from '{projectName}'"))
                       );
        }
示例#7
0
        private async Task <int> ListFilesAsync(
            IReporter reporter,
            string project,
            CancellationToken cancellationToken)
        {
            // TODO multiple projects should be easy enough to add here
            string projectFile;

            try
            {
                projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, project);
            }
            catch (FileNotFoundException ex)
            {
                reporter.Error(ex.Message);
                return(1);
            }

            var fileSetFactory = new MsBuildFileSetFactory(
                reporter,
                DotNetWatchOptions.Default,
                projectFile,
                waitOnError: false,
                trace: false);
            var files = await fileSetFactory.CreateAsync(cancellationToken);

            if (files == null)
            {
                return(1);
            }

            foreach (var file in files)
            {
                _console.Out.WriteLine(file.FilePath);
            }

            return(0);
        }
示例#8
0
        private void LaunchService(Application application, Service service)
        {
            var serviceDescription = service.Description;
            var processInfo        = new ProcessInfo(new Task[service.Description.Replicas]);
            var serviceName        = serviceDescription.Name;

            // Set by BuildAndRunService
            var args             = service.Status.Args !;
            var path             = service.Status.ExecutablePath !;
            var workingDirectory = service.Status.WorkingDirectory !;

            async Task RunApplicationAsync(IEnumerable <(int ExternalPort, int Port, string?Protocol)> ports, string copiedArgs)
            {
                // Make sure we yield before trying to start the process, this is important so we don't block startup
                await Task.Yield();

                var hasPorts = ports.Any();

                var environment = new Dictionary <string, string>
                {
                    // Default to development environment
                    ["DOTNET_ENVIRONMENT"]     = "Development",
                    ["ASPNETCORE_ENVIRONMENT"] = "Development",
                    // Remove the color codes from the console output
                    ["DOTNET_LOGGING__CONSOLE__DISABLECOLORS"]     = "true",
                    ["ASPNETCORE_LOGGING__CONSOLE__DISABLECOLORS"] = "true"
                };

                // Set up environment variables to use the version of dotnet we're using to run
                // this is important for tests where we're not using a globally-installed dotnet.
                var dotnetRoot = GetDotnetRoot();

                if (dotnetRoot is object)
                {
                    environment["DOTNET_ROOT"] = dotnetRoot;
                    environment["DOTNET_MULTILEVEL_LOOKUP"] = "0";
                    environment["PATH"] = $"{dotnetRoot};{Environment.GetEnvironmentVariable("PATH")}";
                }

                application.PopulateEnvironment(service, (k, v) => environment[k] = v);

                if (_options.DebugMode && (_options.DebugAllServices || _options.ServicesToDebug !.Contains(serviceName, StringComparer.OrdinalIgnoreCase)))
                {
                    environment["DOTNET_STARTUP_HOOKS"] = typeof(Hosting.Runtime.HostingRuntimeHelpers).Assembly.Location;
                }

                if (hasPorts)
                {
                    // These are the ports that the application should use for binding

                    // 1. Configure ASP.NET Core to bind to those same ports
                    environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://{application.ContainerEngine.AspNetUrlsHost}:{p.Port}"));

                    // Set the HTTPS port for the redirect middleware
                    foreach (var p in ports)
                    {
                        if (string.Equals(p.Protocol, "https", StringComparison.OrdinalIgnoreCase))
                        {
                            // We need to set the redirect URL to the exposed port so the redirect works cleanly
                            environment["HTTPS_PORT"] = p.ExternalPort.ToString();
                        }
                    }

                    // 3. For non-ASP.NET Core apps, pass the same information in the PORT env variable as a semicolon separated list.
                    environment["PORT"] = string.Join(";", ports.Select(p => $"{p.Port}"));

                    if (service.ServiceType == ServiceType.Function)
                    {
                        // Need to inject port and UseHttps as an argument to func.exe rather than environment variables.
                        var binding = ports.First();
                        copiedArgs += $" --port {binding.Port}";
                        if (binding.Protocol == "https")
                        {
                            copiedArgs += " --useHttps";
                        }
                    }
                }

                var backOff = TimeSpan.FromSeconds(5);

                while (!processInfo.StoppedTokenSource.IsCancellationRequested)
                {
                    var replica = serviceName + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower();
                    var status  = new ProcessStatus(service, replica);

                    using var stoppingCts      = new CancellationTokenSource();
                    status.StoppingTokenSource = stoppingCts;
                    await using var _          = processInfo.StoppedTokenSource.Token.Register(() => status.StoppingTokenSource.Cancel());

                    if (!_options.Watch)
                    {
                        service.Replicas[replica] = status;
                        service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status));
                    }

                    // This isn't your host name
                    environment["APP_INSTANCE"] = replica;

                    status.ExitCode    = null;
                    status.Pid         = null;
                    status.Environment = environment;

                    if (hasPorts)
                    {
                        status.Ports    = ports.Select(p => p.Port);
                        status.Bindings = ports.Select(p => new ReplicaBinding()
                        {
                            Port = p.Port, ExternalPort = p.ExternalPort, Protocol = p.Protocol
                        }).ToList();
                    }

                    // TODO clean this up.
                    foreach (var env in environment)
                    {
                        copiedArgs = copiedArgs.Replace($"%{env.Key}%", env.Value);
                    }

                    _logger.LogInformation("Launching service {ServiceName}: {ExePath} {args}", replica, path, copiedArgs);

                    try
                    {
                        service.Logs.OnNext($"[{replica}]:{path} {copiedArgs}");
                        var processInfo = new ProcessSpec
                        {
                            Executable           = path,
                            WorkingDirectory     = workingDirectory,
                            Arguments            = copiedArgs,
                            EnvironmentVariables = environment,
                            OutputData           = data =>
                            {
                                service.Logs.OnNext($"[{replica}]: {data}");
                            },
                            ErrorData = data => service.Logs.OnNext($"[{replica}]: {data}"),
                            OnStart   = pid =>
                            {
                                if (hasPorts)
                                {
                                    _logger.LogInformation("{ServiceName} running on process id {PID} bound to {Address}", replica, pid, string.Join(", ", ports.Select(p => $"{p.Protocol ?? "http"}://localhost:{p.Port}")));
                                }
                                else
                                {
                                    _logger.LogInformation("{ServiceName} running on process id {PID}", replica, pid);
                                }

                                // Reset the backoff
                                backOff = TimeSpan.FromSeconds(5);

                                status.Pid = pid;

                                WriteReplicaToStore(pid.ToString());

                                if (_options.Watch)
                                {
                                    // OnStart/OnStop will be called multiple times for watch.
                                    // Watch will constantly be adding and removing from the list, so only add here for watch.
                                    service.Replicas[replica] = status;
                                    service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status));
                                }

                                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, status));
                            },
                            OnStop = exitCode =>
                            {
                                status.ExitCode = exitCode;

                                if (status.Pid != null)
                                {
                                    service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Stopped, status));
                                }

                                if (!_options.Watch)
                                {
                                    // Only increase backoff when not watching project as watch will wait for file changes before rebuild.
                                    backOff *= 2;
                                }

                                service.Restarts++;

                                service.Replicas.TryRemove(replica, out var _);
                                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status));

                                if (status.ExitCode != null)
                                {
                                    _logger.LogInformation("{ServiceName} process exited with exit code {ExitCode}", replica, status.ExitCode);
                                }
                            },
                            Build = async() =>
                            {
                                if (service.Description.RunInfo is ProjectRunInfo)
                                {
                                    var buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{service.Status.ProjectFilePath}\" /nologo", throwOnError : false, workingDirectory : application.ContextDirectory);

                                    if (buildResult.ExitCode != 0)
                                    {
                                        _logger.LogInformation("Building projects failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode);
                                    }
                                    return(buildResult.ExitCode);
                                }

                                return(0);
                            }
                        };

                        if (_options.Watch && (service.Description.RunInfo is ProjectRunInfo runInfo))
                        {
                            var projectFile    = runInfo.ProjectFile.FullName;
                            var fileSetFactory = new MsBuildFileSetFactory(_logger,
                                                                           projectFile,
                                                                           waitOnError: true,
                                                                           trace: false);
                            environment["DOTNET_WATCH"] = "1";

                            await new DotNetWatcher(_logger)
                            .WatchAsync(processInfo, fileSetFactory, replica, status.StoppingTokenSource.Token);
                        }
                        else if (_options.Watch && (service.Description.RunInfo is AzureFunctionRunInfo azureFunctionRunInfo) && !string.IsNullOrEmpty(azureFunctionRunInfo.ProjectFile))
                        {
                            var projectFile    = azureFunctionRunInfo.ProjectFile;
                            var fileSetFactory = new MsBuildFileSetFactory(_logger,
                                                                           projectFile,
                                                                           waitOnError: true,
                                                                           trace: false);
                            environment["DOTNET_WATCH"] = "1";

                            await new DotNetWatcher(_logger)
                            .WatchAsync(processInfo, fileSetFactory, replica, status.StoppingTokenSource.Token);
                        }
                        else
                        {
                            await ProcessUtil.RunAsync(processInfo, status.StoppingTokenSource.Token, throwOnError : false);
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(0, ex, "Failed to launch process for service {ServiceName}", replica);

                        if (!_options.Watch)
                        {
                            // Only increase backoff when not watching project as watch will wait for file changes before rebuild.
                            backOff *= 2;
                        }

                        service.Restarts++;
                        service.Replicas.TryRemove(replica, out var _);

                        try
                        {
                            await Task.Delay(backOff, processInfo.StoppedTokenSource.Token);
                        }
                        catch (OperationCanceledException)
                        {
                            // Swallow cancellation exceptions and continue
                        }
                    }
                }
            }

            if (serviceDescription.Bindings.Count > 0)
            {
                // Each replica is assigned a list of internal ports, one mapped to each external
                // port
                for (int i = 0; i < serviceDescription.Replicas; i++)
                {
                    var ports = new List <(int, int, string?)>();
                    foreach (var binding in serviceDescription.Bindings)
                    {
                        if (binding.Port == null)
                        {
                            continue;
                        }

                        ports.Add((binding.Port.Value, binding.ReplicaPorts[i], binding.Protocol));
                    }

                    processInfo.Tasks[i] = RunApplicationAsync(ports, args);
                }
            }
            else
            {
                for (int i = 0; i < service.Description.Replicas; i++)
                {
                    processInfo.Tasks[i] = RunApplicationAsync(Enumerable.Empty <(int, int, string?)>(), args);
                }
            }

            service.Items[typeof(ProcessInfo)] = processInfo;
        }
示例#9
0
        private async Task <int> MainInternalAsync(
            IReporter reporter,
            string project,
            IReadOnlyList <string> args,
            CancellationToken cancellationToken)
        {
            // TODO multiple projects should be easy enough to add here
            string projectFile;

            try
            {
                projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, project);
            }
            catch (FileNotFoundException ex)
            {
                reporter.Error(ex.Message);
                return(1);
            }

            var isDefaultRunCommand = false;

            if (args.Count == 1 && args[0] == "run")
            {
                isDefaultRunCommand = true;
            }
            else if (args.Count == 0)
            {
                isDefaultRunCommand = true;
                args = new[] { "run" };
            }

            var watchOptions = DotNetWatchOptions.Default;

            var fileSetFactory = new MsBuildFileSetFactory(reporter,
                                                           watchOptions,
                                                           projectFile,
                                                           waitOnError: true,
                                                           trace: false);
            var processInfo = new ProcessSpec
            {
                Executable           = DotnetMuxer.MuxerPath,
                WorkingDirectory     = Path.GetDirectoryName(projectFile),
                Arguments            = args,
                EnvironmentVariables =
                {
                    ["DOTNET_WATCH"] = "1"
                },
            };

            if (CommandLineOptions.IsPollingEnabled)
            {
                _reporter.Output("Polling file watcher is enabled");
            }

            var defaultProfile = LaunchSettingsProfile.ReadDefaultProfile(_workingDirectory, reporter);

            var context = new DotNetWatchContext
            {
                ProcessSpec = processInfo,
                Reporter    = _reporter,
                SuppressMSBuildIncrementalism = watchOptions.SuppressMSBuildIncrementalism,
                DefaultLaunchSettingsProfile  = defaultProfile,
            };

            if (isDefaultRunCommand && !string.IsNullOrEmpty(defaultProfile?.HotReloadProfile))
            {
                _reporter.Verbose($"Found HotReloadProfile={defaultProfile.HotReloadProfile}. Watching with hot-reload");

                // We'll sue hot-reload based watching if
                // a) watch was invoked with no args or with exactly one arg - the run command e.g. `dotnet watch` or `dotnet watch run`
                // b) The launch profile supports hot-reload based watching.
                // The watcher will complain if users configure this for runtimes that would not support it.
                await using var watcher = new HotReloadDotNetWatcher(reporter, fileSetFactory, watchOptions, _console);
                await watcher.WatchAsync(context, cancellationToken);
            }
            else
            {
                _reporter.Verbose("Did not find a HotReloadProfile or running a non-default command. Watching with legacy behavior.");

                // We'll use the presence of a profile to decide if we're going to use the hot-reload based watching.
                // The watcher will complain if users configure this for runtimes that would not support it.
                await using var watcher = new DotNetWatcher(reporter, fileSetFactory, watchOptions);
                await watcher.WatchAsync(context, cancellationToken);
            }

            return(0);
        }
示例#10
0
        private async Task <int> MainInternalAsync(CommandLineOptions options, CancellationToken cancellationToken)
        {
            // TODO multiple projects should be easy enough to add here
            string projectFile;

            try
            {
                projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, options.Project);
            }
            catch (FileNotFoundException ex)
            {
                _reporter.Error(ex.Message);
                return(1);
            }

            var args = options.RemainingArguments;

            var isDefaultRunCommand = false;

            if (args.Count == 1 && args[0] == "run")
            {
                isDefaultRunCommand = true;
            }
            else if (args.Count == 0)
            {
                isDefaultRunCommand = true;
                args = new[] { "run" };
            }

            var watchOptions = DotNetWatchOptions.Default;

            watchOptions.NonInteractive = options.NonInteractive;

            var fileSetFactory = new MsBuildFileSetFactory(_reporter,
                                                           watchOptions,
                                                           projectFile,
                                                           waitOnError: true,
                                                           trace: false);
            var processInfo = new ProcessSpec
            {
                Executable           = DotnetMuxer.MuxerPath,
                WorkingDirectory     = Path.GetDirectoryName(projectFile),
                Arguments            = args,
                EnvironmentVariables =
                {
                    ["DOTNET_WATCH"] = "1"
                },
            };

            if (CommandLineOptions.IsPollingEnabled)
            {
                _reporter.Output("Polling file watcher is enabled");
            }

            var defaultProfile = LaunchSettingsProfile.ReadDefaultProfile(processInfo.WorkingDirectory, _reporter) ?? new();

            var context = new DotNetWatchContext
            {
                ProcessSpec = processInfo,
                Reporter    = _reporter,
                SuppressMSBuildIncrementalism = watchOptions.SuppressMSBuildIncrementalism,
                DefaultLaunchSettingsProfile  = defaultProfile,
            };

            context.ProjectGraph = TryReadProject(projectFile);

            if (!options.NoHotReload && isDefaultRunCommand && context.ProjectGraph is not null && IsHotReloadSupported(context.ProjectGraph))
            {
                _reporter.Verbose($"Project supports hot reload and was configured to run with the default run-command. Watching with hot-reload");

                // Use hot-reload based watching if
                // a) watch was invoked with no args or with exactly one arg - the run command e.g. `dotnet watch` or `dotnet watch run`
                // b) The launch profile supports hot-reload based watching.
                // The watcher will complain if users configure this for runtimes that would not support it.
                await using var watcher = new HotReloadDotNetWatcher(_reporter, _requester, fileSetFactory, watchOptions, _console, _workingDirectory);
                await watcher.WatchAsync(context, cancellationToken);
            }