diff --git a/cli/commands/site/set.ts b/cli/commands/site/set.ts index 6ba393037a..024f4bba98 100644 --- a/cli/commands/site/set.ts +++ b/cli/commands/site/set.ts @@ -3,6 +3,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { DEFAULT_WORDPRESS_VERSION, MINIMUM_WORDPRESS_VERSION } from 'common/constants'; import { getDomainNameValidationError } from 'common/lib/domains'; import { arePathsEqual } from 'common/lib/fs-utils'; +import { siteNeedsRestart } from 'common/lib/site-needs-restart'; import { getWordPressVersionUrl, isValidWordPressVersion, @@ -68,7 +69,7 @@ export async function runCommand( throw new LoggerError( __( 'Site name cannot be empty.' ) ); } - if ( xdebug !== undefined && ! ( await isXdebugBetaEnabled() ) ) { + if ( xdebug === true && ! ( await isXdebugBetaEnabled() ) ) { throw new LoggerError( __( 'Xdebug support is a beta feature. Enable it in Studio settings first.' ) ); @@ -81,7 +82,7 @@ export async function runCommand( const initialAppdata = await readAppdata(); - if ( domain !== undefined ) { + if ( domain ) { const existingDomainNames = initialAppdata.sites .filter( ( s ) => s.id !== site.id ) .map( ( s ) => s.customDomain ) @@ -129,7 +130,13 @@ export async function runCommand( ); } - const needsRestart = domainChanged || httpsChanged || phpChanged || wpChanged || xdebugChanged; + const needsRestart = siteNeedsRestart( { + domainChanged, + httpsChanged, + phpChanged, + wpChanged, + xdebugChanged, + } ); const oldDomain = site.customDomain; try { @@ -144,7 +151,7 @@ export async function runCommand( foundSite.name = name!; } if ( domainChanged ) { - foundSite.customDomain = domain; + foundSite.customDomain = domain || undefined; } if ( httpsChanged ) { foundSite.enableHttps = https; @@ -185,7 +192,6 @@ export async function runCommand( const phpVersion = validatePhpVersion( site.phpVersion ); const zipUrl = getWordPressVersionUrl( wp ); - // Use the correct site URL to avoid corrupting WordPress URL options let siteUrl: string | undefined; if ( site.customDomain ) { const protocol = site.enableHttps ? 'https' : 'http'; @@ -307,7 +313,6 @@ export const registerCommand = ( yargs: StudioArgv ) => { wp: argv.wp, xdebug: argv.xdebug, } ); - // WP-CLI leaves handles open, so we need to explicitly exit // See: cli/lib/run-wp-cli-command.ts FIXME comment if ( result.usedWpCli ) { process.exit( 0 ); @@ -319,6 +324,7 @@ export const registerCommand = ( yargs: StudioArgv ) => { const loggerError = new LoggerError( __( 'Failed to configure site' ), error ); logger.reportError( loggerError ); } + process.exit( 1 ); } }, } ); diff --git a/cli/lib/types/wordpress-server-ipc.ts b/cli/lib/types/wordpress-server-ipc.ts index d08df182ab..f4e305098e 100644 --- a/cli/lib/types/wordpress-server-ipc.ts +++ b/cli/lib/types/wordpress-server-ipc.ts @@ -122,6 +122,7 @@ export const childMessagePm2Schema = z.object( { export const pm2ProcessEventSchema = z.object( { process: z.object( { name: z.string(), + pm_id: z.number().optional(), } ), event: z.string(), } ); diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts index c84c8e81bc..0b5020d35a 100644 --- a/cli/lib/wordpress-server-manager.ts +++ b/cli/lib/wordpress-server-manager.ts @@ -25,6 +25,7 @@ import { ProcessDescription } from 'cli/lib/types/pm2'; import { ServerConfig, childMessagePm2Schema, + pm2ProcessEventSchema, ManagerMessagePayload, } from 'cli/lib/types/wordpress-server-ipc'; import { Logger } from 'cli/logger'; @@ -125,6 +126,7 @@ export async function startWordPressServer( await waitForReadyMessage( processDesc.pmId ); await sendMessage( processDesc.pmId, + processName, { topic: 'start-server', data: { config: serverConfig }, @@ -192,6 +194,7 @@ interface SendMessageOptions { async function sendMessage( pmId: number, + processName: string, message: ManagerMessagePayload, options: SendMessageOptions = {} ): Promise< unknown > { @@ -199,6 +202,7 @@ async function sendMessage( const bus = await getPm2Bus(); const messageId = crypto.randomUUID(); let responseHandler: ( packet: unknown ) => void; + let processEventHandler: ( event: unknown ) => void; let abortListener: () => void; return new Promise( ( resolve, reject ) => { @@ -228,6 +232,17 @@ async function sendMessage( activityCheckIntervalId, } ); + processEventHandler = ( event: unknown ) => { + const result = pm2ProcessEventSchema.safeParse( event ); + if ( ! result.success ) { + return; + } + + if ( result.data.process.name === processName && result.data.event === 'exit' ) { + reject( new Error( 'WordPress server process exited unexpectedly' ) ); + } + }; + responseHandler = ( packet: unknown ) => { const validationResult = childMessagePm2Schema.safeParse( packet ); if ( ! validationResult.success ) { @@ -274,11 +289,13 @@ async function sendMessage( }; abortController.signal.addEventListener( 'abort', abortListener ); + bus.on( 'process:event', processEventHandler ); bus.on( 'process:msg', responseHandler ); sendMessageToProcess( pmId, { ...message, messageId } ).catch( reject ); } ).finally( () => { abortController.signal.removeEventListener( 'abort', abortListener ); + bus.off( 'process:event', processEventHandler ); bus.off( 'process:msg', responseHandler ); const tracker = messageActivityTrackers.get( messageId ); @@ -299,6 +316,7 @@ export async function stopWordPressServer( siteId: string ): Promise< void > { try { await sendMessage( runningProcess.pmId, + processName, { topic: 'stop-server', data: {} }, { maxTotalElapsedTime: GRACEFUL_STOP_TIMEOUT } ); @@ -376,6 +394,7 @@ export async function runBlueprint( await waitForReadyMessage( processDesc.pmId ); await sendMessage( processDesc.pmId, + processName, { topic: 'run-blueprint', data: { config: serverConfig }, @@ -405,7 +424,7 @@ export async function sendWpCliCommand( throw new Error( `WordPress server is not running` ); } - const result = await sendMessage( runningProcess.pmId, { + const result = await sendMessage( runningProcess.pmId, processName, { topic: 'wp-cli-command', data: { args }, } ); diff --git a/cli/wordpress-server-child.ts b/cli/wordpress-server-child.ts index ae41d2e474..7978bc84b1 100644 --- a/cli/wordpress-server-child.ts +++ b/cli/wordpress-server-child.ts @@ -344,11 +344,48 @@ const runWpCliCommand = sequential( { concurrent: 3, max: 10 } ); +function parsePhpError( error: unknown ): string { + if ( ! ( error instanceof Error ) ) { + return String( error ); + } + + const message = error.message; + + // Check for WordPress critical error in HTML output + const wpDieMatch = message.match( /
]*>([\s\S]*?)<\/div>/ ); + if ( wpDieMatch ) { + // Extract text from HTML, removing tags + const htmlContent = wpDieMatch[ 1 ]; + const textContent = htmlContent + .replace( /<[^>]+>/g, ' ' ) + .replace( /\s+/g, ' ' ) + .trim(); + if ( textContent ) { + return `WordPress error: ${ textContent }`; + } + } + + // Check for PHP fatal error pattern + const fatalMatch = message.match( /PHP Fatal error:\s*(.+?)(?:\sin\s|$)/i ); + if ( fatalMatch ) { + return `PHP Fatal error: ${ fatalMatch[ 1 ].trim() }`; + } + + // Check for generic PHP.run() failure - provide a cleaner message + if ( message.includes( 'PHP.run() failed with exit code' ) ) { + const exitCodeMatch = message.match( /exit code (\d+)/ ); + const exitCode = exitCodeMatch ? exitCodeMatch[ 1 ] : 'unknown'; + return `WordPress failed to start (PHP exit code ${ exitCode }). Check the site's debug.log for details.`; + } + + return message; +} + function sendErrorMessage( messageId: string, error: unknown ) { const errorResponse: ChildMessageRaw = { originalMessageId: messageId, topic: 'error', - errorMessage: error instanceof Error ? error.message : String( error ), + errorMessage: parsePhpError( error ), errorStack: error instanceof Error ? error.stack : undefined, cliArgs: lastCliArgs ?? undefined, }; diff --git a/common/lib/site-needs-restart.ts b/common/lib/site-needs-restart.ts new file mode 100644 index 0000000000..90a6592cb5 --- /dev/null +++ b/common/lib/site-needs-restart.ts @@ -0,0 +1,13 @@ +export interface SiteSettingChanges { + domainChanged?: boolean; + httpsChanged?: boolean; + phpChanged?: boolean; + wpChanged?: boolean; + xdebugChanged?: boolean; +} + +export function siteNeedsRestart( changes: SiteSettingChanges ): boolean { + const { domainChanged, httpsChanged, phpChanged, wpChanged, xdebugChanged } = changes; + + return !! ( domainChanged || httpsChanged || phpChanged || wpChanged || xdebugChanged ); +} diff --git a/common/lib/tests/site-needs-restart.test.ts b/common/lib/tests/site-needs-restart.test.ts new file mode 100644 index 0000000000..0a8471474c --- /dev/null +++ b/common/lib/tests/site-needs-restart.test.ts @@ -0,0 +1,60 @@ +import { siteNeedsRestart } from '../site-needs-restart'; + +describe( 'siteNeedsRestart', () => { + it( 'returns false when no changes are provided', () => { + expect( siteNeedsRestart( {} ) ).toBe( false ); + } ); + + it( 'returns false when all changes are false', () => { + expect( + siteNeedsRestart( { + domainChanged: false, + httpsChanged: false, + phpChanged: false, + wpChanged: false, + xdebugChanged: false, + } ) + ).toBe( false ); + } ); + + it( 'returns true when domain changed', () => { + expect( siteNeedsRestart( { domainChanged: true } ) ).toBe( true ); + } ); + + it( 'returns true when https changed', () => { + expect( siteNeedsRestart( { httpsChanged: true } ) ).toBe( true ); + } ); + + it( 'returns true when php changed', () => { + expect( siteNeedsRestart( { phpChanged: true } ) ).toBe( true ); + } ); + + it( 'returns true when wp changed', () => { + expect( siteNeedsRestart( { wpChanged: true } ) ).toBe( true ); + } ); + + it( 'returns true when xdebug changed', () => { + expect( siteNeedsRestart( { xdebugChanged: true } ) ).toBe( true ); + } ); + + it( 'returns true when multiple settings changed', () => { + expect( + siteNeedsRestart( { + phpChanged: true, + wpChanged: true, + } ) + ).toBe( true ); + } ); + + it( 'returns true when one change is true among false values', () => { + expect( + siteNeedsRestart( { + domainChanged: false, + httpsChanged: false, + phpChanged: true, + wpChanged: false, + xdebugChanged: false, + } ) + ).toBe( true ); + } ); +} ); diff --git a/src/components/tests/content-tab-settings.test.tsx b/src/components/tests/content-tab-settings.test.tsx index 7ded339cb9..7a0712f296 100644 --- a/src/components/tests/content-tab-settings.test.tsx +++ b/src/components/tests/content-tab-settings.test.tsx @@ -371,7 +371,8 @@ describe( 'ContentTabSettings', () => { await waitFor( () => { expect( updateSite ).toHaveBeenCalledWith( - expect.objectContaining( { phpVersion: '8.2' } ) + expect.objectContaining( { phpVersion: '8.2' } ), + undefined ); expect( stopServer ).not.toHaveBeenCalled(); expect( startServer ).not.toHaveBeenCalled(); @@ -441,10 +442,9 @@ describe( 'ContentTabSettings', () => { await waitFor( () => { expect( updateSite ).toHaveBeenCalledWith( - expect.objectContaining( { phpVersion: '8.2' } ) + expect.objectContaining( { phpVersion: '8.2' } ), + undefined ); - expect( stopServer ).toHaveBeenCalled(); - expect( startServer ).toHaveBeenCalled(); } ); rerenderWithProvider( diff --git a/src/hooks/use-site-details.tsx b/src/hooks/use-site-details.tsx index 706412a893..104cd0c426 100644 --- a/src/hooks/use-site-details.tsx +++ b/src/hooks/use-site-details.tsx @@ -21,7 +21,7 @@ import type { Blueprint } from 'src/stores/wpcom-api'; interface SiteDetailsContext { selectedSite: SiteDetails | null; - updateSite: ( site: SiteDetails ) => Promise< void >; + updateSite: ( site: SiteDetails, wpVersion?: string ) => Promise< void >; sites: SiteDetails[]; setSelectedSiteId: ( selectedSiteId: string ) => void; createSite: ( @@ -375,8 +375,8 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) { [ selectedTab, setSelectedSiteId, setSelectedTab ] ); - const updateSite = useCallback( async ( site: SiteDetails ) => { - await getIpcApi().updateSite( site ); + const updateSite = useCallback( async ( site: SiteDetails, wpVersion?: string ) => { + await getIpcApi().updateSite( site, wpVersion ); const updatedSites = await getIpcApi().getSiteDetails(); setSites( updatedSites ); }, [] ); diff --git a/src/ipc-handlers.ts b/src/ipc-handlers.ts index 8c9ce861d3..c487e6faf2 100644 --- a/src/ipc-handlers.ts +++ b/src/ipc-handlers.ts @@ -63,6 +63,7 @@ import { import { getLogsFilePath, writeLogToFile, type LogLevel } from 'src/logging'; import { getMainWindow } from 'src/main-window'; import { popupMenu, setupMenu } from 'src/menu'; +import { editSiteViaCli, EditSiteOptions } from 'src/modules/cli/lib/cli-site-editor'; import { shouldExcludeFromSync, shouldLimitDepth } from 'src/modules/sync/lib/tree-utils'; import { supportedEditorConfig, SupportedEditor } from 'src/modules/user-settings/lib/editor'; import { getUserTerminal } from 'src/modules/user-settings/lib/ipc-handlers'; @@ -347,23 +348,69 @@ export async function createSite( export async function updateSite( event: IpcMainInvokeEvent, - updatedSite: SiteDetails + updatedSite: SiteDetails, + wpVersion?: string ): Promise< void > { - try { - await lockAppdata(); - const userData = await loadUserData(); - const updatedSites = userData.sites.map( ( site ) => - site.id === updatedSite.id ? { ...site, ...updatedSite } : site - ); - userData.sites = updatedSites; + const server = SiteServer.get( updatedSite.id ); + if ( ! server ) { + throw new Error( `Site not found: ${ updatedSite.id }` ); + } - const server = SiteServer.get( updatedSite.id ); - if ( server ) { - await server.updateSiteDetails( updatedSite ); + const currentSite = server.details; + + const options: EditSiteOptions = { + path: currentSite.path, + siteId: updatedSite.id, + }; + + if ( updatedSite.name !== currentSite.name ) { + options.name = updatedSite.name; + } + + if ( updatedSite.customDomain !== currentSite.customDomain ) { + options.domain = updatedSite.customDomain ?? ''; + } + + if ( updatedSite.enableHttps !== currentSite.enableHttps ) { + options.https = updatedSite.enableHttps ?? false; + } + + if ( updatedSite.phpVersion !== currentSite.phpVersion ) { + options.php = updatedSite.phpVersion; + } + + if ( wpVersion ) { + options.wp = wpVersion; + } + + if ( updatedSite.enableXdebug !== currentSite.enableXdebug ) { + options.xdebug = updatedSite.enableXdebug ?? false; + } + + const hasCliChanges = Object.keys( options ).length > 2; + + if ( hasCliChanges ) { + await editSiteViaCli( options ); + + const userData = await loadUserData(); + const freshSiteData = userData.sites.find( ( s ) => s.id === updatedSite.id ); + if ( freshSiteData ) { + const wasRunning = server.details.running; + const url = wasRunning ? ( server.details as StartedSiteDetails ).url : undefined; + + if ( wasRunning && url ) { + server.details = { + ...freshSiteData, + running: true, + url, + }; + } else { + server.details = { + ...freshSiteData, + running: false, + }; + } } - await saveUserData( userData ); - } finally { - await unlockAppdata(); } } diff --git a/src/modules/cli/lib/cli-site-editor.ts b/src/modules/cli/lib/cli-site-editor.ts new file mode 100644 index 0000000000..a82f668bbf --- /dev/null +++ b/src/modules/cli/lib/cli-site-editor.ts @@ -0,0 +1,87 @@ +import { z } from 'zod'; +import { SiteCommandLoggerAction } from 'common/logger-actions'; +import { executeCliCommand } from './execute-command'; + +const cliEventSchema = z.object( { + action: z.nativeEnum( SiteCommandLoggerAction ), + status: z.enum( [ 'inprogress', 'fail', 'success', 'warning' ] ), + message: z.string(), +} ); + +export interface EditSiteOptions { + path: string; + siteId: string; + name?: string; + domain?: string; + https?: boolean; + php?: string; + wp?: string; + xdebug?: boolean; +} + +export async function editSiteViaCli( options: EditSiteOptions ): Promise< void > { + const args = buildCliArgs( options ); + console.log( `[CLI Site Editor] Executing: studio ${ args.join( ' ' ) }` ); + + return new Promise( ( resolve, reject ) => { + let lastErrorMessage: string | null = null; + + const [ emitter ] = executeCliCommand( args, { output: 'capture', logPrefix: options.siteId } ); + + emitter.on( 'data', ( { data } ) => { + const parsed = cliEventSchema.safeParse( data ); + if ( ! parsed.success ) { + return; + } + + if ( parsed.data.status === 'inprogress' ) { + console.log( `[CLI - ${ options.siteId }] ${ parsed.data.message }` ); + } else if ( parsed.data.status === 'fail' ) { + lastErrorMessage = parsed.data.message; + } + } ); + + emitter.on( 'success', () => { + resolve(); + } ); + + emitter.on( 'failure', () => { + console.error( `[CLI Site Editor] Command failed: ${ lastErrorMessage }` ); + reject( new Error( lastErrorMessage || 'CLI site set failed' ) ); + } ); + + emitter.on( 'error', ( { error } ) => { + reject( error ); + } ); + } ); +} + +function buildCliArgs( options: EditSiteOptions ): string[] { + const args = [ 'site', 'set', '--path', options.path ]; + + if ( options.name !== undefined ) { + args.push( '--name', options.name ); + } + + if ( options.domain !== undefined ) { + args.push( '--domain', options.domain ); + } + + if ( options.https !== undefined ) { + args.push( options.https ? '--https' : '--no-https' ); + } + + if ( options.php !== undefined ) { + args.push( '--php', options.php ); + } + + if ( options.wp !== undefined ) { + args.push( '--wp', options.wp ); + } + + if ( options.xdebug !== undefined ) { + args.push( options.xdebug ? '--xdebug' : '--no-xdebug' ); + } + + return args; +} diff --git a/src/modules/site-settings/edit-site-details.tsx b/src/modules/site-settings/edit-site-details.tsx index c5e8acd0ea..8ae96e0897 100644 --- a/src/modules/site-settings/edit-site-details.tsx +++ b/src/modules/site-settings/edit-site-details.tsx @@ -3,9 +3,8 @@ import { createInterpolateElement } from '@wordpress/element'; import { sprintf } from '@wordpress/i18n'; import { useI18n } from '@wordpress/react-i18n'; import { FormEvent, useCallback, useEffect, useState } from 'react'; -import stripAnsi from 'strip-ansi'; import { generateCustomDomainFromSiteName, getDomainNameValidationError } from 'common/lib/domains'; -import { getWordPressVersionUrl } from 'common/lib/wordpress-version-utils'; +import { siteNeedsRestart } from 'common/lib/site-needs-restart'; import Button from 'src/components/button'; import { ErrorInformation } from 'src/components/error-information'; import { LearnMoreLink, LearnHowLink } from 'src/components/learn-more'; @@ -33,8 +32,7 @@ type EditSiteDetailsProps = { const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) => { const { __ } = useI18n(); - const { updateSite, selectedSite, stopServer, startServer, isEditModalOpen, setIsEditModalOpen } = - useSiteDetails(); + const { updateSite, selectedSite, isEditModalOpen, setIsEditModalOpen } = useSiteDetails(); const defaultWordPressVersion = useRootSelector( selectDefaultWordPressVersion ); const allowedPhpVersions = useRootSelector( selectAllowedPhpVersions ); const defaultPhpVersion = useRootSelector( selectDefaultPhpVersion ); @@ -139,60 +137,43 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = const hasWpVersionChanged = selectedWpVersion !== getEffectiveWpVersion(); const hasPhpVersionChanged = selectedPhpVersion !== selectedSite.phpVersion; const hasXdebugChanged = enableXdebug !== ( selectedSite.enableXdebug ?? false ); + const hasDomainChanged = + Boolean( selectedSite.customDomain ) !== useCustomDomain || + ( useCustomDomain && customDomain !== selectedSite.customDomain ); + const hasHttpsChanged = + useCustomDomain && enableHttps !== ( selectedSite.enableHttps ?? false ); + const needsRestart = - selectedSite.running && ( hasWpVersionChanged || hasPhpVersionChanged || hasXdebugChanged ); + selectedSite.running && + siteNeedsRestart( { + domainChanged: hasDomainChanged, + httpsChanged: hasHttpsChanged, + phpChanged: hasPhpVersionChanged, + wpChanged: hasWpVersionChanged, + xdebugChanged: hasXdebugChanged, + } ); setNeedsRestart( needsRestart ); try { - if ( needsRestart ) { - await stopServer( selectedSite.id ); - } - - if ( hasWpVersionChanged ) { - try { - const zipUrl = getWordPressVersionUrl( selectedWpVersion ); - const result = await getIpcApi().executeWPCLiInline( { - siteId: selectedSite.id, - args: `core update ${ zipUrl } --force`, - skipPluginsAndThemes: true, - } ); - if ( result.exitCode !== 0 ) { - throw new Error( result.stderr ); - } - } catch ( wpError ) { - console.error( 'Error changing WordPress version:', wpError ); - const errorMessage = stripAnsi( ( wpError as Error )?.message ); - getIpcApi().showErrorMessageBox( { - title: __( 'Error changing WordPress version' ), - message: __( 'An error occurred while updating the WordPress version.' ), - error: new Error( errorMessage ), - showOpenLogs: true, - } ); - setSelectedWpVersion( getEffectiveWpVersion() ); - setIsEditingSite( false ); - return; - } - } - // Determine custom domain setting let usedCustomDomain = useCustomDomain && customDomain ? customDomain : undefined; if ( useCustomDomain && ! customDomain ) { usedCustomDomain = generateCustomDomainFromSiteName( siteName ?? '' ); } - await updateSite( { - ...selectedSite, - name: siteName, - phpVersion: selectedPhpVersion, - isWpAutoUpdating: selectedWpVersion === defaultWordPressVersion, - customDomain: usedCustomDomain, - enableHttps: !! usedCustomDomain && enableHttps, - enableXdebug, - } ); + await updateSite( + { + ...selectedSite, + name: siteName, + phpVersion: selectedPhpVersion, + isWpAutoUpdating: selectedWpVersion === defaultWordPressVersion, + customDomain: usedCustomDomain, + enableHttps: !! usedCustomDomain && enableHttps, + enableXdebug, + }, + hasWpVersionChanged ? selectedWpVersion : undefined + ); - if ( needsRestart ) { - await startServer( selectedSite.id ); - } onSave(); closeModal(); resetFormState(); diff --git a/src/modules/site-settings/tests/edit-site-details.test.tsx b/src/modules/site-settings/tests/edit-site-details.test.tsx index a4da6a5b52..644a887720 100644 --- a/src/modules/site-settings/tests/edit-site-details.test.tsx +++ b/src/modules/site-settings/tests/edit-site-details.test.tsx @@ -266,7 +266,7 @@ describe( 'EditSiteDetails', () => { } ); } ); - it( 'should update site and restart server when PHP version is changed', async () => { + it( 'should update site when PHP version is changed (CLI handles restart)', async () => { ( useSiteDetails as jest.Mock ).mockReturnValue( { ...useSiteDetails(), isEditModalOpen: true, @@ -283,15 +283,13 @@ describe( 'EditSiteDetails', () => { await user.click( screen.getByRole( 'button', { name: 'Save' } ) ); await waitFor( () => { - expect( mockStopServer ).toHaveBeenCalledWith( 'site-123' ); expect( mockUpdateSite ).toHaveBeenCalled(); expect( mockUpdateSite.mock.calls[ 0 ][ 0 ].phpVersion ).toBe( '8.2' ); - expect( mockStartServer ).toHaveBeenCalledWith( 'site-123' ); expect( defaultProps.onSave ).toHaveBeenCalled(); } ); } ); - it( 'should update isWpAutoUpdating to false when changed from latest to specific version', async () => { + it( 'should update isWpAutoUpdating and pass wpVersion when changed from latest to specific version', async () => { ( useSiteDetails as jest.Mock ).mockReturnValue( { ...useSiteDetails(), isEditModalOpen: true, @@ -307,19 +305,16 @@ describe( 'EditSiteDetails', () => { await user.click( screen.getByRole( 'button', { name: 'Save' } ) ); - expect( mockStopServer ).toHaveBeenCalledWith( 'site-123' ); - expect( mockExecuteWPCLiInline ).toHaveBeenCalledWith( { - siteId: 'site-123', - args: 'core update https://wordpress.org/wordpress-6.4.zip --force', - skipPluginsAndThemes: true, + await waitFor( () => { + expect( mockUpdateSite ).toHaveBeenCalled(); + expect( mockUpdateSite.mock.calls[ 0 ][ 0 ].isWpAutoUpdating ).toBe( false ); + // wpVersion is passed as second argument + expect( mockUpdateSite.mock.calls[ 0 ][ 1 ] ).toBe( '6.4' ); + expect( defaultProps.onSave ).toHaveBeenCalled(); } ); - expect( mockUpdateSite ).toHaveBeenCalled(); - expect( mockUpdateSite.mock.calls[ 0 ][ 0 ].isWpAutoUpdating ).toBe( false ); - expect( mockStartServer ).toHaveBeenCalledWith( 'site-123' ); - expect( defaultProps.onSave ).toHaveBeenCalled(); } ); - it( 'should update WordPress version when changed to beta', async () => { + it( 'should pass wpVersion when WordPress version is changed to beta', async () => { ( useSiteDetails as jest.Mock ).mockReturnValue( { ...useSiteDetails(), isEditModalOpen: true, @@ -335,20 +330,19 @@ describe( 'EditSiteDetails', () => { await user.click( screen.getByRole( 'button', { name: 'Save' } ) ); - expect( mockExecuteWPCLiInline ).toHaveBeenCalledWith( { - siteId: 'site-123', - args: 'core update https://wordpress.org/wordpress-6.8-beta1.zip --force', - skipPluginsAndThemes: true, + await waitFor( () => { + expect( mockUpdateSite ).toHaveBeenCalled(); + expect( mockUpdateSite.mock.calls[ 0 ][ 1 ] ).toBe( '6.8-beta1' ); } ); } ); - it( 'should show error when WordPress version update fails', async () => { + it( 'should show error when site update fails', async () => { ( useSiteDetails as jest.Mock ).mockReturnValue( { ...useSiteDetails(), isEditModalOpen: true, } ); - const consoleSpy = jest.spyOn( console, 'error' ).mockImplementation( () => {} ); - mockExecuteWPCLiInline.mockResolvedValue( { exitCode: 1, stderr: 'Update failed' } ); + + mockUpdateSite.mockRejectedValueOnce( new Error( 'CLI site set failed' ) ); renderWithProvider( ); await waitFor( () => { @@ -362,15 +356,8 @@ describe( 'EditSiteDetails', () => { await user.click( screen.getByRole( 'button', { name: 'Save' } ) ); await waitFor( () => { - expect( mockShowErrorMessageBox ).toHaveBeenCalledWith( { - title: 'Error changing WordPress version', - message: 'An error occurred while updating the WordPress version.', - error: new Error( 'Update failed' ), - showOpenLogs: true, - } ); + expect( screen.getByText( 'CLI site set failed' ) ).toBeInTheDocument(); } ); - - consoleSpy.mockRestore(); } ); it( 'should disable form controls when site is being edited', async () => { diff --git a/src/preload.ts b/src/preload.ts index df6b1e9638..045b379b94 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -40,7 +40,8 @@ const api: IpcApi = { ), deleteSite: ( id, deleteFiles ) => ipcRendererInvoke( 'deleteSite', id, deleteFiles ), createSite: ( path, config ) => ipcRendererInvoke( 'createSite', path, config ), - updateSite: ( updatedSite ) => ipcRendererInvoke( 'updateSite', updatedSite ), + updateSite: ( updatedSite, wpVersion ) => + ipcRendererInvoke( 'updateSite', updatedSite, wpVersion ), connectWpcomSites: ( ...args ) => ipcRendererInvoke( 'connectWpcomSites', ...args ), disconnectWpcomSites: ( ...args ) => ipcRendererInvoke( 'disconnectWpcomSites', ...args ), updateConnectedWpcomSites: ( ...args ) => diff --git a/src/site-server.ts b/src/site-server.ts index 1fc603f528..b51d8129fc 100644 --- a/src/site-server.ts +++ b/src/site-server.ts @@ -9,10 +9,6 @@ import { WP_CLI_DEFAULT_RESPONSE_TIMEOUT, WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT, } from 'src/constants'; -import { deleteSiteCertificate, generateSiteCertificate } from 'src/lib/certificate-manager'; -import { getSiteUrl } from 'src/lib/get-site-url'; -import { updateDomainInHosts } from 'src/lib/hosts-file'; -import { updateSiteUrl } from 'src/lib/update-site-url'; import { setupWordPressSite, getWordPressProvider } from 'src/lib/wordpress-provider'; import { CliServerProcess } from 'src/modules/cli/lib/cli-server-process'; import { createSiteViaCli, type CreateSiteOptions } from 'src/modules/cli/lib/cli-site-creator'; @@ -193,12 +189,7 @@ export class SiteServer { } } - async updateSiteDetails( site: SiteDetails ) { - const oldDomain = this.details.customDomain; - const newDomain = site.customDomain; - const oldEnableHttps = this.details.enableHttps; - const newEnableHttps = site.enableHttps; - + updateSiteDetails( site: SiteDetails ) { this.details = { ...this.details, name: site.name, @@ -207,40 +198,15 @@ export class SiteServer { isWpAutoUpdating: site.isWpAutoUpdating, customDomain: site.customDomain, enableHttps: site.enableHttps, + tlsKey: site.tlsKey, + tlsCert: site.tlsCert, + enableXdebug: site.enableXdebug, }; if ( this.server && this.details.running ) { this.details.url = getAbsoluteUrl( this.details ); this.server.url = this.details.url; } - - // Handle domain changes and url changes (updates hosts and database) - if ( oldDomain !== newDomain ) { - void updateDomainInHosts( oldDomain, newDomain, this.details.port ); - } - if ( ( oldDomain && ! newDomain ) || oldEnableHttps !== newEnableHttps ) { - await updateSiteUrl( this, getSiteUrl( this.details ) ); - } - - if ( - oldDomain && - oldEnableHttps && - ( oldDomain !== newDomain || oldEnableHttps !== newEnableHttps ) - ) { - deleteSiteCertificate( oldDomain ); - } - if ( - newDomain && - newEnableHttps && - ( oldDomain !== newDomain || oldEnableHttps !== newEnableHttps ) - ) { - const { cert, key } = await generateSiteCertificate( newDomain ); - this.details = { - ...this.details, - tlsKey: key, - tlsCert: cert, - }; - } } async stop() {