PluginProbe
Extendify / trunk
Extendify vtrunk
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
← All changes | src/AutoLaunch/functions/plugins.js +131 -21 3.1.1 → trunk View file →
@@ -1,6 +1,11 @@
1 1 import { recordPluginActivity } from '@shared/api/DataApi';
2 +import { digest } from '@shared/api/digest';
2 3 import { enableAutoUpdate } from '@shared/api/wp';
4 +import {
5 + failedDependencies,
6 + processWithSecondPass,
7 +} from '@shared/lib/patterns';
3 8 import apiFetch from '@wordpress/api-fetch';
4 9 import { addQueryArgs } from '@wordpress/url';
5 10
6 11 export const getActivePlugins = () =>
@@ -8,9 +13,34 @@
8 13
9 14 export const alreadyActive = (activePlugins, pluginSlug) =>
10 15 activePlugins?.filter((p) => p.includes(pluginSlug))?.length;
11 16
12 -export const installPlugin = async (slug) => {
17 +// WP unpacks every install through one shared dir; two at once corrupt both.
18 +let installQueue = Promise.resolve();
19 +
20 +// Re-installing is not free: WP unpacks before it finds the folder exists.
21 +const installed = new Map();
22 +
23 +const enqueue = (task) => {
24 + const result = installQueue.then(task);
25 + installQueue = result.catch(() => {});
26 + return result;
27 +};
28 +
29 +const INSTALL_DRAIN_MS = 60_000;
30 +
31 +// The install POST has no timeout; uncapped, one stall parks the build.
32 +const waitForInstalls = () => {
33 + let timer;
34 + return Promise.race([
35 + installQueue,
36 + new Promise((resolve) => {
37 + timer = setTimeout(resolve, INSTALL_DRAIN_MS);
38 + }),
39 + ]).finally(() => clearTimeout(timer));
40 +};
41 +
42 +const install = async (slug) => {
13 43 const fn = async () => {
14 44 const p = await apiFetch({
15 45 path: '/wp/v2/plugins',
16 46 method: 'POST',
@@ -15,10 +45,11 @@
15 45 path: '/wp/v2/plugins',
16 46 method: 'POST',
17 47 data: { slug },
18 48 });
19 - await recordPluginActivity({ slug, source: 'auto-launch' });
20 - await enableAutoUpdate(p?.plugin);
49 + // Unawaited: no timeout on these, and a stall would hold the queue.
50 + recordPluginActivity({ slug, source: 'auto-launch' });
51 + enableAutoUpdate(p?.plugin);
21 52 return p;
22 53 };
23 54 try {
24 55 return await fn();
@@ -23,23 +54,33 @@
23 54 try {
24 55 return await fn();
25 56 } catch (error) {
26 57 if (error?.code === 'folder_exists') {
27 - console.warn(
28 - `Plugin ${slug} already installed. Attempting to activate...`,
29 - );
30 - // Get the plugin info directly here
58 + // Already on disk — fetch its record so the caller can activate it.
31 59 return await getPlugin(slug);
32 60 }
33 - console.error(`Error installing ${slug}. Retrying...`, error);
34 61 try {
35 62 return await fn();
36 63 } catch (error) {
37 - console.error(`Failed ${slug} again. Giving up`, error);
64 + digest({
65 + error,
66 + details: { source: 'auto-launch', caller: 'installPlugin' },
67 + });
68 + return null;
38 69 }
39 70 }
40 71 };
41 72
73 +export const installPlugin = (slug) =>
74 + enqueue(async () => {
75 + const known = installed.get(slug);
76 + if (known) return { plugin: known };
77 +
78 + const plugin = await install(slug);
79 + if (plugin?.plugin) installed.set(slug, plugin.plugin);
80 + return plugin;
81 + });
82 +
42 83 export const getPlugin = async (slug) => {
43 84 const response = await apiFetch({
44 85 path: addQueryArgs('/wp/v2/plugins', { search: slug }),
45 86 });
@@ -55,20 +96,93 @@
55 96 });
56 97
57 98 try {
58 99 await fn(slug);
100 + return true;
59 101 } catch (_) {
60 - console.warn(`Error activating ${slug}. Retrying with fresh data...`);
61 102 try {
62 103 // try once more but get the slug first
63 104 const { plugin } = await getPlugin(slug);
64 105 await fn(plugin);
106 + return true;
65 107 } catch (error) {
66 - console.error(`Failed to activate ${slug} again. Giving up`, error);
108 + digest({
109 + error,
110 + details: { source: 'auto-launch', caller: 'activatePlugin' },
111 + });
112 + return false;
67 113 }
68 114 }
69 115 };
70 116
117 +// Exact match, not substring: woocommerce-payments would mask a missing woocommerce.
118 +const notActive = (activePlugins, slugs) =>
119 + slugs.filter(
120 + (slug) => !activePlugins?.some((path) => path.split('/')[0] === slug),
121 + );
122 +
123 +export const ensurePluginsActive = async (slugs) => {
124 + // Two callers don't await this; a throw here silently skips every install.
125 + const active = await getActivePlugins().catch((error) => {
126 + digest({
127 + error,
128 + details: { source: 'auto-launch', caller: 'ensurePluginsActive' },
129 + });
130 + return [];
131 + });
132 + const missing = notActive(active, slugs);
133 + if (!missing.length) return { failed: [] };
134 +
135 + const onDisk = window.extSharedData?.installedPluginsSlugs ?? [];
136 + const failed = [];
137 + for (const slug of missing) {
138 + try {
139 + const plugin = onDisk.includes(slug) ? null : await installPlugin(slug);
140 + const activated = await activatePlugin(plugin?.plugin ?? slug);
141 + if (!activated) failed.push(slug);
142 + } catch (_) {
143 + failed.push(slug);
144 + }
145 + }
146 + return { failed };
147 +};
148 +
149 +export const reportInactivePlugins = async (slugs) => {
150 + // Falling back to an empty list would name every plugin as inactive.
151 + const active = await getActivePlugins().catch(() => null);
152 + if (!active) return;
153 +
154 + const inactive = notActive(active, slugs);
155 + if (!inactive.length) return;
156 +
157 + digest({
158 + error: { message: `Plugins inactive after launch: ${inactive.join(', ')}` },
159 + details: {
160 + source: 'auto-launch',
161 + caller: 'reportInactivePlugins',
162 + inactive,
163 + },
164 + });
165 +};
166 +
167 +export const reportFailedDependencies = (pages) => {
168 + const failed = [
169 + ...new Set(pages.flatMap(({ patterns }) => failedDependencies(patterns))),
170 + ];
171 + if (!failed.length) return;
172 +
173 + digest({
174 + error: {
175 + message: `Patterns left static after a failed dependency: ${failed.join(', ')}`,
176 + },
177 + details: {
178 + source: 'auto-launch',
179 + caller: 'reportFailedDependencies',
180 + failed,
181 + },
182 + });
183 +};
184 +
71 185 // Currently this only processes patterns with placeholders
72 186 // by swapping out the placeholders with the actual code
73 187 // returns the patterns as blocks with the placeholders replaced
74 188 export const replacePlaceholderPatterns = async (patterns) => {
@@ -103,20 +217,16 @@
103 217 source: 'auto-launch',
104 218 });
105 219 }
106 220
107 - try {
108 - return await processPlaceholders(patterns);
109 - } catch (_e) {
110 - // Try one more time (plugins installed may not be fully loaded)
111 - return await processPlaceholders(patterns)
112 - // If this fails, just return the original patterns
113 - .catch(() => patterns);
114 - }
221 + return await processWithSecondPass(processPlaceholders, patterns);
115 222 };
116 223
117 -export const processPlaceholders = (patterns) =>
118 - apiFetch({
224 +// This endpoint installs pattern dependencies from PHP, outside the queue.
225 +export const processPlaceholders = async (patterns) => {
226 + await waitForInstalls();
227 + return apiFetch({
119 228 path: '/extendify/v1/shared/process-placeholders',
120 229 method: 'POST',
121 230 data: { patterns },
122 231 });
232 +};