Skip to content

Commit 440f54f

Browse files
authored
Merge pull request #213 from parthlambdatest/Dot-4568
[Dot-4568] start, stop and ping implementation in CLI
2 parents ec70b62 + 2a23803 commit 440f54f

File tree

12 files changed

+249
-11
lines changed

12 files changed

+249
-11
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lambdatest/smartui-cli",
3-
"version": "4.0.20",
3+
"version": "4.0.21",
44
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
55
"files": [
66
"dist/**/*"

src/commander/commander.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import capture from './capture.js'
55
import upload from './upload.js'
66
import { version } from '../../package.json'
77
import { uploadFigma, uploadWebFigmaCommand } from './uploadFigma.js'
8+
import startServer from './server.js';
9+
import stopServer from './stopServer.js'
10+
import ping from './ping.js'
811

912
const program = new Command();
1013

@@ -18,11 +21,14 @@ program
1821
.addCommand(configWeb)
1922
.addCommand(configStatic)
2023
.addCommand(upload)
24+
.addCommand(startServer)
25+
.addCommand(stopServer)
26+
.addCommand(ping)
2127
.addCommand(configFigma)
2228
.addCommand(uploadFigma)
2329
.addCommand(configWebFigma)
2430
.addCommand(uploadWebFigmaCommand)
2531

26-
32+
2733

2834
export default program;

src/commander/ping.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Command } from 'commander';
2+
import axios from 'axios';
3+
import chalk from 'chalk'
4+
5+
function getSmartUIServerAddress() {
6+
const serverAddress = process.env.SMARTUI_SERVER_ADDRESS || 'http://localhost:49152';
7+
return serverAddress;
8+
}
9+
10+
const command = new Command();
11+
12+
command
13+
.name('exec:ping')
14+
.description('Ping the SmartUI server to check if it is running')
15+
.action(async function(this: Command) {
16+
try {
17+
console.log(chalk.yellow("Pinging server..."));
18+
const serverAddress = getSmartUIServerAddress();
19+
console.log(chalk.yellow(`Pinging server at ${serverAddress} from terminal...`));
20+
21+
// Send GET request to the /ping endpoint
22+
const response = await axios.get(`${serverAddress}/ping`, { timeout: 15000 });
23+
24+
// Log the response from the server
25+
if (response.status === 200) {
26+
console.log(chalk.green('SmartUI Server is running'));
27+
console.log(chalk.green(`Response: ${JSON.stringify(response.data)}`)); // Log response data if needed
28+
} else {
29+
console.log(chalk.red('Failed to reach the server'));
30+
}
31+
} catch (error: any) {
32+
// Handle any errors during the HTTP request
33+
if (error.code === 'ECONNABORTED') {
34+
console.error(chalk.red('Error: SmartUI server did not respond in 15 seconds'));
35+
} else {
36+
console.error(chalk.red('SmartUI server is not running'));
37+
}
38+
}
39+
});
40+
41+
export default command;

src/commander/server.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Command } from 'commander';
2+
import { Context } from '../types.js';
3+
import { color, Listr, ListrDefaultRendererLogLevels } from 'listr2';
4+
import startServer from '../tasks/startServer.js';
5+
import auth from '../tasks/auth.js';
6+
import ctxInit from '../lib/ctx.js';
7+
import getGitInfo from '../tasks/getGitInfo.js';
8+
import createBuild from '../tasks/createBuild.js';
9+
import snapshotQueue from '../lib/snapshotQueue.js';
10+
import { startPolling, startPingPolling } from '../lib/utils.js';
11+
12+
const command = new Command();
13+
14+
command
15+
.name('exec:start')
16+
.description('Start SmartUI server')
17+
.option('-P, --port <number>', 'Port number for the server')
18+
.option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., <filename>.json')
19+
.option('--buildName <string>', 'Specify the build name')
20+
.action(async function(this: Command) {
21+
const options = command.optsWithGlobals();
22+
if (options.buildName === '') {
23+
console.log(`Error: The '--buildName' option cannot be an empty string.`);
24+
process.exit(1);
25+
}
26+
let ctx: Context = ctxInit(command.optsWithGlobals());
27+
ctx.snapshotQueue = new snapshotQueue(ctx);
28+
ctx.totalSnapshots = 0
29+
ctx.isStartExec = true
30+
31+
let tasks = new Listr<Context>(
32+
[
33+
auth(ctx),
34+
startServer(ctx),
35+
getGitInfo(ctx),
36+
createBuild(ctx),
37+
38+
],
39+
{
40+
rendererOptions: {
41+
icon: {
42+
[ListrDefaultRendererLogLevels.OUTPUT]: `→`
43+
},
44+
color: {
45+
[ListrDefaultRendererLogLevels.OUTPUT]: color.gray
46+
}
47+
}
48+
}
49+
);
50+
51+
try {
52+
await tasks.run(ctx);
53+
startPingPolling(ctx);
54+
if (ctx.options.fetchResults) {
55+
startPolling(ctx);
56+
}
57+
58+
59+
} catch (error) {
60+
console.error('Error during server execution:', error);
61+
}
62+
});
63+
64+
export default command;

