PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
← All changes | src/AutoLaunch/functions/plugins.js +106 -48 3.1.3 → 3.2.2 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 });
@@ -57,36 +97,45 @@
57 97
58 98 try {
59 99 await fn(slug);
60 100 return true;
61 - } catch (_) {
62 - try {
63 - // try once more but get the slug first
64 - const { plugin } = await getPlugin(slug);
65 - await fn(plugin);
66 - return true;
67 - } catch (error) {
101 + } catch (error) {
102 + const report = (e) => {
68 103 digest({
69 - error,
104 + error: e,
70 105 details: { source: 'auto-launch', caller: 'activatePlugin' },
71 106 });
72 107 return false;
73 - }
108 + };
109 + const found = await getPlugin(slug).catch(() => undefined);
110 + if (!found?.plugin) return report(error);
111 + return fn(found.plugin).then(() => true, report);
74 112 }
75 113 };
76 114
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 -) => {
115 +// Exact match, not substring: woocommerce-payments would mask a missing woocommerce.
116 +const notActive = (activePlugins, slugs) =>
117 + slugs.filter(
118 + (slug) => !activePlugins?.some((path) => path.split('/')[0] === slug),
119 + );
120 +
121 +export const ensurePluginsActive = async (slugs) => {
122 + // Two callers don't await this; a throw here silently skips every install.
123 + const active = await getActivePlugins().catch((error) => {
124 + digest({
125 + error,
126 + details: { source: 'auto-launch', caller: 'ensurePluginsActive' },
127 + });
128 + return [];
129 + });
130 + const missing = notActive(active, slugs);
131 + if (!missing.length) return { failed: [] };
132 +
133 + const onDisk = window.extSharedData?.installedPluginsSlugs ?? [];
83 134 const failed = [];
84 - for (const slug of slugs) {
135 + for (const slug of missing) {
85 136 try {
86 - const plugin = installedSlugs.includes(slug)
87 - ? null
88 - : await installPlugin(slug);
137 + const plugin = onDisk.includes(slug) ? null : await installPlugin(slug);
89 138 const activated = await activatePlugin(plugin?.plugin ?? slug);
90 139 if (!activated) failed.push(slug);
91 140 } catch (_) {
92 141 failed.push(slug);
@@ -94,28 +143,41 @@
94 143 }
95 144 return { failed };
96 145 };
97 146
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;
147 +export const reportInactivePlugins = async (slugs) => {
148 + // Falling back to an empty list would name every plugin as inactive.
149 + const active = await getActivePlugins().catch(() => null);
150 + if (!active) return;
111 151
112 - const { failed } = await ensurePluginsActive(missing, { installedSlugs });
152 + const inactive = notActive(active, slugs);
153 + if (!inactive.length) return;
154 +
155 + digest({
156 + error: { message: `Plugins inactive after launch: ${inactive.join(', ')}` },
157 + details: {
158 + source: 'auto-launch',
159 + caller: 'reportInactivePlugins',
160 + inactive,
161 + },
162 + });
163 +};
164 +
165 +export const reportFailedDependencies = (pages) => {
166 + const failed = [
167 + ...new Set(pages.flatMap(({ patterns }) => failedDependencies(patterns))),
168 + ];
113 169 if (!failed.length) return;
114 170
115 171 digest({
116 - error: { message: `Plugins inactive after verify: ${failed.join(', ')}` },
117 - details: { source: 'auto-launch', caller: 'verifyPluginsActive', failed },
172 + error: {
173 + message: `Patterns left static after a failed dependency: ${failed.join(', ')}`,
174 + },
175 + details: {
176 + source: 'auto-launch',
177 + caller: 'reportFailedDependencies',
178 + failed,
179 + },
118 180 });
119 181 };
120 182
121 183 // Currently this only processes patterns with placeholders
@@ -153,20 +215,16 @@
153 215 source: 'auto-launch',
154 216 });
155 217 }
156 218
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 - }
219 + return await processWithSecondPass(processPlaceholders, patterns);
165 220 };
166 221
167 -export const processPlaceholders = (patterns) =>
168 - apiFetch({
222 +// This endpoint installs pattern dependencies from PHP, outside the queue.
223 +export const processPlaceholders = async (patterns) => {
224 + await waitForInstalls();
225 + return apiFetch({
169 226 path: '/extendify/v1/shared/process-placeholders',
170 227 method: 'POST',
171 228 data: { patterns },
172 229 });
230 +};