Skip to content
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

Connect My Computer: Join cluster #29479

Merged
merged 32 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5376e1a
Add `generateAgentConfigPaths` function that creates config path base…
gzdunek Jul 24, 2023
ca6a5b2
Add functions to run agent and subscribe to its events
gzdunek Jul 24, 2023
da19a68
Clear attempts when restarting the process
gzdunek Jul 24, 2023
ea2c34f
Run the agent from the UI and remove node token
gzdunek Jul 24, 2023
8da756d
Show errors from the process in the setup UI
gzdunek Jul 24, 2023
904e253
Refactor reporting errors from the agent process
gzdunek Jul 25, 2023
ff265fa
Add `isLocalBuild`
gzdunek Jul 25, 2023
7ba35c4
Join arguments with space when logging
gzdunek Jul 25, 2023
40a6231
Add `killProcess` function that handles process closing
gzdunek Jul 26, 2023
1e3a5bd
Spawn a real process in `agentRunner` tests
gzdunek Jul 26, 2023
b32f405
Keep `agentRunner` files in a single directory
gzdunek Jul 26, 2023
b94bd7e
Catch errors from `deleteToken`
gzdunek Jul 26, 2023
72eaf48
Remove `env: process.env`
gzdunek Jul 26, 2023
50b01ef
Match on "access denied" when checking error from `deleteToken`
gzdunek Jul 28, 2023
a1ca16f
Reject when an agent process fails to start in test
gzdunek Jul 28, 2023
cdc0e8f
Match only on "ENOENT"
gzdunek Jul 28, 2023
00234d7
Correct test name ("SIGTERM" -> "SIGKILL")
gzdunek Jul 28, 2023
ec73591
Test terminating the process and then trying to kill it
gzdunek Jul 28, 2023
0a6a5e6
Wait for "exit" event instead of "close"
gzdunek Jul 28, 2023
103483b
Rename `killProcess` to `terminateWithTimeout`
gzdunek Jul 28, 2023
53e2cc5
Add `getAgentState` method to synchronously get the agent state
gzdunek Jul 31, 2023
040a50f
Remove space before new line
gzdunek Jul 31, 2023
ecdf232
Simplify the logic in `AgentRunner`
gzdunek Jul 31, 2023
086bc16
Fix TS error
gzdunek Jul 31, 2023
c80bd6e
Merge branch 'master' into gzdunek/cmc-run-agent
gzdunek Aug 1, 2023
0eebf95
Do not send agent updates to a destroyed window
gzdunek Aug 1, 2023
aef7d7e
Add logging cluster URI and updated state
gzdunek Aug 1, 2023
d3b858a
Catch errors that are thrown while spawning the process
gzdunek Aug 2, 2023
a57477a
Strip ANSI codes
gzdunek Aug 2, 2023
d80094d
Add `exitedSuccessfully` property to `exited` state, so we won't have…
gzdunek Aug 2, 2023
0b55dfd
Move `strip-ansi-stream` to `dependencies`
gzdunek Aug 2, 2023
5a466e2
Fix license
gzdunek Aug 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 67 additions & 59 deletions web/packages/teleterm/src/mainProcess/agentRunner.test.ts
ravicious marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import childProcess, { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
import { PassThrough } from 'node:stream';
import path from 'node:path';

import Logger, { NullService } from 'teleterm/logger';
import { RootClusterUri } from 'teleterm/ui/uri';
Expand All @@ -26,72 +24,56 @@ import { makeRuntimeSettings } from './fixtures/mocks';
import { AgentRunner } from './agentRunner';
import { AgentProcessState } from './types';

jest.mock('node:child_process');

let eventEmitter: EventEmitter;
let childProcessMock: ChildProcess;
beforeEach(() => {
Logger.init(new NullService());

eventEmitter = new EventEmitter();
childProcessMock = {
stderr: new PassThrough(),
once: (event, listener) => {
eventEmitter.once(event, listener);
return this;
},
on: (event, listener) => {
eventEmitter.on(event, listener);
return this;
},
off: (event, listener) => {
eventEmitter.off(event, listener);
return this;
},
kill: jest.fn().mockImplementation(() => {
eventEmitter.emit('exit', 0);
}),
} as unknown as ChildProcess;

jest.spyOn(childProcess, 'spawn').mockReturnValue(childProcessMock);
});

const userDataDir = '/Users/test/Application Data/Teleport Connect';
const agentBinaryPath = '/Users/test/Caches/Teleport Connect/teleport/teleport';
const agentBinaryPath = path.join(__dirname, 'agentTestProcess.mjs');
const rootClusterUri: RootClusterUri = '/clusters/cluster.local';

test('agent process starts with correct arguments', () => {
test('agent process starts with correct arguments', async () => {
const agentRunner = new AgentRunner(
makeRuntimeSettings({
agentBinaryPath,
userDataDir,
}),
() => {}
);
agentRunner.start(rootClusterUri);

expect(childProcess.spawn).toHaveBeenCalledWith(
agentBinaryPath,
['start', `--config=${userDataDir}/agents/cluster.local/config.yaml`],
expect.anything()
);
try {
const agentProcess = await agentRunner.start(rootClusterUri);

expect(agentProcess.spawnargs).toEqual([
agentBinaryPath,
'start',
`--config=${userDataDir}/agents/cluster.local/config.yaml`,
]);
} finally {
await agentRunner.killAll();
}
});

test('previous agent process is killed when a new one is started', () => {
test('previous agent process is killed when a new one is started', async () => {
const agentRunner = new AgentRunner(
makeRuntimeSettings({
agentBinaryPath,
userDataDir,
}),
() => {}
);
agentRunner.start(rootClusterUri);
agentRunner.start(rootClusterUri);

expect(childProcessMock.kill).toHaveBeenCalledWith('SIGKILL');
try {
const firstProcess = await agentRunner.start(rootClusterUri);
await agentRunner.start(rootClusterUri);

expect(firstProcess.killed).toBeTruthy();
} finally {
await agentRunner.killAll();
}
});

test('status updates are sent', () => {
test('status updates are sent on a successful start', async () => {
const updateSender = jest.fn();
const agentRunner = new AgentRunner(
makeRuntimeSettings({
Expand All @@ -101,21 +83,47 @@ test('status updates are sent', () => {
updateSender
);

agentRunner.start(rootClusterUri);
expect(updateSender).toHaveBeenCalledWith(rootClusterUri, {
status: 'running',
} as AgentProcessState);

const error = new Error('unknown error');
eventEmitter.emit('error', error);
expect(updateSender).toHaveBeenCalledWith(rootClusterUri, {
status: 'error',
message: `${error}`,
} as AgentProcessState);

agentRunner.kill(rootClusterUri);
expect(updateSender).toHaveBeenCalledWith(rootClusterUri, {
status: 'exited',
code: 0,
} as AgentProcessState);
try {
const agentProcess = await agentRunner.start(rootClusterUri);
await new Promise(resolve => agentProcess.on('spawn', resolve));
gzdunek marked this conversation as resolved.
Show resolved Hide resolved
expect(updateSender).toHaveBeenCalledWith(rootClusterUri, {
status: 'running',
} as AgentProcessState);

await agentRunner.kill(rootClusterUri);
expect(updateSender).toHaveBeenCalledWith(rootClusterUri, {
status: 'exited',
code: null,
signal: 'SIGTERM',
} as AgentProcessState);

expect(updateSender).toHaveBeenCalledTimes(2);
} finally {
await agentRunner.killAll();
}
});

test('status updates are sent on a failed start', async () => {
const updateSender = jest.fn();
const nonExisingPath = path.join(__dirname, 'agentTestProcess-nonExisting.mjs');
const agentRunner = new AgentRunner(
makeRuntimeSettings({
agentBinaryPath: nonExisingPath,
userDataDir,
}),
updateSender
);

try {
const agentProcess = await agentRunner.start(rootClusterUri);
await new Promise(resolve => agentProcess.on('error', resolve));

expect(updateSender).toHaveBeenCalledTimes(1);
expect(updateSender).toHaveBeenCalledWith(rootClusterUri, {
status: 'error',
message: `Error: spawn ${nonExisingPath} ENOENT`,
gzdunek marked this conversation as resolved.
Show resolved Hide resolved
} as AgentProcessState);
} finally {
await agentRunner.killAll();
}
});
22 changes: 15 additions & 7 deletions web/packages/teleterm/src/mainProcess/agentRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class AgentRunner {
* Starts a new agent process.
* If an existing process exists for the given root cluster, the old one will be killed.
*/
async start(rootClusterUri: RootClusterUri): Promise<void> {
async start(rootClusterUri: RootClusterUri): Promise<ChildProcess> {
if (this.agentProcesses.has(rootClusterUri)) {
await this.kill(rootClusterUri);
this.logger.warn(`Killed agent process for ${rootClusterUri}`);
Expand All @@ -69,12 +69,10 @@ export class AgentRunner {
env: process.env,
});

this.sendProcessState(rootClusterUri, {
status: 'running',
});

this.addListeners(rootClusterUri, agentProcess);
this.agentProcesses.set(rootClusterUri, agentProcess);

return agentProcess;
}

async kill(rootClusterUri: RootClusterUri): Promise<void> {
Expand Down Expand Up @@ -104,7 +102,15 @@ export class AgentRunner {
stderrOutput = limitProcessOutputLines(stderrOutput);
});

const spawnHandler = () => {
this.sendProcessState(rootClusterUri, {
status: 'running',
});
};

const errorHandler = (error: Error) => {
process.off('spawn', spawnHandler);

this.sendProcessState(rootClusterUri, {
status: 'error',
message: `${error}`,
Expand All @@ -115,17 +121,19 @@ export class AgentRunner {
code: number | null,
signal: NodeJS.Signals | null
) => {
// Remove error handler when the process exits.
// Remove handlers when the process exits.
process.off('error', errorHandler);
process.off('spawn', spawnHandler);

this.sendProcessState(rootClusterUri, {
status: 'exited',
code,
signal,
stackTrace: code !== 0 ? stderrOutput : undefined,
stackTrace: signal !== 'SIGTERM' ? stderrOutput : undefined,
});
};

process.once('spawn', spawnHandler);
process.once('error', errorHandler);
process.once('exit', exitHandler);
}
Expand Down
21 changes: 21 additions & 0 deletions web/packages/teleterm/src/mainProcess/agentTestProcess.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env node
/**
* Copyright 2023 Gravitational, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/


import { setTimeout } from 'node:timers/promises';

await setTimeout(10_000);