dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.45k stars 10.03k forks source link

.NET Core 3.0 + Angular 9 cli application called via the SpaServices extensions hangs in common configuration #17277

Closed miguellira closed 3 years ago

miguellira commented 4 years ago

Describe the bug

Running a .NET Core 3.0 + Angular 9 cli (version 9.0.0-rc.2 as of this post) with the angular cli build option progress set to false will hang until the StartupTimeout expires (default: 120 seconds)

To Reproduce

I created a repo with a bare bones .NET Core 3.0 + Angular 9 cli application to demo the bug. Simply clone, npm build the ClientApp folder, and dotnet run. Or you can follow the steps below to do this from scratch:

  1. Ensure you have .NET Core 3.0 installed
  2. Create a .NET Core application with the current Angular templates by typing: dotnet new angular -n HelloWorld
  3. Navigate to the ClientApp folder and install the Typescript helper functions required for Angular 9 by typing: npm install tslib@latest
  4. Update to Angular 9 by typing: ng update @angular/core @angular/cli --next
  5. Ensure the progress build option in the angular.json file is set to false
  6. Run the application by navigating back to the root folder and typing: dotnet run
  7. Launch a browser and navigate to: https://localhost:5001

The application will hang and eventually timeout.

Further technical details

This appears to be caused by a change in how ng serve outputs to the console in the new Angular 9 CLI. The AngularCliMiddleware makes a WaitForMatch() method call against the standard output to signify when the Angular assets have been generated and the web server is ready to receive requests (Line 84). However, unless the progress option is set to true in the angular.json file you never see the expected line.

UPDATE: Updated to .NET Core 3.1 and Angular 9.0.0-rc.5. Same issue. New, simpler workaround is to modify your npm start script to perform a simple echo prior to ng serve (see comment below)

