Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions apps/cli/commands/site/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { __, sprintf } from '@wordpress/i18n';
import { getSiteByFolder, updateSiteLatestCliPid } from 'cli/lib/cli-config/sites';
import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client';
import { getAiInstructionsPath } from 'cli/lib/dependency-management/paths';
import { getSiteDetails } from 'cli/lib/site-details';
import { withSiteOperation } from 'cli/lib/site-operations';
import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils';
import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration';
Expand Down Expand Up @@ -126,6 +127,11 @@ async function startSite(
export async function commandHandler( argv: CommandArgs< typeof builder > ) {
try {
await runCommand( argv.path, argv.skipBrowser, argv.skipLogDetails );
// Read after the operation is released, so Studio can show the site as it now is.
defaultLogger.reportResult( {
...getSiteDetails( await getSiteByFolder( argv.path ) ),
running: true,
} );
} catch ( error ) {
if ( error instanceof LoggerError ) {
defaultLogger.reportError( error );
Expand Down
6 changes: 6 additions & 0 deletions apps/cli/commands/site/stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
disconnectFromDaemon,
killDaemonAndChildren,
} from 'cli/lib/daemon-client';
import { getSiteDetails } from 'cli/lib/site-details';
import { withSiteOperation } from 'cli/lib/site-operations';
import { stopProxyIfNoSitesNeedIt } from 'cli/lib/site-utils';
import { getTracksOrigin, recordTracksEvent, TRACKS_EVENTS } from 'cli/lib/tracks';
Expand Down Expand Up @@ -158,6 +159,11 @@ export async function commandHandler( argv: CommandArgs< typeof builder > ) {
await runCommand( Mode.STOP_ALL_SITES, undefined );
} else {
await runCommand( Mode.STOP_SINGLE_SITE, argv.path );
// Read after the operation is released, so Studio can show the site as it now is.
defaultLogger.reportResult( {
...getSiteDetails( await getSiteByFolder( argv.path ) ),
running: false,
} );
}
} catch ( error ) {
if ( error instanceof LoggerError ) {
Expand Down
6 changes: 2 additions & 4 deletions apps/local/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -830,8 +830,7 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
res.status( 404 ).json( { error: `Site ${ req.params.id } not found` } );
return;
}
await startSite( execute, site.path );
res.sendStatus( 204 );
res.json( await startSite( execute, site.path ) );
} )
);

Expand All @@ -844,8 +843,7 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
res.status( 404 ).json( { error: `Site ${ req.params.id } not found` } );
return;
}
await stopSite( execute, site.path );
res.sendStatus( 204 );
res.json( await stopSite( execute, site.path ) );
} )
);

Expand Down
21 changes: 14 additions & 7 deletions apps/studio/src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import {
downloadAndExtractBlueprintBundle,
removeBlueprintTempDir,
} from '@studio/common/lib/blueprint-bundle';
import { SITE_EVENTS } from '@studio/common/lib/cli-events';
import { SITE_EVENTS, type SiteListItem } from '@studio/common/lib/cli-events';
import { CliCommandError } from '@studio/common/lib/cli-process';
import { stripIpcErrorPrefix } from '@studio/common/lib/error-formatting';
import {
Expand Down Expand Up @@ -615,7 +615,11 @@ export async function updateSite(
}
}

