-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GH-815: Replace child_process
with the Process
API in the LS backend contribution
#841
Conversation
set killed(killed: boolean) { | ||
/* readonly public property */ | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this be ommited ?
what happens on process.killed = boolean ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
process.killed = boolean
That is a compiler error. Having a property getter is equivalent to a readonly
property from the client code's point of view. Let me know if you meant something else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK good :) that's what I wanted
import { Process } from './process'; | ||
import { Emitter, Event } from '@theia/core/lib/common'; | ||
import { ILogger } from '@theia/core/lib/common/logger'; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to have these changes in the previous commit
this.id++; | ||
return id; | ||
this.processes.set(++this.id, process); | ||
return this.id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just wanted the id to start at 0 .. you mind if if is -1 then ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not understand what you mean. Could you please explain this to me?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean doing ++this.id from this.id = 0... will make the first process have id 1 instead of 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it!
you mind if if is -1 then
-1
confused me. I will restore the original behavior. Thanks for the reminder!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.processes.set(this.id++, process);
return this.id
This would work actually :) with this.id = 0 to start.. so less confusing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strangely, changing to this.id++
breaks the Terminal Backend Contribution is data received from the terminal ws server
test. I have to check why.
readonly output: stream.Readable; | ||
readonly errorOutput: stream.Readable; | ||
protected process: child.ChildProcess; | ||
// XXX: Do we have to make this public? How to attach additional listeners to the underlying process then? | ||
readonly process: child.ChildProcess; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not want to shadow the process to allow direct API access to it since I wasn't sure yet what was needed.
I think it may be better to leave it public in case a user wants to do something non trivial with it and we don't want to wrap the whole API in raw-process.
Not sure what you mean by attaching additional listeners ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good the refactoring of process/terminal :) thx |
BTW I have not checked properly but it seems a bit weird to me that we have to manually send stdin to the terminal since it spawns the process and binds stdout/stderr to it... Maybe we don't have to do this and it's node by node-pty already? |
// Handles `Ctrl+C`. | ||
process.on('SIGINT', () => this.onStop()); | ||
// Handles `kill pid` | ||
process.on('SIGUSR1', () => this.onStop()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kill pid sends SIGTERM, not SIGUSR...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure? I am not, after all. See: https://stackoverflow.com/questions/14031763/doing-a-cleanup-action-just-before-node-js-exits
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I'm sure, this answer is wrong. See https://linux.die.net/man/1/kill
The default signal for kill is TERM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SIGURS are user-defined:
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
So we should not exit on those see: http://man7.org/linux/man-pages/man7/signal.7.html
You're the expert on this topic. |
this.spawnedProcessIds | ||
.map(id => this.processManager.get(id)) | ||
.filter(process => process) | ||
.forEach(process => this.processManager.delete(process!)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we wait for the processes to be killed before exiting ?
We can't be sure that just sending SIGTERM here will kill them...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, each call must be synchronous. https://nodejs.org/api/process.html#process_event_exit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean the call to kill I think is synchronous... but the signal handler of the processes is not...
So you send kill to all .. the main process exists.. but the childs will live on until they've handled the SIGTERM.
And if they don't exit properly with SIGTERM you will have lost them.
So maybe we should wait for their exit event before ending this operation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LS processes should exit automatically when the parent process is not alive, look for InitializeParams.processId comment in the lsp. If it does not happen it means that the worker process is alive. One should check it and if it is the case what the forked worker does not exit when the master exits, then changes in this pr won’t help since the worker is still running and the proper fix will be to explicitly stop running workers on exit in the master process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LS processes should exit automatically when the parent process is not alive
That is not happening: #815 (comment)
the worker process is alive
I cannot see other Theia processes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean, in general, any LS started from a jar?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or we use outdated java ls, I can see in their code handling of this case: https://github.com/eclipse/eclipse.jdt.ls/blob/master/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/LanguageServer.java
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the screenshot (gif whatever) I have attached to this thread does not do anything with the Java LS. It starts the Yang LS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, I thought lsp4j or Xtext ls supports it, but cannot find such code there. Btw the lsp says that shutdown + exit requests should be sent to shutdown the server.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eclipse/xtext-core#552 - a bug for it in Xtext
OK so I've verified this a bit and looking at the code for linux:
Stdin/stdout/stderr are mapped to the slave part of the pseudo terminal so anything writting to these outputs will appear. (And by default it's echo mode is on) So you don't need to manually write what is coming from stdin or stdout. See: http://rachid.koucha.free.fr/tech_corner/pty_pdip.html#Introduction_to_pseudo-terminals for more background info on ptys |
abstract readonly id: string; | ||
abstract readonly name: string; | ||
protected readonly spawnedProcessIds: number[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder processes can exit even if the worker is still running, then they should be removed from this list. Maybe we can use disposable pattern instead of onStop callback, here disposable collection will be used which is returned as a disposable by start method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this will change how the processes are disposed they will still be in the disposable list , even if they exited no ?
However this doesn't matter much.. if it has exited already the processManager will just ignore it and since ids are unique and not reused this is safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought that will be removed on exit by calling dispose on disposable returned from collection.push.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you mean to make the Process disposable and that it disposes of itself on exit() that won't work since you can't be certain that all the event listeners on the object have been called once you dispose of it in your exit() handler.
Also it complicates things a lot because other components could have a reference to that process and try to call invalid methods on it now that it is disposed....
Or maybe I'm missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I only meant to consider an alternative api for onStop: use disposable as a return type for onStart. If it makes things complicated I am fine with the current state like now.
5f4a5df
to
5219954
Compare
child_process
with the Process
API in the LS backend contributionchild_process
with the Process
API in the LS backend contribution
Please review. |
abstract start(clientConnection: IConnection): void; | ||
|
||
stop(): void { | ||
while (this.disposables.length > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if disposables
is DisposableCollection
, then you can just call dispose
here
return serverProcess; | ||
protected spawnProcess(command: string, args?: string[], options?: cp.SpawnOptions): RawProcess { | ||
const rawProcess = this.processFactory({ command, args, options }); | ||
this.disposables.push({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
disposable should be removed if the process exits before stop. DisposableCollection.push
returns Disposable
to do it
dispose: () => { | ||
const process = this.processManager.get(rawProcess.id); | ||
if (process) { | ||
this.processManager.delete(process); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and it happens automatically when the process exits, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it happens when you stop the lsp.
The process has no auto cleanup mecanism
readonly input: stream.Writable; | ||
readonly output: stream.Readable; | ||
readonly errorOutput: stream.Readable; | ||
// XXX: Do we have to make this public? How to attach additional listeners to the underlying process then? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think public is fine, why will we want to abstract clients from node.js apis?
serverProcess.stderr.on('data', this.logError.bind(this)); | ||
return serverProcess; | ||
protected spawnProcess(command: string, args?: string[], options?: cp.SpawnOptions): RawProcess { | ||
const rawProcess = this.processFactory({ command, args, options }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when will this process be deleted from the manager? as I read the code it seems never except onStop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing them should be managed by the ProcessManager, since we might want to keep a certain amount of history about closed processes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We decided to automatically unregister processes when they are killed until we have a different way of cleaning up terminated processes in the ProcessManager.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the process being killed in the code ?
Also I think it may be good to unregister the process after all automatically but it should be done in a on('exit' callback and registered by the processManager.
This is fine a long as the Process is not disposable. That way even if it's unregistered references to that process still exists and will receive the onDeleted event.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is killed when the connection is closed:
https://github.com/theia-ide/theia/blob/3ee55262a0d2a2758e680d1d845b81e746e86b48/packages/languages/src/node/language-server-contribution.ts#L71
during forwarding, if one connection get closed another will be closed as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the on('exit' could also be in the Process class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha ok thx my comments still apply
@hexa00 could you show where does it happen in the code? |
@akosyakov It doesn't happen anymore seems I was commenting on oudated code, I just mean that the lsp should call delete if it needs to because the process won't clean itself on exit. |
/** | ||
* Called on shut down. Only synchronous operations are permitted. | ||
*/ | ||
stop?(): void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this API is misleading
start
method is called each time when a new connection is established to start a language server, it can happen multiple times for the same client, e.g. the connection got lost and LanguageClient
decided to restart the server.
I would expect that stop
means to stop a language server and it should be called each time when the connection got lost. For this, though there is already clientConnectiont.onClose
event, which btw can be used to remove a process from the process manager.
I think the following API would be better:
start(clientConnection: IConnection): Disposable;
then a returned disposable can be disposed when the connection got lost as well as onStop
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the note, that's a leftover.
*/ | ||
unregister(process: Process): void { | ||
if (!process.killed) { | ||
process.kill(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if a process is not killed first it will not be unregistred ?
So you have to call it twice ?
I think the return line should be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? Can you explain? Thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well if it's not killed killed is called and returns before calling unregister
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you are wrong: process.kill()
will invoke processManager.unregister(this)
after setting killed
to true
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ho yes that is quite weird however imo... I'm commenting in another comment on what I think should be done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ho yes that is quite weird however imo...
I am open to any other approach. Since you came up with the initial implementation of the processes and the process manager, maybe you could advise how to unregister the process automatically from the manager on termination. Any ideas, and then I can update the PR with that? Thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes see: #841 (comment)
I think this will work fine
@@ -60,6 +62,8 @@ export class RawProcess extends Process { | |||
kill(signal?: string) { | |||
if (this.killed === false) { | |||
this.process.kill(signal); | |||
this.processManager.unregister(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is wrong here for a few reasons:
-
kill may have another signal than SIGTERM here, the user may send SIGHUP for example and thus the process shoud not be unregistred.
-
We should not unregister the process on SIGTERM to keep history, and if we did for now do that we would need to leave the kill to the unregister call, othewise we call kill twice and that could mean sending a kill signal to the wrong pid.
-
I think the LSP contribution should clean up it's processes by calling processManager.unregister(process)
-
That should be the general way to unregister a process. Otherwise we could add a Process->unregister() function that calls the processManager->unregister()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not unregister the process on SIGTERM to keep history
That is a very good idea, but this PR has no intention doing anything with the history right now.
I think the LSP contribution should clean up it's processes by calling processManager.unregister(process)
I think it would be odd. So when I create a process via the factory, then the process is registered into the system. Registration is done "automagically" but the unregister code should be invoked explicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I also this it's odd.
I think the best solution would be as described a bit in : #841 (comment)
- In Process class register a on('exit') callback and call processManager->unregister()
- Kill doesn't call unregister
That way processes will be cleaned-up automatically and terminal references will get notified of that using the delete event from processManager
It will also avoid checking for the signal type etc in kill.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops sorry it needs to be the ProcessManager that registers the on('exit') callback otherwise the Process will have a reference to itself and not get cleaned up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Process class register a on('exit') callback and call processManager->unregister()
What do you think, shall we remove the on('exit') => unregister from the TerminalBackendContribution
? Seems to be a duplicate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see such a callback ? You mean ws.on('close' ? this one is used to kill the process.
We could change it to kill once the auto unregister is done.
@@ -71,6 +86,7 @@ export class TerminalProcess extends Process { | |||
kill(signal?: string) { | |||
if (this.killed === false) { | |||
this.terminal.kill(signal); | |||
this.processManager.unregister(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not good for the same reasons as RawProcess
@hexa00, does this look correct? Maybe I am just tired; the process registration and the deletion are done with different IDs. We register with the sequence ID of the process manager then we try to delete it with the ID of the process. I think we have the same problem in this PR too. Please confirm. Thank you! Get looks OK, if you one pass in the returning number from the |
I think so. 👍 Sorry for the noise. |
Not sure what you're refering to exactly but the Process.id is the same as the ProcessManager id and that's all that is used to handle the processes. The real process PID is never used. |
@kittaakos I could do a commit in your PR to fix the issues tomorrow ? |
@hexa00, thank you! I will take care of it. |
Signed-off-by: Akos Kitta <kittaakos@gmail.com>
Replaced `child_process` with `RawProcess` in LS contribution. Signed-off-by: Akos Kitta <kittaakos@gmail.com>
Signed-off-by: Akos Kitta <kittaakos@gmail.com>
Signed-off-by: Akos Kitta <kittaakos@gmail.com>
Signed-off-by: Akos Kitta <kittaakos@gmail.com>
Signed-off-by: Akos Kitta <kittaakos@gmail.com>
Instead of terminating all the LS contribution processes, we shut down all processes spawned from the process manager. Signed-off-by: Akos Kitta <kittaakos@gmail.com>
On both `exit` and `error`. Signed-off-by: Akos Kitta <kittaakos@gmail.com>
I have updated the PR again. From now on, the processes will be unregistered from the manager on both CC: @hexa00 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The term lifecyle is OK now thanks!
Just a few details left.
*/ | ||
protected unregister(process: Process): void { | ||
const processLabel = this.getProcessLabel(process); | ||
this.logger.info(`Unregistering process. ${processLabel}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think info is a big much here ? debug seems more appropriate.
The user will get notified that the process is killed at the info level, however I don't think the user cares about the internal registry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same with file watching, right? We use info
for file-watching. I disagree with you, but I want this PR to get merged. I will change it to debug as you have requested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
File watching is user visible however ? while this is not... thx for the change.
const processLabel = this.getProcessLabel(process); | ||
this.logger.info(`Unregistering process. ${processLabel}`); | ||
if (!process.killed) { | ||
this.logger.info(`Ensuring process termination. ${processLabel}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dtito info
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK.
} | ||
if (this.processes.delete(process.id)) { | ||
this.deleteEmitter.fire(process.id); | ||
this.logger.info(`The process was successfully unregistered. ${processLabel}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
} | ||
}); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be removed as we discussed here: (#841 (comment)) stdin is already in the pty we don't have to write it ourselves.
|
||
this.terminal = pty.spawn( | ||
options.command, | ||
options.args, | ||
options.options); | ||
|
||
this.terminal.on('exit', this.emitOnExit.bind(this)); | ||
this.output = new TerminalReadableStream(this.terminal); | ||
this.output = new ReadableTerminalStream(this.terminal); | ||
this.input = new WritableTerminalStream(this.terminal); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this.input, the to write to a term the one should use the write method directly or we could make a stream over write...
Also changed the info log level to debug in the process manager. Signed-off-by: Akos Kitta <kittaakos@gmail.com>
We have the green builds. I am merging this PR to the master. |
Closes: #815