UPDATE (6/5/2020): Lots of recent comments so I figured I'd share exactly what has worked for me with every release of .NET and Angular since my original post. Update your package.json like so:

  "scripts": {
    "ng": "ng",
    "start": "echo hello && ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  }
benelliott commented 4 years ago

I also have this issue, but don't want to have to re-enable progress as webpack's build progress output is routed to stderr, and this makes log4net go a bit nuts. If this could be solved in a different way it would be great - especially since listening for the dev server to spit out a specific string always felt a bit fragile.

benelliott commented 4 years ago

Actually, I don't think it's quite as simple as that - when I run ng serve --progress=false from my App directory, I still see the listening on... message.

I think the problem is that the message will only show on the first log output from the CLI (https://github.com/angular/angular-cli/blob/2c0efd834b08ac5ea166d88c0ff57cc35df19421/packages/angular_devkit/build_angular/src/dev-server/index.ts#L361) and this often takes longer than the five second default timeout after which .NET assumes that the CLI has died (https://github.com/aspnet/AspNetCore/blob/master/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs#L22).

elianora commented 4 years ago

I am using .NET Core 3.1 and am encountering the same issue using @angular/cli@~9.0.0-rc.5. I never get the line expected by the AngularCliMiddleware from the Angular CLI. I have had it work correctly intermittently with both --progress=true and --progress=false.

I have zero clue as to why the following workaround works, but I consistently get the expected behavior if I add any statement that outputs to stdout prior to ng serve . My package.json script is echo hello && npm serve --progress=false, and I get the following reliably:

image

EDIT: Adding environment info:

benelliott commented 4 years ago

@elianora I spent some time digging around today and found this bug: https://github.com/aspnet/AspNetCore/issues/6146

Basically, the wrapper around StdOut might miss the listening on... message if it's at the end of a block with lots of newlines. As of Angular 9 this is a problem because the message is more likely to be printed in the same block as the webpack chunk output.

Could you check that you're referencing the latest version of the SpaServices packages? I think this fix (https://github.com/aspnet/AspNetCore/commit/ab02951b37ac0cb09f8f6c3ed0280b46d89b06e0) is only present in 3.0.0 onwards.

elianora commented 4 years ago

I am currently referencing 3.1.0 and my TargetFramework is set to netcoreapp3.1.

benelliott commented 4 years ago

I've made a shamelessly hacky workaround which will do for now. Basically, I wrap the call to the Angular CLI in another Node script, and spoof the "open your browser on" message so that dotnet notices it.

You can try the workaround yourself:

  1. Edit your package.json to rename your start script to start:ng
  2. Add a new start script: "start": "node start-dotnet.js". Your package.json should look like
        "start":  "node start-for-dotnet.js",
        "start:ng": "ng serve [or whatever you had before]",
  3. Next to your package.json, add a file called start-dotnet.js with contents
// Hack to work around https://github.com/aspnet/AspNetCore/issues/17277

const { spawn } = require('child_process');

const portArgIndex = process.argv.indexOf('--port');

if (portArgIndex === -1) {
    throw new Error('Could not detect port number');
}

const port = process.argv[portArgIndex + 1];

console.log(`open your browser on http://localhost:${port}`);

const child = spawn(`cmd`, ['/c', `npm.cmd run start:ng -- --port ${port}`]);

console.log('Angular CLI started on PID', child.pid);

child.stdout.on('data', x => console.log(x && x.toString()));

child.stderr.on('data', x => console.error(x && x.toString()));

const sleep = () => {
    console.log('[Node.js keepalive]');
    setTimeout(sleep, 10000);
}

sleep();
benelliott commented 4 years ago
const child = spawn(`cmd`, ['/c', `npm.cmd run start:ng -- --port ${port}`]);

This line is Windows-specific, the Linux equivalent would be something along the lines of (but possibly not exactly)

const child = spawn(`npm`, ['run', `start:ng -- --port ${port}`]);
elianora commented 4 years ago

I run into no issues just using echo something && ng serve, but certainly will keep this in mind if I or anyone else I'm working with run into this issue again; when I get home I will see what the behavior on other operating systems looks like.

benelliott commented 4 years ago

Do you still have dotnet randomly choosing a CLI port when you do that?

elianora commented 4 years ago

I do, yes.

miguellira commented 4 years ago

@elianora suggestion of modifying the npm start script in your package.json by adding an echo statement prior to ng serve is enough to notify the AngularCliMiddleware the angular dev server is running. This is a better workaround than setting the progress flag to true.

BTW, can't see how it would matter but I did notice a subtle difference in how the Angular CLI outputs build info between Angular 8 and 9. Specifically, the Date and Hash information comes after the chunk information in Angular 9, where as it is the first line in Angular 8.

info: Microsoft.AspNetCore.SpaServices[0]
chunk {main} main.js, main.js.map (main) 60.7 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 140 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.15 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 9.94 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 2.99 MB [initial] [rendered]
Date: 2019-12-06T04:20:45.321Z - Hash: 6ee5d8996273b040afe7 - Time: 5917ms <-- This is now on one line and in a new location in Angular 9
...
Of course the actual string the AngularCliMiddleware is searching for: "open your browser on" is still being emitted by the angular_devkit package (Line 303).

benelliott commented 4 years ago

The echo workaround works for me too... so my workaround might be slight overkill :)

I don't fully get why it fixes it though...

EDIT: it seems that when I add the --aot flag, the echo workaround no longer helps. I have to use the NodeJS workaround in this case.

krisztiankocsis commented 4 years ago

ng serve --verbose also helps!

krisztiankocsis commented 4 years ago

But as verbose mode extremely pollutes the console, it would be better to detect readiness by a background HTTP GET instead of parsing the console output.

pbarranis commented 4 years ago

Due to a dependency, I'm unable to upgrade my .NET Core 2.2 app to 3 yet, so when I upgraded to Angular 9 I ran into this issue as well. The echo solution saved me; thank you @elianora !

Side note: I don't have progress set to false. It is true and I still ran into this issue, so I don't know that that's a factor here.

blogcraft commented 4 years ago

Echo solution works sometimes. Some other times, it just hangs after logging the chunks without printing this message:

info: Microsoft.AspNetCore.SpaServices[0] : Compiled successfully.

JessyParis commented 4 years ago

I encounter the same issue, and the "echo" workaround works for me (even if I don't understand why). I don't know if it may help but with Angular 8 I had this lines

Microsoft.AspNetCore.SpaServices: Information: i ´¢ówds´¢ú: webpack output is served from /
i ´¢ówds´¢ú: 404s will fallback to //index.html

Microsoft.AspNetCore.SpaServices: Information: chunk {main} main.js, main.js.map (main) 1.29 MB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 284 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.15 kB [entry] [rendered]
chunk {styles} styles.css, styles.css.map (styles) 827 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 10.8 MB [initial] [rendered]
Date: 2020-02-20T14:29:22.992Z - Hash: 6706dd23fbf91f7a606a - Time: 20082ms

Microsoft.AspNetCore.SpaServices: Information: ** Angular Live Development Server is listening on localhost:50733, open your browser on http://localhost:50733/ **
i ´¢ówdm´¢ú: Compiled successfully.

With Angular 9: the line 404s will fallback to //index.html is not there anymore

Microsoft.AspNetCore.SpaServices: Information: > evalorplastwebapp@0.0.0 start D:\Sources\Repos\Valorplast\eValorplastWebApp\ClientApp
> ng serve "--port" "51738"

Microsoft.AspNetCore.SpaServices: Information: chunk {main} main.js, main.js.map (main) 2.15 MB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 140 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.15 kB [entry] [rendered]
chunk {styles} styles.css, styles.css.map (styles) 889 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 12.6 MB [initial] [rendered]
Date: 2020-02-20T14:57:58.254Z - Hash: 081b4ea7fae540c9d19b - Time: 25137ms

Microsoft.AspNetCore.SpaServices: Information: ** Angular Live Development Server is listening on localhost:51738, open your browser on http://localhost:51738/ **
: Compiled successfully.
kurtisauc3 commented 4 years ago

Still waiting on a fix for this. The echo solution doesn't work for me, but the node does. It's pretty ugly. Not sure if angular needs to change the way they emit to the console or the spa service extension needs to change the way it listens.

For anyone wanting to use Angular 9 with a netcore3 app, i would recommend going back to angular 8 until this gets resolved. Otherwise the node hack from @benelliott worked for me

hordani commented 4 years ago

What worked for me was changing "ng serve" to "ng serve --host 0.0.0.0" in package.json "start"

zql9000 commented 4 years ago

Hello, I had the same problem and I performed several tests on different projects and simply saving some change in a file inside the ClientApp folder, it makes angular recompile and then VS2019 / VSCode recognizes the node server, without changing any configuration. I comment this in case it helps anyone.

I also tested in the solution of the repo miguellira/Angular9CliTemplateBug with VS 2019 Community and VS Code and works.

ravimsnetdev commented 4 years ago

What worked for me was changing "ng serve" to "ng serve --host 0.0.0.0" in package.json "start"

this worked for me

markoj21 commented 4 years ago

The above solutions did not work for me but this did

  1. In your Startup class, replace the spa.UseAngularCliServer invocation with the following: spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
  2. In a command prompt, switch to the ClientApp subdirectory, and launch the Angular CLI development server: cd ClientApp npm start
  3. Run the solution like normal
kemsky commented 4 years ago

EventedStreamReader is simply buggy:

               //  should use LastIndexOf and process all lines, otherwise loop will stuck on _streamReader.ReadAsync until some file is modified
                var lineBreakPos = Array.IndexOf(buf, '\n', 0, chunkLength); 
                if (lineBreakPos < 0)
                {
                    _linesBuffer.Append(buf, 0, chunkLength);
                }
                else
                {
                    _linesBuffer.Append(buf, 0, lineBreakPos + 1);
                    OnCompleteLine(_linesBuffer.ToString());
                    _linesBuffer.Clear();
                    _linesBuffer.Append(buf, lineBreakPos + 1, chunkLength - (lineBreakPos + 1));
                }
natenho commented 4 years ago

I have zero clue as to why the following workaround works, but I consistently get the expected behavior if I add any statement that outputs to stdout prior to ng serve . My package.json script is echo hello && npm serve --progress=false, and I get the following reliably:

@elianora, according to the screenshot, the command is echo hello && ng serve --progress=false (instead of npm serve)

It's fine as a workaround for now, thanks.

Cito commented 4 years ago

Four months now and still no proper solution for this?

blogcraft commented 4 years ago

I guess no contributor has looked at this yet. Still, that's a bad thing. 😕

aprilmaraat commented 4 years ago

Mine works by editing "start": "ng serve --port 4200"

nhonlvsoict commented 4 years ago

What worked for me was changing "ng serve" to "ng serve --host 0.0.0.0" in package.json "start"

this worked for me, thankss

kemsky commented 4 years ago

@mkArtakMSFT, EventedStreamReader is already fixed in master, related issues #6306, #6146. However nuget package Microsoft.AspNetCore.SpaServices.Extensions 3.1.3 does not include these fixes:

https://github.com/dotnet/aspnetcore/blob/e81033e094d4663ffd227bb4aed30b76b0631e6d/src/Middleware/SpaServices.Extensions/src/Util/EventedStreamReader.cs

hmheng commented 4 years ago

seems like issue still persists in 3.1.3 spa extension, even with "echo"

sismino commented 4 years ago

hi, waiting for fix, this works under win os; edit package.json like this:

"start": "@echo open your browser on http://localhost:4200 & ng serve & rem",

hmheng commented 4 years ago

it seems like

hi, waiting for fix, this works under win os; edit package.json like this:

"start": "@echo open your browser on http://localhost:4200 & ng serve & rem",

thanks for the help. it works for a while. very soon it will hang again.

hmheng commented 4 years ago

I've made a shamelessly hacky workaround which will do for now. Basically, I wrap the call to the Angular CLI in another Node script, and spoof the "open your browser on" message so that dotnet notices it.

You can try the workaround yourself:

  1. Edit your package.json to rename your start script to start:ng
  2. Add a new start script: "start": "node start-dotnet.js". Your package.json should look like
        "start":  "node start-for-dotnet.js",
        "start:ng": "ng serve [or whatever you had before]",
  1. Next to your package.json, add a file called start-dotnet.js with contents
// Hack to work around https://github.com/aspnet/AspNetCore/issues/17277

const { spawn } = require('child_process');

const portArgIndex = process.argv.indexOf('--port');

if (portArgIndex === -1) {
    throw new Error('Could not detect port number');
}

const port = process.argv[portArgIndex + 1];

console.log(`open your browser on http://localhost:${port}`);

const child = spawn(`cmd`, ['/c', `npm.cmd run start:ng -- --port ${port}`]);

console.log('Angular CLI started on PID', child.pid);

child.stdout.on('data', x => console.log(x && x.toString()));

child.stderr.on('data', x => console.error(x && x.toString()));

const sleep = () => {
    console.log('[Node.js keepalive]');
    setTimeout(sleep, 10000);
}

sleep();

While waiting to proper fix, i think this is best hack that seems working fine. thank you @benelliott

mkArtakMSFT commented 4 years ago

Sorry that this is taking this long. We've have been busy with Blazor WebAssembly during last several months. The good news is that we're getting to the end of it and will investigate this in approximately a month. Just moved this to 5.0-preview5 release to make sure this doesn't get ignored any further, given the amount of feedback this has accumulated. /cc @danroth27

varghesep commented 4 years ago

The angular app created via Visual Studio 2019 16.6 Preview is version 8 not 9. Will it support Angular 9 in the future?

mkArtakMSFT commented 4 years ago

The angular app created via Visual Studio 2019 16.6 Preview is version 8 not 9. Will it support Angular 9 in the future?

We haven't added official support for Angular 9 yet. The issue I've referenced above is tracking that work. We'll just make sure this issue is resolved when doing that.

hmheng commented 4 years ago

thank you so much @mkArtakMSFT . While it is being brought forward to 5.0-preview, will it be fixed in next patch of .NET Core 3.1 too considering the timeline of 5.0 RTM is still months away from now?

hmheng commented 4 years ago

bde93cce-5137-4e82-844f-bd11993edb83

@benelliott 's solution is definitely working, however, I noticed that it will cause memory hogging and not releasing the nodejs process after each debug session. I'm not sure if it is caused by VS2019 for not releasing it while opening new nodejs instance or it is caused by NG9.

naveedahmed1 commented 4 years ago

I also noticed this, and I believe its the issue with VS2019.

dstj commented 4 years ago

[...] I noticed that it will cause memory hogging and not releasing the nodejs process after each debug session. I'm not sure if it is caused by VS2019 for not releasing it while opening new nodejs instance or it is caused by NG9.

This is definitely not new, I have been having that problem for a long time now, see #5239 and #5204

naveedahmed1 commented 4 years ago

taskkill /f /im node.exe will close all running node processes, I was wondering if this could be added to .csproj file like <Exec Command="taskkill /im node.exe /f"></Exec> and have it executed after each debug session ends?

hmheng commented 4 years ago

taskkill /f /im node.exe will close all running node processes, I was wondering if this could be added to .csproj file like <Exec Command="taskkill /im node.exe /f"></Exec> and have it executed after each debug session ends?

With taskkill like that, it may be killing other nodejs process that are opened in another instance/solution of VS2019?

benelliott commented 4 years ago

With taskkill like that, it may be killing other nodejs process that are opened in another instance/solution of VS2019?

Yes definitely, that would kill every Node process on the box.

naveedahmed1 commented 4 years ago

Anyway to target only node processes that are launched by Visual Studio?

BTW I dont have anyother node processes running and this seems to work in my case. But the issue is it terminates the processes only when next time I start debug.

artar94 commented 4 years ago

@mkArtakMSFT Still, this is not fixed in master. Here is added a line with a large number of trailing zeros, which sometimes leads to a hang of the thread. https://github.com/dotnet/aspnetcore/blob/1815d37d752ec1066726c1be7c52e808b11145f1/src/Middleware/SpaServices.Extensions/src/Util/EventedStreamReader.cs#L114 Only the significant portion of the buffer needs to be added:

_linesBuffer.Append(buf, startPos, chunkLength - startPos);

Perhaps the problem is deeper and Append should not freeze, but there is definitely an error in this code.

viveksinghnegi7 commented 4 years ago

Encountered the same issue. Waiting for a permanent solution. Sometime it works after re-starting the Visual Studio but after some time the exception occurs. An unhandled exception occurred while processing the request. TimeoutException: The Angular CLI process did not start listening for requests within the timeout period of 20 seconds. Check the log output for error information.

`System.TimeoutException: The Angular CLI process did not start listening for requests within the timeout period of 20 seconds. Check the log output for error information.
   at Microsoft.AspNetCore.SpaServices.Extensions.Util.TaskTimeoutExtensions.WithTimeout[T](Task`1 task, TimeSpan timeoutDelay, String message)
   at Microsoft.AspNetCore.SpaServices.Extensions.Proxy.SpaProxy.PerformProxyRequest(HttpContext context, HttpClient httpClient, Task`1 baseUriTask, CancellationToken applicationStoppingToken, Boolean proxy404s)
   at Microsoft.AspNetCore.Builder.SpaProxyingExtensions.<>c__DisplayClass2_0.<<UseProxyToSpaDevelopmentServer>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)`
Rinsen commented 4 years ago

@mkArtakMSFT

Sorry that this is taking this long. We've have been busy with Blazor WebAssembly during last several months. The good news is that we're getting to the end of it and will investigate this in approximately a month. Just moved this to 5.0-preview5 release to make sure this doesn't get ignored any further, given the amount of feedback this has accumulated.

This will not be fixed in 3.x? It's a bummer if that's the case since 5.0 is not a LTS version and it will be a great challenge to use in products that customers buy and run because of the "short" support window.

If we look into the future Angular will be at version 12 or so when .NET 6 is ready for release so if we have breaking changes in every other Angular release similar to this and only get fixes in majour releases we will have a big problem.

.NET 6 is planned to be released as LTS in end of 2021 and Angular 9 is end of life in Aug 06, 2021 but is end of what they call "Active Support"already Aug 06, 2020. That might give us a situation where this fix is not available at all during Angular 9s entire "active support" lifespan....?

https://angular.io/guide/releases

smaugcoath commented 4 years ago

Bfff... I've just seen that I'm not alone here... 6 AM and still working.

dobrinsky commented 4 years ago

For me, none of the solutions posted here worked, not even changing "ng serve" to "ng serve --host 0.0.0.0" in package.json "start"

But another change in the same place found in:

https://github.com/angular/angular-cli/issues/16961

change "start": "ng serve" to "start": "echo Starting... && ng serve"

worked

hembk44 commented 4 years ago

I am using .NET Core 3.1 and am encountering the same issue using @angular/cli@~9.0.0-rc.5. I never get the line expected by the AngularCliMiddleware from the Angular CLI. I have had it work correctly intermittently with both --progress=true and --progress=false.

I have zero clue as to why the following workaround works, but I consistently get the expected behavior if I add any statement that outputs to stdout prior to ng serve . My package.json script is echo hello && npm serve --progress=false, and I get the following reliably:

image

EDIT: Adding environment info:

  • Windows 10 build 18363
  • Node.js 12.13.0
  • Visual Studio Code 1.40.2
  • dotnet CLI 3.1.100
  • @angular/cli 9.0.0-rc.5

It worked for me.