export async function startServer( event: IpcMainInvokeEvent, id: string ): Promise< void > {
// Resolves with the started site, or nothing when an error page is served instead.
export async function startServer(
event: IpcMainInvokeEvent,
id: string
): Promise< SiteListItem | undefined > {
// Starting only needs the path, which `cli.json` has; the CLI's full details wait for a failure.
const site = await readSite( id );
if ( ! site ) {
Expand All @@ -629,7 +633,9 @@ export async function startServer( event: IpcMainInvokeEvent, id: string ): Prom
const start = () => startSite( executeCliCommand, site.path );
try {
console.log( `Starting server for '${ site.name }'` );
await start();
const started = await start();
console.log( `Server started for '${ site.name }'` );
return started;
} catch ( error ) {
// Too many running sites or a WordPress update in progress: expected, not a bug — skip Sentry
if ( error instanceof SiteStartRefusedError ) {
Expand Down Expand Up @@ -701,11 +707,12 @@ export async function startServer( event: IpcMainInvokeEvent, id: string ): Prom

throw error;
}

console.log( `Server started for '${ site.name }'` );
}

export async function stopServer( event: IpcMainInvokeEvent, id: string ): Promise< void > {
export async function stopServer(
event: IpcMainInvokeEvent,
id: string
): Promise< SiteListItem | undefined > {
const sitePath = await readSitePath( id );
if ( ! sitePath ) {
return;
Expand All @@ -714,7 +721,7 @@ export async function stopServer( event: IpcMainInvokeEvent, id: string ): Promi
await stopErrorRecovery( id );
console.log( 'Stopping server with ID', id );
try {
await stopSite( executeCliCommand, sitePath );
return await stopSite( executeCliCommand, sitePath );
} catch ( error ) {
console.error( error );
}
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/src/lib/php-error-recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export async function startErrorRecovery(
site: { id: string; port: number; path: string },
errorMessage: string,
readPm2Logs: ( siteId: string ) => { stdout?: string[]; stderr?: string[] },
startSite: () => Promise< void >
startSite: () => Promise< unknown >
): Promise< void > {
const { id, port, path } = site;

Expand Down
4 changes: 2 additions & 2 deletions apps/ui/src/data/core/connectors/ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,11 +308,11 @@ export function createIpcConnector(): Connector {
},

async startSite( id ) {
await unwrapIpcError( ipcApi.startServer( id ) );
return unwrapIpcError( ipcApi.startServer( id ) );
},

async stopSite( id ) {
await ipcApi.stopServer( id );
return ipcApi.stopServer( id );
},

async updateSite( site, wpVersion ) {
Expand Down
8 changes: 5 additions & 3 deletions apps/ui/src/data/core/connectors/local/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import type {
} from '../../types';
import type { AgentRunEvent } from '@studio/agent/protocol/agent-events';
import type { AiSettings } from '@studio/agent/protocol/providers';
import type { SiteEvent, SnapshotEvent } from '@studio/common/lib/cli-events';
import type { SiteEvent, SiteListItem, SnapshotEvent } from '@studio/common/lib/cli-events';
import type { SyncEvent } from '@studio/common/lib/sync/activity';
import type { RawDirectoryEntry } from '@studio/common/types/sync-tree';

Expand Down Expand Up @@ -334,10 +334,12 @@ export function createLocalConnector( { apiBaseUrl }: LocalConnectorOptions ): C
return lastSites;
},
async startSite( id ) {
await api( `/sites/${ encodeURIComponent( id ) }/start`, { method: 'POST' } );
return api< SiteListItem >( `/sites/${ encodeURIComponent( id ) }/start`, {
method: 'POST',
} );
},
async stopSite( id ) {
await api( `/sites/${ encodeURIComponent( id ) }/stop`, { method: 'POST' } );
return api< SiteListItem >( `/sites/${ encodeURIComponent( id ) }/stop`, { method: 'POST' } );
},
async getSiteThumbnail( siteId ) {
const { thumbnail } = await api< { thumbnail: string | null } >(
Expand Down
8 changes: 5 additions & 3 deletions apps/ui/src/data/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
} from '@studio/agent/protocol/sessions/types';
import type { StudioVisualAnnotationSummary } from '@studio/agent/protocol/visual-annotations';
import type { SiteAgentFile, SiteAgentFileKind } from '@studio/common/lib/agent-skills';
import type { SiteEvent, SnapshotEvent } from '@studio/common/lib/cli-events';
import type { SiteEvent, SiteListItem, SnapshotEvent } from '@studio/common/lib/cli-events';
import type { SupportedLocale } from '@studio/common/lib/locale';
import type {
TracksAuthSource,
Expand Down Expand Up @@ -183,8 +183,10 @@ export interface Connector {
// registers it as a new site). The connector picks a numbered name and a new
// site id internally so callers stay simple.
copySite( sourceSiteId: string ): Promise< SiteDetails >;
startSite( id: string ): Promise< void >;
stopSite( id: string ): Promise< void >;
// Resolve with the site as it is once the command finishes, or nothing when
// the caller has to read the list for it.
startSite( id: string ): Promise< SiteListItem | void >;
stopSite( id: string ): Promise< SiteListItem | void >;
// Persists edits from the site-settings screen via the CLI-backed main
// process handler. `wpVersion` is only forwarded when the user explicitly
// picked a pinned version — undefined means "keep auto-updating".
Expand Down
18 changes: 6 additions & 12 deletions apps/ui/src/data/queries/use-sites.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,28 +200,22 @@ describe( 'useSyncSitesWithEvents', () => {
[ 'start', true ],
[ 'stop', false ],
] as const )(
'shows a %s from its events without refetching the list',
'shows the site a %s reports, before its release event, without refetching the list',
async ( action, running ) => {
// What the CLI reports, and the sort order only Studio keeps.
const details = { ...makeSite(), url: 'http://localhost:9010' };
const site = { ...details, running: ! running, sortOrder: 1000 };
const connector = createFakeConnector( { sites: [ site ] } );
connector[ action === 'start' ? 'startSite' : 'stopSite' ].mockImplementation( async () => {
const siteId = site.id;
const held = { ...details, operation: { pid: 1, kind: action } };
// The event while the operation still holds the site. Its release
// event can land after the command finishes.
connector.emit( 'onSiteEvent', {
event: SITE_EVENTS.UPDATED,
siteId,
site: held,
siteId: site.id,
site: { ...details, operation: { pid: 1, kind: action } },
running,
} );
// An operation's release doesn't know whether the site runs.
connector.emit( 'onSiteEvent', {
event: SITE_EVENTS.OPERATIONS_CHANGED,
siteId,
site: details,
running: ! running,
} );
return { ...details, running };
} );
const { result } = renderHook(
() => {
Expand Down
31 changes: 16 additions & 15 deletions apps/ui/src/data/queries/use-sites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { SESSIONS_QUERY_KEY } from '@/data/queries/use-sessions';
import { WP_VERSION_QUERY_KEY } from '@/data/queries/use-wordpress-versions';
import { useOpenSite } from '@/data/use-open-site';
import type { Connector, CreateSiteParams, SiteDetails } from '@/data/core';
import type { SiteEvent } from '@studio/common/lib/cli-events';
import type { SiteEvent, SiteListItem } from '@studio/common/lib/cli-events';
import type { SiteOperationKind } from '@studio/common/lib/site-operation';
import type { SiteMetadata } from '@studio/common/sites/metadata';
import type { QueryClient } from '@tanstack/react-query';
Expand Down Expand Up @@ -157,10 +157,7 @@ export function useStartSite() {
if ( isSiteMutating( queryClient, STOP_SITE_MUTATION_KEY, id ) ) {
return false;
}
await connector.startSite( id );
if ( ! listShowsSite( queryClient, id, true ) ) {
await queryClient.invalidateQueries( { queryKey: SITES_QUERY_KEY } );
}
await showSite( queryClient, await connector.startSite( id ) );
return true;
},
onError: ( error, id ) => {
Expand All @@ -181,10 +178,7 @@ export function useStopSite() {
return useMutation( {
mutationKey: STOP_SITE_MUTATION_KEY,
mutationFn: async ( id: string ) => {
await connector.stopSite( id );
if ( ! listShowsSite( queryClient, id, false ) ) {
await queryClient.invalidateQueries( { queryKey: SITES_QUERY_KEY } );
}
await showSite( queryClient, await connector.stopSite( id ) );
},
onError: ( _error, id ) => {
const { title, description } = getBusyNotice( queryClient, id, __( 'Failed to stop site' ) );
Expand Down Expand Up @@ -395,12 +389,19 @@ function applySiteEvent( queryClient: QueryClient, event: SiteEvent ): boolean {
return true;
}

/** Whether the cached list already shows the site in this state, with no operation holding it. */
function listShowsSite( queryClient: QueryClient, id: string, running: boolean ): boolean {
const site = queryClient
.getQueryData< SiteDetails[] >( SITES_QUERY_KEY )
?.find( ( item ) => item.id === id );
return site?.running === running && ! site.operation;
/** Shows the site a start or stop reported, or reads the list when there's none to show. */
async function showSite( queryClient: QueryClient, site: SiteListItem | void ): Promise< void > {
const applied =
site &&
applySiteEvent( queryClient, {
event: SITE_EVENTS.UPDATED,
siteId: site.id,
running: site.running,
site,
} );
if ( ! applied ) {
await queryClient.invalidateQueries( { queryKey: SITES_QUERY_KEY } );
}
}

/**
Expand Down
39 changes: 34 additions & 5 deletions packages/common/sites/lifecycle.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { __ } from '@wordpress/i18n';
import { siteListItemSchema, type SiteListItem } from '@studio/common/lib/cli-events';
import {
CliCommandError,
runCliCommand,
type ExecuteCliCommand,
} from '@studio/common/lib/cli-process';
import { getProxyPortConflictMessage } from '@studio/common/lib/domains';
import { checkMaintenanceFile } from '@studio/common/lib/maintenance-file';
import type { ExecuteCliCommand } from '@studio/common/lib/cli-process';

// Start / stop / delete a local site via the Studio CLI's one-shot `site`
// commands, for the desktop main process and the `studio ui` server alike.
Expand All @@ -22,11 +27,31 @@ function runSiteCommand(
} );
}

// Start and stop report the site as it is once they finish, so the caller can
// show it without waiting for the matching site event.
async function runSiteStateCommand(
execute: ExecuteCliCommand,
args: string[],
baseMessage: string
): Promise< SiteListItem > {
try {
return await runCliCommand( execute, args, siteListItemSchema );
} catch ( error ) {
if ( error instanceof CliCommandError ) {
error.baseMessage = baseMessage;
}
throw error;
}
}

/** The site can't start for an expected reason that its message explains to the user. */
export class SiteStartRefusedError extends Error {}

/** Start a local site's server via the Studio CLI. Resolves when it's already running. */
export async function startSite( execute: ExecuteCliCommand, sitePath: string ): Promise< void > {
export async function startSite(
execute: ExecuteCliCommand,
sitePath: string
): Promise< SiteListItem > {
const maintenance = checkMaintenanceFile( sitePath );
if ( maintenance.exists && ! maintenance.isStale ) {
throw new SiteStartRefusedError(
Expand All @@ -36,7 +61,7 @@ export async function startSite( execute: ExecuteCliCommand, sitePath: string ):
);
}
try {
await runSiteCommand(
return await runSiteStateCommand(
execute,
[ 'site', 'start', '--path', sitePath, '--skip-browser', '--skip-log-details' ],
'Failed to start site'
Expand All @@ -59,8 +84,12 @@ export async function startSite( execute: ExecuteCliCommand, sitePath: string ):
}

/** Stop a local site's server via the Studio CLI. */
export function stopSite( execute: ExecuteCliCommand, sitePath: string ): Promise< void > {
return runSiteCommand( execute, [ 'site', 'stop', '--path', sitePath ], 'Failed to stop site' );
export function stopSite( execute: ExecuteCliCommand, sitePath: string ): Promise< SiteListItem > {
return runSiteStateCommand(
execute,
[ 'site', 'stop', '--path', sitePath ],
'Failed to stop site'
);
}

/** Remove a site from Studio via the Studio CLI, trashing its files unless `deleteFiles` is false. */
Expand Down
Loading