Does this issue occur when all extensions are disabled?: Yes
- VS Code Version: 1.77.3
- OS Version: MacOS 13.3.1
Steps to Reproduce:
Run the following commands
mkdir test
cd test
cat <<"EOF" > package.json
{
"scripts": {
"watch": "while true; do sleep 1; echo \"$$ $(date)\"; done"
}
}
EOF
code .
Trust the workspace, then start the script from the "NPM Scripts" panel. The script runs, and the name of the script appears to the right of the output panel, together with a spinner showing that it's running.
Now run the script again. A popup window appears saying that it's already running, and do you want to terminate or restart it (just close it without doing either).
This is all expected behavior.
Now (with the task running) run Cmd-Shift-P "Developer: Reload Window". The window reloads, and the script continues to run (you can tell it's the same script because it's printing its pid, and it doesn't change). The name is still in the right hand pane, but now the spinner has gone, as if vscode no longer knows that it's running.
Now run the script again, and although it's already running, it starts a second copy. This is the bug. It's especially painful for "runOnOpen" tasks, because every time you reload the window (eg because an extension needs updating) they all continue to run AND a new copy of each one starts.
I thought there might be another bug in the recently added AbstractTaskService._inProgressTasks (since that was supposed to fix exactly this issue - #159640), and took a look at that. There does appear to be a pretty serious bug in that code:
|
if (this._inProgressTasks.has(qualifiedLabel)) { |
|
this._logService.info('Prevented duplicate task from running', qualifiedLabel); |
|
return { exitCode: 0 }; |
|
} |
|
this._inProgressTasks.add(qualifiedLabel); |
|
if (await this._saveBeforeRun()) { |
|
await this._configurationService.reloadConfiguration(); |
|
await this._updateWorkspaceTasks(); |
|
const taskFolder = task.getWorkspaceFolder(); |
|
const taskIdentifier = task.configurationProperties.identifier; |
|
const taskType = CustomTask.is(task) ? task.customizes()?.type : (ContributedTask.is(task) ? task.type : undefined); |
|
// Since we save before running tasks, the task may have changed as part of the save. |
|
// However, if the TaskRunSource is not User, then we shouldn't try to fetch the task again |
|
// since this can cause a new'd task to get overwritten with a provided task. |
|
taskToRun = ((taskFolder && taskIdentifier && (runSource === TaskRunSource.User)) |
|
? await this.getTask(taskFolder, taskIdentifier, false, taskType) : task) ?? task; |
|
} |
|
await ProblemMatcherRegistry.onReady(); |
|
const executeResult = runSource === TaskRunSource.Reconnect ? this._getTaskSystem().reconnect(taskToRun, resolver) : this._getTaskSystem().run(taskToRun, resolver); |
|
if (executeResult) { |
|
this._inProgressTasks.delete(qualifiedLabel); |
|
return this._handleExecuteResult(executeResult, runSource); |
|
} |
|
this._inProgressTasks.delete(qualifiedLabel); |
It adds qualifiedLabel to the _inProgressTasks at line 1839, then starts the task at line 1853, and then removes qualifiedLabel at line 1855 without waiting for the task to finish. So in practice the list is almost always empty, and doesn't actually do anything. I think the delete at line 1855 should actually be in a .finally clause attached to the _handleExecuteResult(...) on line 1856, so that it doesn't run until the task completes.
But fixing that doesn't actually fix this issue, because the set isn't preserved (or reconstructed) across reloads. In addition it breaks the above behavior where trying to run an already running task asks what you want to do with it (because now that inProgressTasks is actually working, it aborts the attempt to re-run the task before it gets to the point where that dialog is presented). It also breaks tasks that are explicitly marked as allowing more than one instance.
So since it isn't currently doing anything, and fixing it breaks things, I would suggest removing all the associated code (@meganrogge)
I'm not sure what the actual fix should be though. There seems to be code to try to reconnect tasks to terminals:
|
if (this._terminalService.getReconnectedTerminals('Task')?.length) { |
|
this._attemptTaskReconnection(); |
but it doesn't seem to work (I've stepped through the code with my example, and it goes into
_attemptTaskReconnection but doesn't actually find any tasks to reconnect). So maybe the bug is there. Or maybe the bug is that the tasks shouldn't be preserved across reload (many are not; a terminal that isn't running anything will be killed by reload, for example).
But I've reached the limits of what I can debug, unless anyone has suggestions for where to look.
Does this issue occur when all extensions are disabled?: Yes
Steps to Reproduce:
Run the following commands
Trust the workspace, then start the script from the "NPM Scripts" panel. The script runs, and the name of the script appears to the right of the output panel, together with a spinner showing that it's running.
Now run the script again. A popup window appears saying that it's already running, and do you want to terminate or restart it (just close it without doing either).
This is all expected behavior.
Now (with the task running) run Cmd-Shift-P "Developer: Reload Window". The window reloads, and the script continues to run (you can tell it's the same script because it's printing its pid, and it doesn't change). The name is still in the right hand pane, but now the spinner has gone, as if vscode no longer knows that it's running.
Now run the script again, and although it's already running, it starts a second copy. This is the bug. It's especially painful for "runOnOpen" tasks, because every time you reload the window (eg because an extension needs updating) they all continue to run AND a new copy of each one starts.
I thought there might be another bug in the recently added
AbstractTaskService._inProgressTasks(since that was supposed to fix exactly this issue - #159640), and took a look at that. There does appear to be a pretty serious bug in that code:vscode/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts
Lines 1835 to 1858 in e9c1114
It adds
qualifiedLabelto the_inProgressTasksat line 1839, then starts the task at line 1853, and then removesqualifiedLabelat line 1855 without waiting for the task to finish. So in practice the list is almost always empty, and doesn't actually do anything. I think thedeleteat line 1855 should actually be in a.finallyclause attached to the_handleExecuteResult(...)on line 1856, so that it doesn't run until the task completes.But fixing that doesn't actually fix this issue, because the set isn't preserved (or reconstructed) across reloads. In addition it breaks the above behavior where trying to run an already running task asks what you want to do with it (because now that inProgressTasks is actually working, it aborts the attempt to re-run the task before it gets to the point where that dialog is presented). It also breaks tasks that are explicitly marked as allowing more than one instance.
So since it isn't currently doing anything, and fixing it breaks things, I would suggest removing all the associated code (@meganrogge)
I'm not sure what the actual fix should be though. There seems to be code to try to reconnect tasks to terminals:
vscode/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts
Lines 340 to 341 in e9c1114
but it doesn't seem to work (I've stepped through the code with my example, and it goes into
_attemptTaskReconnectionbut doesn't actually find any tasks to reconnect). So maybe the bug is there. Or maybe the bug is that the tasks shouldn't be preserved across reload (many are not; a terminal that isn't running anything will be killed by reload, for example).But I've reached the limits of what I can debug, unless anyone has suggestions for where to look.