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
67 changes: 55 additions & 12 deletions apps/cli/commands/pull-reprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import { generateNumberedName } from '@studio/common/lib/generate-site-name';
import { encodePassword } from '@studio/common/lib/passwords';
import { portFinder } from '@studio/common/lib/port-finder';
import { readAuthToken, type StoredAuthToken } from '@studio/common/lib/shared-config';
import { SITE_RUNTIME_PLAYGROUND } from '@studio/common/lib/site-runtime';
import {
SITE_RUNTIME_NATIVE_PHP,
SITE_RUNTIME_PLAYGROUND,
SiteRuntime,
getSiteRuntime,
} from '@studio/common/lib/site-runtime';
import { sortSites } from '@studio/common/lib/sort-sites';
import { PullReprintCommandLoggerAction as LoggerAction } from '@studio/common/logger-actions';
import { __, sprintf } from '@wordpress/i18n';
Expand Down Expand Up @@ -55,13 +60,18 @@ import {
import {
ensureImportedSiteSqliteReady,
loadImportedRuntimeStartOptions,
loadImportedRuntimeStartOptionsNative,
} from 'cli/lib/pull/runtime-start-options';
import { getDefaultSitePath } from 'cli/lib/site-paths';
import { buildAutoLoginUrl } from 'cli/lib/site-utils';
import { fetchSyncableSites } from 'cli/lib/sync-api';
import { pickSyncSite } from 'cli/lib/sync-site-picker';
import { getPrettyPath } from 'cli/lib/utils';
import { getProcessName, startWordPressServer } from 'cli/lib/wordpress-server-manager';
import {
startWordPressServer,
StartServerOptions,
getProcessName,
} from 'cli/lib/wordpress-server-manager';
import { Logger, LoggerError } from 'cli/logger';
import { StudioArgv } from 'cli/types';
import type { SyncSite } from '@studio/common/types/sync';
Expand Down Expand Up @@ -390,7 +400,13 @@ export async function runCommand(

let preflight;
try {
preflight = await runPreflight( studioMetadata, apiUrl, secret, verbose );
preflight = await runPreflight(
SITE_RUNTIME_NATIVE_PHP,
studioMetadata,
apiUrl,
secret,
verbose
);
} catch ( preflightError ) {
// Preflight against ?reprint-api can fail for two reasons we can
// recover from on WP.com: the stored secret expired, or the
Expand Down Expand Up @@ -434,7 +450,13 @@ export async function runCommand(
sourceSite.wpComToken.accessToken,
verbose
);
preflight = await runPreflight( studioMetadata, apiUrl, secret, verbose );
preflight = await runPreflight(
SITE_RUNTIME_NATIVE_PHP,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use a dynamic value for this SITE_RUNTIME_NATIVE_PHP? I'm wondering what happens if a user selects the playground runtime in Studio. Not a blocker, just a question for consideration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to iterate on this in another PR. I'm trying to think of ways that we could tie the pull-reprint state management closer to the regular Studio state management (which is site-based rather than pull-based). That would help address this issue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great

studioMetadata,
apiUrl,
secret,
verbose
);
}
studioMetadata.remoteSiteUrl = preflight.siteurl || studioMetadata.normalizedUrl;
studioMetadata.tablePrefix = preflight.table_prefix || undefined;
Expand All @@ -452,7 +474,7 @@ export async function runCommand(
// a delta re-pull, resets its own sub-command state via
// prepare_repull().
if ( ! hasPullCompletedStage( studioMetadata, 'pulled' ) ) {
await runFullPull( studioMetadata, apiUrl, secret, verbose );
await runFullPull( SITE_RUNTIME_NATIVE_PHP, studioMetadata, apiUrl, secret, verbose );
}

let createdSiteRecord = false;
Expand Down Expand Up @@ -494,10 +516,18 @@ export async function runCommand(
}

if ( ! hasPullCompletedStage( studioMetadata, 'site-started' ) ) {
await ensureImportedSiteSqliteReady( studioMetadata.runtimeBlueprintPath );
const runtimeStartOptions = await loadImportedRuntimeStartOptions(
studioMetadata.runtimeBlueprintPath
);
let runtimeStartOptions: StartServerOptions;
if ( getSiteRuntime( site ) === SITE_RUNTIME_NATIVE_PHP ) {
runtimeStartOptions = loadImportedRuntimeStartOptionsNative(
studioMetadata.technicalSiteDirectory,
studioMetadata.runtimeDirectory
);
} else {
await ensureImportedSiteSqliteReady( studioMetadata.runtimeBlueprintPath );
runtimeStartOptions = await loadImportedRuntimeStartOptions(
studioMetadata.runtimeBlueprintPath
);
}

// Persist the computed start options so `studio site start` and
// the daemon can re-read them without recomputing (which spins
Expand Down Expand Up @@ -566,7 +596,13 @@ export async function runCommand(

if ( ! hasPullCompletedStage( studioMetadata, 'completed' ) ) {
if ( hasSkippedFiles( studioMetadata.stateDirectory ) ) {
await downloadSkippedFiles( studioMetadata, apiUrl, secret, verbose );
await downloadSkippedFiles(
getSiteRuntime( site ),
studioMetadata,
apiUrl,
secret,
verbose
);
}

recordCompletedStage( studioMetadata, 'completed' );
Expand Down Expand Up @@ -606,6 +642,7 @@ export async function runCommand(
* metadata before the download stages begin.
*/
async function runPreflight(
runtime: SiteRuntime,
sessionMetadata: PullSessionMetadata,
sourceSiteApiUrl: string,
sourceSiteSecret: string,
Expand Down Expand Up @@ -640,6 +677,7 @@ async function runPreflight(
undefined,
{
verboseCommands: verbose,
runtime,
}
);
} catch ( preflightError ) {
Expand Down Expand Up @@ -810,7 +848,7 @@ function readPullMetadata( metadataPath: string ): PullSessionMetadata | null {
/**
* Run reprint's composite `pull` command: the whole site-clone
* pipeline (preflight → files-pull → db-pull → db-apply →
* flat-docroot → apply-runtime) in a single PHP-WASM fork, with
* flat-docroot → apply-runtime) in a single child process, with
* reprint owning the stage ordering and, when the prior pull already
* completed, resetting its own sub-command state for a delta re-pull
* via prepare_repull().
Expand All @@ -830,6 +868,7 @@ function readPullMetadata( metadataPath: string ): PullSessionMetadata | null {
* Advances the pull stage to 'pulled'.
*/
export async function runFullPull(
runtime: SiteRuntime,
metadata: PullSessionMetadata,
apiUrl: string,
secret: string,
Expand All @@ -839,6 +878,7 @@ export async function runFullPull(
const sqlitePath = contentDir
? `${ metadata.rawDirectory }${ contentDir }/database/.ht.sqlite`
: `${ metadata.sitePath }/wp-content/database/.ht.sqlite`;
const reprintRuntime = runtime === SITE_RUNTIME_NATIVE_PHP ? 'nginx-fpm' : 'playground-cli';

logger.reportStart( LoggerAction.DOWNLOAD_FILES, __( 'Pulling site…' ) );
await runReprintCommandUntilComplete(
Expand All @@ -853,7 +893,7 @@ export async function runFullPull(
`--target-sqlite-path=${ sqlitePath }`,
`--new-site-url=${ metadata.localUrl! }`,
`--flatten-to=${ metadata.sitePath }`,
'--runtime=playground-cli',
`--runtime=${ reprintRuntime }`,
'--start-runtime=none',
`--output-dir=${ metadata.runtimeDirectory }`,
'--no-adaptive',
Expand All @@ -868,6 +908,7 @@ export async function runFullPull(
{ hostPath: metadata.runtimeDirectory, vfsPath: metadata.runtimeDirectory },
],
verboseCommands: verbose,
runtime,
}
);
logger.reportSuccess( __( 'Site pulled' ) );
Expand All @@ -886,6 +927,7 @@ export async function runFullPull(
* internal validation, so we leave the state alone in that case.
*/
export async function downloadSkippedFiles(
runtime: SiteRuntime,
metadata: PullSessionMetadata,
apiUrl: string,
secret: string,
Expand Down Expand Up @@ -939,6 +981,7 @@ export async function downloadSkippedFiles(
{
progressLabel: __( 'Remaining files' ),
verboseCommands: verbose,
runtime,
}
);
logger.reportSuccess( __( 'Remaining files downloaded' ) );
Expand Down
26 changes: 23 additions & 3 deletions apps/cli/commands/tests/pull-reprint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { readAuthToken } from '@studio/common/lib/shared-config';
import { SITE_RUNTIME_PLAYGROUND } from '@studio/common/lib/site-runtime';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { enableReprintExporter, rotateReprintSecret } from 'cli/lib/api';
import * as migrationClient from 'cli/lib/pull/migration-client';
Expand Down Expand Up @@ -153,6 +154,7 @@ describe( 'CLI: studio pull-reprint helpers', () => {
} );

await downloadSkippedFiles(
SITE_RUNTIME_PLAYGROUND,
{
normalizedUrl: 'https://example.com/',
stateDirectory,
Expand Down Expand Up @@ -235,7 +237,13 @@ describe( 'CLI: studio pull-reprint single pull phase', () => {
remoteSiteUrl: 'https://example.com',
} as never;

await runFullPull( metadata, 'https://example.com/?reprint-api', 'hmac-secret', false );
await runFullPull(
SITE_RUNTIME_PLAYGROUND,
metadata,
'https://example.com/?reprint-api',
'hmac-secret',
false
);

expect( reprint ).toHaveBeenCalledTimes( 1 );
const [ passedState, passedRaw, passedArgs, , passedOptions ] = reprint.mock.calls[ 0 ];
Expand Down Expand Up @@ -309,7 +317,13 @@ describe( 'CLI: studio pull-reprint single pull phase', () => {
remoteSiteUrl: 'https://example.com',
} as never;

await runFullPull( metadata, 'https://example.com/?reprint-api', 'hmac-secret', false );
await runFullPull(
SITE_RUNTIME_PLAYGROUND,
metadata,
'https://example.com/?reprint-api',
'hmac-secret',
false
);

const [ , , passedArgs, , passedOptions ] = reprint.mock.calls[ 0 ];
// With no content dir from preflight, the sqlite target falls back to
Expand Down Expand Up @@ -362,7 +376,13 @@ describe( 'CLI: studio pull-reprint single pull phase', () => {
};

await expect(
runFullPull( metadata as never, 'https://example.com/?reprint-api', 'hmac-secret', false )
runFullPull(
SITE_RUNTIME_PLAYGROUND,
metadata as never,
'https://example.com/?reprint-api',
'hmac-secret',
false
)
).rejects.toThrow( 'reprint exited with code 1' );

// Stage must NOT advance to 'pulled' — otherwise a resume would skip
Expand Down
46 changes: 31 additions & 15 deletions apps/cli/lib/native-php/php-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,23 @@ export const DETACH_FOR_GROUP_KILL = process.platform !== 'win32';
// in-flight children (mid-startup workers, install/blueprint subprocesses) callers don't track.
const livePhpProcesses = new Set< ChildProcess >();

export type SpawnPhpProcessOptions = {
export type BasePhpOptions = {
autoPrependFile?: string;
detached?: boolean;
disallowRiskyFunctions?: boolean;
env?: NodeJS.ProcessEnv;
mode?: 'pipe' | 'capture-stdout';
enableXdebug?: boolean;
onlyPathsThatPhpCanAccess?: string[];
phpVersion: NativePhpSupportedVersion;
siteFolder?: string;
signal?: AbortSignal;
};
type SpawnPhpProcessOptions = BasePhpOptions & {
mode?: 'pipe' | 'no-pipe';
};
type RunPhpCommandOptions = BasePhpOptions & {
mode?: 'pipe' | 'no-pipe' | 'capture';
};

export function spawnPhpProcess(
args: string[],
Expand All @@ -38,6 +44,7 @@ export function spawnPhpProcess(
enableXdebug = false,
onlyPathsThatPhpCanAccess = [],
disallowRiskyFunctions = false,
autoPrependFile,
}: SpawnPhpProcessOptions
): ChildProcess {
const defaultArgs = getDefaultPhpArgs(
Expand All @@ -46,7 +53,11 @@ export function spawnPhpProcess(
disallowRiskyFunctions,
enableXdebug
);
const phpArgs = [ ...defaultArgs, ...args ];
// Run a PHP file before the main script on every request — used to inject
// reprint's generated runtime.php (constants, SQLite loader, upload proxy)
// into imported sites without modifying their wp-config.php.
const prependArgs = autoPrependFile ? [ '-d', `auto_prepend_file=${ autoPrependFile }` ] : [];
const phpArgs = [ ...defaultArgs, ...prependArgs, ...args ];
const phpScriptProcess = spawn( getPhpBinaryPath( phpVersion ), phpArgs, {
cwd: siteFolder,
env: env ? { ...process.env, ...env } : process.env,
Expand All @@ -62,9 +73,6 @@ export function spawnPhpProcess(

if ( mode === 'pipe' ) {
phpScriptProcess.stdout?.pipe( process.stdout, { end: false } );
}

if ( mode === 'pipe' || mode === 'capture-stdout' ) {
phpScriptProcess.stderr?.pipe( process.stderr, { end: false } );
}

Expand Down Expand Up @@ -150,31 +158,39 @@ export function reapPhpTreeOnInterrupt( child: ChildProcess ): () => void {
};
}

type RunPhpCommandOptions = SpawnPhpProcessOptions;

export async function runPhpCommand(
args: string[],
options: RunPhpCommandOptions
): Promise< { stdout: string } > {
return await new Promise< { stdout: string } >( ( resolve, reject ) => {
const phpScriptProcess = spawnPhpProcess( args, options );
): Promise< { stdout: string; stderr: string } > {
return await new Promise< { stdout: string; stderr: string } >( ( resolve, reject ) => {
const phpScriptProcess = spawnPhpProcess( args, {
...options,
mode: options.mode === 'capture' ? 'no-pipe' : options.mode,
} );
const reportActivity = () => process.send?.( { topic: 'activity' } );

let stdout = '';
const reportActivity = () => process.send?.( { topic: 'activity' } );
phpScriptProcess.stdout?.on( 'data', ( chunk ) => {
reportActivity();
if ( options.mode === 'capture-stdout' ) {
if ( options.mode === 'capture' ) {
stdout += chunk.toString();
}
} );
phpScriptProcess.stderr?.on( 'data', reportActivity );

let stderr = '';
phpScriptProcess.stderr?.on( 'data', ( chunk ) => {
reportActivity();
if ( options.mode === 'capture' ) {
stderr += chunk.toString();
}
} );

phpScriptProcess.once( 'error', ( error: Error ) => {
reject( error );
} );
phpScriptProcess.once( 'close', ( code ) => {
if ( code === 0 ) {
resolve( { stdout } );
resolve( { stdout, stderr } );
return;
}

Expand Down
2 changes: 1 addition & 1 deletion apps/cli/lib/native-php/site-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ echo is_blog_installed() ? '1' : '0';
phpVersion,
siteFolder,
signal,
mode: 'capture-stdout',
mode: 'capture',
} );
stdout = result.stdout;
} catch ( error ) {
Expand Down
Loading