src/commander/stopServer.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Command } from 'commander';
2+
import axios from 'axios'; // Import axios for HTTP requests
3+
import chalk from 'chalk'
4+
5+
const command = new Command();
6+
7+
function getSmartUIServerAddress() {
8+
const serverAddress = process.env.SMARTUI_SERVER_ADDRESS || 'http://localhost:49152';
9+
return serverAddress;
10+
}
11+
12+
command
13+
.name('exec:stop')
14+
.description('Stop the SmartUI server')
15+
.action(async function(this: Command) {
16+
try {
17+
const serverAddress = getSmartUIServerAddress();
18+
console.log(chalk.yellow(`Stopping server at ${serverAddress} from terminal...`));
19+
20+
// Send POST request to the /stop endpoint with the correct headers
21+
const response = await axios.post(`${serverAddress}/stop`, {}, {
22+
headers: {
23+
'Content-Type': 'application/json' // Ensure the correct Content-Type header
24+
}
25+
});
26+
27+
// Log the response from the server
28+
if (response.status === 200) {
29+
console.log(chalk.green('Server stopped successfully'));
30+
console.log(chalk.green(`Response: ${JSON.stringify(response.data)}`)); // Log response data if needed
31+
} else {
32+
console.log(chalk.red('Failed to stop server'));
33+
}
34+
} catch (error: any) {
35+
// Handle any errors during the HTTP request
36+
console.error(chalk.red('Error while stopping server'));
37+
}
38+
});
39+
40+
export default command;

src/lib/ctx.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export default (options: Record<string, string>): Context => {
126126
fetchResultsFileName: fetchResultsFileObj,
127127
},
128128
cliVersion: version,
129-
totalSnapshots: -1
129+
totalSnapshots: -1,
130+
isStartExec: false
130131
}
131132
}

src/lib/httpClient.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,15 @@ export default class httpClient {
8484
}
8585
}
8686

87-
createBuild(git: Git, config: any, log: Logger, buildName: string) {
87+
createBuild(git: Git, config: any, log: Logger, buildName: string, isStartExec: boolean) {
8888
return this.request({
8989
url: '/build',
9090
method: 'POST',
9191
data: {
9292
git,
9393
config,
94-
buildName
94+
buildName,
95+
isStartExec
9596
}
9697
}, log)
9798
}
@@ -102,7 +103,18 @@ export default class httpClient {
102103
method: 'GET',
103104
params: { buildId, baseline }
104105
}, log);
105-
}
106+
}
107+
108+
ping(buildId: string, log: Logger) {
109+
return this.request({
110+
url: '/build/ping',
111+
method: 'POST',
112+
data: {
113+
buildId: buildId
114+
}
115+
}, log);
116+
}
117+
106118

107119
finalizeBuild(buildId: string, totalSnapshots: number, log: Logger) {
108120
let params: Record<string, string | number> = {buildId};

src/lib/server.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,49 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
4848
return reply.code(replyCode).send(replyBody);
4949
});
5050

