From cf9d47d032d7ee103e6e9aadee1af9628afac175 Mon Sep 17 00:00:00 2001 From: ashfame Date: Thu, 6 Feb 2025 20:26:56 +0400 Subject: [PATCH 1/7] set higher timeouts for import/export wp-cli commands --- src/lib/import-export/export/export-database.ts | 2 ++ .../import-export/import/importers/importer.ts | 2 +- src/lib/wp-cli-process.ts | 17 ++++++++++++++--- src/site-server.ts | 16 +++++++++++++--- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/lib/import-export/export/export-database.ts b/src/lib/import-export/export/export-database.ts index 13b5a3ce8..e5b9c2331 100644 --- a/src/lib/import-export/export/export-database.ts +++ b/src/lib/import-export/export/export-database.ts @@ -21,6 +21,7 @@ export async function exportDatabaseToFile( `sqlite export ${ tempFileName } --require=/tmp/sqlite-command/command.php`, { skipPluginsAndThemes: true, + longRunning: true, } ); @@ -88,6 +89,7 @@ export async function exportDatabaseToMultipleFiles( `sqlite export ${ fileName } --tables=${ table } --require=/tmp/sqlite-command/command.php`, { skipPluginsAndThemes: true, + longRunning: true, } ); diff --git a/src/lib/import-export/import/importers/importer.ts b/src/lib/import-export/import/importers/importer.ts index b82ec5d2a..687b55e78 100644 --- a/src/lib/import-export/import/importers/importer.ts +++ b/src/lib/import-export/import/importers/importer.ts @@ -57,7 +57,7 @@ abstract class BaseImporter extends EventEmitter implements Importer { const { stderr, exitCode } = await server.executeWpCliCommand( `sqlite import ${ sqlTempFile } --require=/tmp/sqlite-command/command.php`, // SQLite plugin requires PHP 8+ - { targetPhpVersion: DEFAULT_PHP_VERSION, skipPluginsAndThemes: true } + { targetPhpVersion: DEFAULT_PHP_VERSION, skipPluginsAndThemes: true, longRunning: true } ); if ( stderr ) { diff --git a/src/lib/wp-cli-process.ts b/src/lib/wp-cli-process.ts index 264273284..365eb9381 100644 --- a/src/lib/wp-cli-process.ts +++ b/src/lib/wp-cli-process.ts @@ -11,7 +11,8 @@ export type MessageName = 'execute'; export type WpCliResult = ReturnType< typeof executeWPCli >; export type MessageCanceled = { error: Error; canceled: boolean }; -const DEFAULT_RESPONSE_TIMEOUT = 300 * 1000; +const DEFAULT_RESPONSE_TIMEOUT = 5 * 60 * 1000; // 5min +const IMPORT_EXPORT_RESPONSE_TIMEOUT = 24 * 60 * 60 * 1000; // 24hr export default class WpCliProcess { lastMessageId = 0; @@ -54,7 +55,13 @@ export default class WpCliProcess { async execute( args: string[], - { phpVersion }: { phpVersion?: string } = {} + { + phpVersion, + longRunning = false, + }: { + phpVersion?: string; + longRunning?: boolean; + } = {} ): Promise< WpCliResult > { const message = 'execute'; const messageId = this.sendMessage( message, { @@ -62,7 +69,11 @@ export default class WpCliProcess { args, phpVersion, } ); - return await this.waitForResponse( message, messageId ); + return await this.waitForResponse( + message, + messageId, + longRunning ? IMPORT_EXPORT_RESPONSE_TIMEOUT : DEFAULT_RESPONSE_TIMEOUT + ); } async stop() { diff --git a/src/site-server.ts b/src/site-server.ts index 973d541a3..0edc99700 100644 --- a/src/site-server.ts +++ b/src/site-server.ts @@ -170,9 +170,11 @@ export class SiteServer { { targetPhpVersion, skipPluginsAndThemes = false, + longRunning = false, }: { targetPhpVersion?: string; skipPluginsAndThemes?: boolean; + longRunning?: boolean; } = {} ): Promise< WpCliResult > { const projectPath = this.details.path; @@ -199,14 +201,22 @@ export class SiteServer { } try { - return await this.wpCliExecutor.execute( wpCliArgs as string[], { phpVersion } ); + return await this.wpCliExecutor.execute( wpCliArgs as string[], { phpVersion, longRunning } ); } catch ( error ) { if ( ( error as MessageCanceled )?.canceled ) { - return { stdout: '', stderr: 'wp-cli command canceled', exitCode: 1 }; + return { + stdout: '', + stderr: 'WP-CLI command was canceled (timed out)', + exitCode: 1, + }; } Sentry.captureException( error ); - return { stdout: '', stderr: 'error when executing wp-cli command', exitCode: 1 }; + return { + stdout: '', + stderr: `Error executing WP-CLI command: ${ ( error as MessageCanceled ).error.message }`, + exitCode: 1, + }; } } From 9bc0c68f54f66d16cac48436284fd623799eec92 Mon Sep 17 00:00:00 2001 From: ashfame Date: Thu, 6 Feb 2025 20:33:27 +0400 Subject: [PATCH 2/7] adapt tests --- .../import-export/tests/export/exporters/sql-exporter.test.ts | 2 +- .../tests/import/importer/jetpack-importer.test.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/import-export/tests/export/exporters/sql-exporter.test.ts b/src/lib/import-export/tests/export/exporters/sql-exporter.test.ts index 4b00c8dd5..219b7dc4c 100644 --- a/src/lib/import-export/tests/export/exporters/sql-exporter.test.ts +++ b/src/lib/import-export/tests/export/exporters/sql-exporter.test.ts @@ -61,7 +61,7 @@ platformTestSuite( 'SqlExporter', ( { normalize } ) => { const siteServer = SiteServer.get( '123' ); expect( siteServer?.executeWpCliCommand ).toHaveBeenCalledWith( 'sqlite export studio-backup-db-export-2024-08-01-12-00-00.sql --require=/tmp/sqlite-command/command.php', - { skipPluginsAndThemes: true } + { skipPluginsAndThemes: true, longRunning: true } ); } ); diff --git a/src/lib/import-export/tests/import/importer/jetpack-importer.test.ts b/src/lib/import-export/tests/import/importer/jetpack-importer.test.ts index 4643e2007..929200c78 100644 --- a/src/lib/import-export/tests/import/importer/jetpack-importer.test.ts +++ b/src/lib/import-export/tests/import/importer/jetpack-importer.test.ts @@ -87,10 +87,12 @@ platformTestSuite( 'JetpackImporter', ( { normalize } ) => { expect( siteServer?.executeWpCliCommand ).toHaveBeenNthCalledWith( 1, expectedCommand, { targetPhpVersion: '8.2', skipPluginsAndThemes: true, + longRunning: true, } ); expect( siteServer?.executeWpCliCommand ).toHaveBeenNthCalledWith( 2, expectedCommand, { targetPhpVersion: '8.2', skipPluginsAndThemes: true, + longRunning: true, } ); const expectedUnlinkPath = normalize( From de19d669ec0ad3180c442ad9660dcafc61e71c7f Mon Sep 17 00:00:00 2001 From: ashfame Date: Thu, 6 Feb 2025 22:07:13 +0400 Subject: [PATCH 3/7] improve error logging and display when time out happens --- src/hooks/use-import-export.tsx | 7 +++++++ src/lib/import-export/import/importers/importer.ts | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/hooks/use-import-export.tsx b/src/hooks/use-import-export.tsx index fa3ca15c8..130185671 100644 --- a/src/hooks/use-import-export.tsx +++ b/src/hooks/use-import-export.tsx @@ -92,6 +92,13 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode 'The ZIP archive is invalid. Try to unpack and pack it again. If this problem persists, please contact support.' ), } ); + } else if ( + ( error as Error ).message.includes( 'WP-CLI command was canceled (timed out)' ) + ) { + await getIpcApi().showErrorMessageBox( { + title: __( 'Failed importing site' ), + message: __( 'Import process timed out. Very large import?' ), + } ); } else { await getIpcApi().showErrorMessageBox( { title: __( 'Failed importing site' ), diff --git a/src/lib/import-export/import/importers/importer.ts b/src/lib/import-export/import/importers/importer.ts index 687b55e78..89dadcb1b 100644 --- a/src/lib/import-export/import/importers/importer.ts +++ b/src/lib/import-export/import/importers/importer.ts @@ -61,11 +61,11 @@ abstract class BaseImporter extends EventEmitter implements Importer { ); if ( stderr ) { - console.error( `Warning during import of ${ sqlFile }:`, stderr ); + console.error( `Error during import of ${ sqlFile }:`, stderr ); } if ( exitCode ) { - throw new Error( 'Database import failed' ); + throw new Error( 'Database import failed: ' + stderr ); } } finally { await this.safelyDeletePath( tmpPath ); From ee5c85090617ffc67857b220ceba84804876dd45 Mon Sep 17 00:00:00 2001 From: ashfame Date: Mon, 17 Feb 2025 10:59:33 +0400 Subject: [PATCH 4/7] change error message copy & change import/export timeout to 6hrs --- src/hooks/use-import-export.tsx | 9 +++++++-- src/lib/wp-cli-process.ts | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/hooks/use-import-export.tsx b/src/hooks/use-import-export.tsx index 130185671..e40bf8a88 100644 --- a/src/hooks/use-import-export.tsx +++ b/src/hooks/use-import-export.tsx @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/electron/renderer'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { createContext, useMemo, useState, useCallback, useContext } from 'react'; import { useIpcListener } from 'src/hooks/use-ipc-listener'; import { useSiteDetails } from 'src/hooks/use-site-details'; @@ -97,7 +97,12 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode ) { await getIpcApi().showErrorMessageBox( { title: __( 'Failed importing site' ), - message: __( 'Import process timed out. Very large import?' ), + message: sprintf( + __( + 'The import process timed out after %d hours, which can occur when processing very large imports. If the issue persists, please contact support.' + ), + 6 + ), } ); } else { await getIpcApi().showErrorMessageBox( { diff --git a/src/lib/wp-cli-process.ts b/src/lib/wp-cli-process.ts index 365eb9381..7fb75de1c 100644 --- a/src/lib/wp-cli-process.ts +++ b/src/lib/wp-cli-process.ts @@ -12,7 +12,7 @@ export type WpCliResult = ReturnType< typeof executeWPCli >; export type MessageCanceled = { error: Error; canceled: boolean }; const DEFAULT_RESPONSE_TIMEOUT = 5 * 60 * 1000; // 5min -const IMPORT_EXPORT_RESPONSE_TIMEOUT = 24 * 60 * 60 * 1000; // 24hr +const IMPORT_EXPORT_RESPONSE_TIMEOUT = 6 * 60 * 60 * 1000; // 6hr export default class WpCliProcess { lastMessageId = 0; From b427255a31c2c91a8565e01312247bf064510e06 Mon Sep 17 00:00:00 2001 From: ashfame Date: Mon, 17 Feb 2025 14:48:31 +0400 Subject: [PATCH 5/7] remove explicitly specifying longRunning command and infer from command itself --- .../import-export/export/export-database.ts | 2 -- .../import-export/import/importers/importer.ts | 2 +- .../export/exporters/sql-exporter.test.ts | 2 +- .../import/importer/jetpack-importer.test.ts | 2 -- src/lib/wp-cli-process.ts | 18 ++++++------------ src/site-server.ts | 4 +--- 6 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/lib/import-export/export/export-database.ts b/src/lib/import-export/export/export-database.ts index e5b9c2331..13b5a3ce8 100644 --- a/src/lib/import-export/export/export-database.ts +++ b/src/lib/import-export/export/export-database.ts @@ -21,7 +21,6 @@ export async function exportDatabaseToFile( `sqlite export ${ tempFileName } --require=/tmp/sqlite-command/command.php`, { skipPluginsAndThemes: true, - longRunning: true, } ); @@ -89,7 +88,6 @@ export async function exportDatabaseToMultipleFiles( `sqlite export ${ fileName } --tables=${ table } --require=/tmp/sqlite-command/command.php`, { skipPluginsAndThemes: true, - longRunning: true, } ); diff --git a/src/lib/import-export/import/importers/importer.ts b/src/lib/import-export/import/importers/importer.ts index 89dadcb1b..cb0d8c288 100644 --- a/src/lib/import-export/import/importers/importer.ts +++ b/src/lib/import-export/import/importers/importer.ts @@ -57,7 +57,7 @@ abstract class BaseImporter extends EventEmitter implements Importer { const { stderr, exitCode } = await server.executeWpCliCommand( `sqlite import ${ sqlTempFile } --require=/tmp/sqlite-command/command.php`, // SQLite plugin requires PHP 8+ - { targetPhpVersion: DEFAULT_PHP_VERSION, skipPluginsAndThemes: true, longRunning: true } + { targetPhpVersion: DEFAULT_PHP_VERSION, skipPluginsAndThemes: true } ); if ( stderr ) { diff --git a/src/lib/import-export/tests/export/exporters/sql-exporter.test.ts b/src/lib/import-export/tests/export/exporters/sql-exporter.test.ts index 219b7dc4c..4b00c8dd5 100644 --- a/src/lib/import-export/tests/export/exporters/sql-exporter.test.ts +++ b/src/lib/import-export/tests/export/exporters/sql-exporter.test.ts @@ -61,7 +61,7 @@ platformTestSuite( 'SqlExporter', ( { normalize } ) => { const siteServer = SiteServer.get( '123' ); expect( siteServer?.executeWpCliCommand ).toHaveBeenCalledWith( 'sqlite export studio-backup-db-export-2024-08-01-12-00-00.sql --require=/tmp/sqlite-command/command.php', - { skipPluginsAndThemes: true, longRunning: true } + { skipPluginsAndThemes: true } ); } ); diff --git a/src/lib/import-export/tests/import/importer/jetpack-importer.test.ts b/src/lib/import-export/tests/import/importer/jetpack-importer.test.ts index 929200c78..4643e2007 100644 --- a/src/lib/import-export/tests/import/importer/jetpack-importer.test.ts +++ b/src/lib/import-export/tests/import/importer/jetpack-importer.test.ts @@ -87,12 +87,10 @@ platformTestSuite( 'JetpackImporter', ( { normalize } ) => { expect( siteServer?.executeWpCliCommand ).toHaveBeenNthCalledWith( 1, expectedCommand, { targetPhpVersion: '8.2', skipPluginsAndThemes: true, - longRunning: true, } ); expect( siteServer?.executeWpCliCommand ).toHaveBeenNthCalledWith( 2, expectedCommand, { targetPhpVersion: '8.2', skipPluginsAndThemes: true, - longRunning: true, } ); const expectedUnlinkPath = normalize( diff --git a/src/lib/wp-cli-process.ts b/src/lib/wp-cli-process.ts index 7fb75de1c..3d24500dd 100644 --- a/src/lib/wp-cli-process.ts +++ b/src/lib/wp-cli-process.ts @@ -55,13 +55,7 @@ export default class WpCliProcess { async execute( args: string[], - { - phpVersion, - longRunning = false, - }: { - phpVersion?: string; - longRunning?: boolean; - } = {} + { phpVersion }: { phpVersion?: string } = {} ): Promise< WpCliResult > { const message = 'execute'; const messageId = this.sendMessage( message, { @@ -69,11 +63,11 @@ export default class WpCliProcess { args, phpVersion, } ); - return await this.waitForResponse( - message, - messageId, - longRunning ? IMPORT_EXPORT_RESPONSE_TIMEOUT : DEFAULT_RESPONSE_TIMEOUT - ); + const timeout = + args[ 0 ] === 'sqlite' && ( args[ 1 ] === 'import' || args[ 1 ] === 'export' ) + ? IMPORT_EXPORT_RESPONSE_TIMEOUT + : DEFAULT_RESPONSE_TIMEOUT; + return await this.waitForResponse( message, messageId, timeout ); } async stop() { diff --git a/src/site-server.ts b/src/site-server.ts index 0edc99700..c40ce3bb5 100644 --- a/src/site-server.ts +++ b/src/site-server.ts @@ -170,11 +170,9 @@ export class SiteServer { { targetPhpVersion, skipPluginsAndThemes = false, - longRunning = false, }: { targetPhpVersion?: string; skipPluginsAndThemes?: boolean; - longRunning?: boolean; } = {} ): Promise< WpCliResult > { const projectPath = this.details.path; @@ -201,7 +199,7 @@ export class SiteServer { } try { - return await this.wpCliExecutor.execute( wpCliArgs as string[], { phpVersion, longRunning } ); + return await this.wpCliExecutor.execute( wpCliArgs as string[], { phpVersion } ); } catch ( error ) { if ( ( error as MessageCanceled )?.canceled ) { return { From c33f03cff0379de678044aa8034dcab339169782 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Date: Mon, 17 Feb 2025 21:09:16 +0400 Subject: [PATCH 6/7] Update src/lib/wp-cli-process.ts Co-authored-by: Antonio Sejas --- src/lib/wp-cli-process.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/wp-cli-process.ts b/src/lib/wp-cli-process.ts index 3d24500dd..5d0347997 100644 --- a/src/lib/wp-cli-process.ts +++ b/src/lib/wp-cli-process.ts @@ -64,7 +64,7 @@ export default class WpCliProcess { phpVersion, } ); const timeout = - args[ 0 ] === 'sqlite' && ( args[ 1 ] === 'import' || args[ 1 ] === 'export' ) + args[ 0 ] === 'sqlite' && [ 'import', 'export' ].includes( args[ 1 ] ) ? IMPORT_EXPORT_RESPONSE_TIMEOUT : DEFAULT_RESPONSE_TIMEOUT; return await this.waitForResponse( message, messageId, timeout ); From 01ac264df42ecf419a019a99040d2276d53decbc Mon Sep 17 00:00:00 2001 From: ashfame Date: Mon, 17 Feb 2025 21:18:52 +0400 Subject: [PATCH 7/7] move WP-CLI response timeout constants to src/constants.ts --- src/constants.ts | 6 ++++++ src/hooks/use-import-export.tsx | 3 ++- src/lib/wp-cli-process.ts | 7 ++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 2381292fd..81ed06176 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -49,3 +49,9 @@ export const DAY_MS = HOUR_MS * 24; // IMPORTANT: When updating this value, we need to update the string located in `AIClearHistoryReminder` component. // Reference: https://github.com/Automattic/studio/blob/3dd5c58cdb7998e458d191e508e8e859177225a9/src/components/ai-clear-history-reminder.tsx#L78 export const CLEAR_HISTORY_REMINDER_TIME = 2 * HOUR_MS; // In milliseconds + +// WP-CLI +export const WP_CLI_DEFAULT_RESPONSE_TIMEOUT = 5 * 60 * 1000; // 5min +export const WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT_IN_HRS = 6; +export const WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT = + WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT_IN_HRS * 60 * 60 * 1000; // 6hr diff --git a/src/hooks/use-import-export.tsx b/src/hooks/use-import-export.tsx index e40bf8a88..8ac998499 100644 --- a/src/hooks/use-import-export.tsx +++ b/src/hooks/use-import-export.tsx @@ -1,6 +1,7 @@ import * as Sentry from '@sentry/electron/renderer'; import { __, sprintf } from '@wordpress/i18n'; import { createContext, useMemo, useState, useCallback, useContext } from 'react'; +import { WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT_IN_HRS } from 'src/constants'; import { useIpcListener } from 'src/hooks/use-ipc-listener'; import { useSiteDetails } from 'src/hooks/use-site-details'; import { getIpcApi } from 'src/lib/get-ipc-api'; @@ -101,7 +102,7 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode __( 'The import process timed out after %d hours, which can occur when processing very large imports. If the issue persists, please contact support.' ), - 6 + WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT_IN_HRS ), } ); } else { diff --git a/src/lib/wp-cli-process.ts b/src/lib/wp-cli-process.ts index 5d0347997..51eb56578 100644 --- a/src/lib/wp-cli-process.ts +++ b/src/lib/wp-cli-process.ts @@ -1,5 +1,9 @@ import { app, utilityProcess, UtilityProcess } from 'electron'; import * as Sentry from '@sentry/electron/renderer'; +import { + WP_CLI_DEFAULT_RESPONSE_TIMEOUT as DEFAULT_RESPONSE_TIMEOUT, + WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT as IMPORT_EXPORT_RESPONSE_TIMEOUT, +} from 'src/constants'; import { executeWPCli } from 'vendor/wp-now/src/execute-wp-cli'; // This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack @@ -11,9 +15,6 @@ export type MessageName = 'execute'; export type WpCliResult = ReturnType< typeof executeWPCli >; export type MessageCanceled = { error: Error; canceled: boolean }; -const DEFAULT_RESPONSE_TIMEOUT = 5 * 60 * 1000; // 5min -const IMPORT_EXPORT_RESPONSE_TIMEOUT = 6 * 60 * 60 * 1000; // 6hr - export default class WpCliProcess { lastMessageId = 0; process?: UtilityProcess;