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 +99 -39 3.1.3 → trunk View file →
@@ -1,7 +1,11 @@
1 1 import { recordPluginActivity } from '@shared/api/DataApi';
2 2 import { digest } from '@shared/api/digest';
3 3 import { enableAutoUpdate } from '@shared/api/wp';
4 +import {
5 + failedDependencies,
6 + processWithSecondPass,
7 +} from '@shared/lib/patterns';
4 8 import apiFetch from '@wordpress/api-fetch';
5 9 import { addQueryArgs } from '@wordpress/url';
6 10
7 11 export const getActivePlugins = () =>
@@ -9,9 +13,34 @@
9 13
10 14 export const alreadyActive = (activePlugins, pluginSlug) =>
11 15 activePlugins?.filter((p) => p.includes(pluginSlug))?.length;
12 16
13 -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) => {
14 43 const fn = async () => {
15 44 const p = await apiFetch({
16 45 path: '/wp/v2/plugins',
17 46 method: 'POST',
@@ -16,10 +45,11 @@
16 45 path: '/wp/v2/plugins',
17 46 method: 'POST',
18 47 data: { slug },
19 48 });
20 - await recordPluginActivity({ slug, source: 'auto-launch' });
21 - 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);
22 52 return p;
23 53 };
24 54 try {
25 55 return await fn();
@@ -39,8 +69,18 @@
39 69 }
40 70 }
41 71 };
42 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 +
43 83 export const getPlugin = async (slug) => {
44 84 const response = await apiFetch({
45 85 path: addQueryArgs('/wp/v2/plugins', { search: slug }),
46 86 });
@@ -73,20 +113,31 @@
73 113 }
74 114 }
75 115 };
76 116
77 -// Isolates per-plugin failures so one bad plugin can't block the rest;
78 -// returns the slugs that never went active.
79 -export const ensurePluginsActive = async (
80 - slugs,
81 - { installedSlugs = [] } = {},
82 -) => {
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 ?? [];
83 136 const failed = [];
84 - for (const slug of slugs) {
137 + for (const slug of missing) {
85 138 try {
86 - const plugin = installedSlugs.includes(slug)
87 - ? null
88 - : await installPlugin(slug);
139 + const plugin = onDisk.includes(slug) ? null : await installPlugin(slug);
89 140 const activated = await activatePlugin(plugin?.plugin ?? slug);
90 141 if (!activated) failed.push(slug);
91 142 } catch (_) {
92 143 failed.push(slug);
@@ -94,28 +145,41 @@
94 145 }
95 146 return { failed };
96 147 };
97 148
98 -// Last-chance guarantee before dependent work builds on these plugins: the
99 -// optimistic install pass is best-effort and unverified.
100 -export const verifyPluginsActive = async (
101 - slugs,
102 - { installedSlugs = [] } = {},
103 -) => {
104 - const activePlugins = await getActivePlugins();
105 - // Exact slug match here (not alreadyActive's substring test): matching
106 - // woocommerce against an active woocommerce-payments would skip a real miss.
107 - const missing = slugs.filter(
108 - (slug) => !activePlugins?.some((path) => path.split('/')[0] === slug),
109 - );
110 - if (!missing.length) return;
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;
111 153
112 - const { failed } = await ensurePluginsActive(missing, { installedSlugs });
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 + ];
113 171 if (!failed.length) return;
114 172
115 173 digest({
116 - error: { message: `Plugins inactive after verify: ${failed.join(', ')}` },
117 - details: { source: 'auto-launch', caller: 'verifyPluginsActive', failed },
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 + },
118 182 });
119 183 };
120 184
121 185 // Currently this only processes patterns with placeholders
@@ -153,20 +217,16 @@
153 217 source: 'auto-launch',
154 218 });
155 219 }
156 220
157 - try {
158 - return await processPlaceholders(patterns);
159 - } catch (_e) {
160 - // Try one more time (plugins installed may not be fully loaded)
161 - return await processPlaceholders(patterns)
162 - // If this fails, just return the original patterns
163 - .catch(() => patterns);
164 - }
221 + return await processWithSecondPass(processPlaceholders, patterns);
165 222 };
166 223
167 -export const processPlaceholders = (patterns) =>
168 - apiFetch({
224 +// This endpoint installs pattern dependencies from PHP, outside the queue.
225 +export const processPlaceholders = async (patterns) => {
226 + await waitForInstalls();
227 + return apiFetch({
169 228 path: '/extendify/v1/shared/process-placeholders',
170 229 method: 'POST',
171 230 data: { patterns },
172 231 });
232 +};