51+
server.post('/stop', opts, async (_, reply) => {
52+
let replyCode: number;
53+
let replyBody: Record<string, any>;
54+
try {
55+
if(ctx.config.delayedUpload){
56+
ctx.log.debug("started after processing because of delayedUpload")
57+
ctx.snapshotQueue?.startProcessingfunc()
58+
}
59+
await new Promise((resolve) => {
60+
const intervalId = setInterval(() => {
61+
if (ctx.snapshotQueue?.isEmpty() && !ctx.snapshotQueue?.isProcessing()) {
62+
clearInterval(intervalId);
63+
resolve();
64+
}
65+
}, 1000);
66+
})
67+
await ctx.client.finalizeBuild(ctx.build.id, ctx.totalSnapshots, ctx.log);
68+
await ctx.browser?.close();
69+
if (ctx.server){
70+
ctx.server.close();
71+
}
72+
let resp = await ctx.client.getS3PreSignedURL(ctx);
73+
await ctx.client.uploadLogs(ctx, resp.data.url);
74+
replyCode = 200;
75+
replyBody = { data: { message: "success", type: "DELETE" } };
76+
} catch (error: any) {
77+
ctx.log.debug(error);
78+
ctx.log.debug(`stop endpoint failed; ${error}`);
79+
replyCode = 500;
80+
replyBody = { error: { message: error.message } };
81+
}
82+
83+
// Step 5: Return the response
84+
return reply.code(replyCode).send(replyBody);
85+
});
86+
87+
// Add /ping route to check server status
88+
server.get('/ping', opts, (_, reply) => {
89+
reply.code(200).send({ status: 'Server is running', version: ctx.cliVersion });
90+
});
91+
92+
93+
5194
await server.listen({ port: ctx.options.port });
5295
// store server's address for SDK
5396
let { port } = server.addresses()[0];

src/lib/utils.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,17 @@ import { Context } from '../types.js'
33
import { chromium, firefox, webkit, Browser } from '@playwright/test'
44
import constants from './constants.js';
55
import chalk from 'chalk';
6+
import axios from 'axios';
7+
8+
import { globalAgent } from 'http';
9+
import { promisify } from 'util'
10+
const sleep = promisify(setTimeout);
611

712
let isPollingActive = false;
13+
let globalContext: Context;
14+
export const setGlobalContext = (newContext: Context): void => {
15+
globalContext = newContext;
16+
};
817

918
export function delDir(dir: string): void {
1019
if (fs.existsSync(dir)) {
@@ -207,7 +216,7 @@ export function getRenderViewportsForOptions(options: any): Array<Record<string,
207216
}
208217

209218
// Global SIGINT handler
210-
process.on('SIGINT', () => {
219+
process.on('SIGINT', async () => {
211220
if (isPollingActive) {
212221
console.log('Fetching results interrupted. Exiting...');
213222
isPollingActive = false;
@@ -218,7 +227,7 @@ process.on('SIGINT', () => {
218227
});
219228

220229
// Background polling function
221-
export async function startPolling(ctx: Context, task: any): Promise<void> {
230+
export async function startPolling(ctx: Context): Promise<void> {
222231
ctx.log.info('Fetching results in progress....');
223232
isPollingActive = true;
224233

@@ -294,4 +303,25 @@ export async function startPolling(ctx: Context, task: any): Promise<void> {
294303
isPollingActive = false;
295304
}
296305
}, 5000);
297-
}
306+
}
307+
308+
export async function startPingPolling(ctx: Context): Promise<void> {
309+
try {
310+
ctx.log.debug('Sending initial ping to server...');
311+
await ctx.client.ping(ctx.build.id, ctx.log);
312+
ctx.log.debug('Initial ping sent successfully.');
313+
} catch (error: any) {
314+
ctx.log.error(`Error during initial ping: ${error.message}`);
315+
}
316+
317+
setInterval(async () => {
318+
try {
319+
ctx.log.debug('Sending ping to server...');
320+
await ctx.client.ping(ctx.build.id, ctx.log);
321+
ctx.log.debug('Ping sent successfully.');
322+
} catch (error: any) {
323+
ctx.log.error(`Error during ping polling: ${error.message}`);
324+
}
325+
}, 10 * 60 * 1000);
326+
}
327+

src/tasks/createBuild.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRen
1010
updateLogContext({task: 'createBuild'});
1111

1212
try {
13-
let resp = await ctx.client.createBuild(ctx.git, ctx.config, ctx.log, ctx.build.name);
13+
let resp = await ctx.client.createBuild(ctx.git, ctx.config, ctx.log, ctx.build.name, ctx.isStartExec);
1414
ctx.build = {
1515
id: resp.data.buildId,
1616
name: resp.data.buildName,

0 commit comments

Comments
 (0)