@omega.js/backend 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +98 -0
- package/README.md +1053 -0
- package/bin/omega +2 -0
- package/bin/omega-backend +2 -0
- package/bin/omg +2 -0
- package/cli.js +3 -0
- package/dist/cli/command-table.js +268 -0
- package/dist/cli/commands/auth.js +259 -0
- package/dist/cli/commands/base-command.js +451 -0
- package/dist/cli/commands/build.js +27 -0
- package/dist/cli/commands/clean.js +13 -0
- package/dist/cli/commands/clear.js +11 -0
- package/dist/cli/commands/cwd.js +9 -0
- package/dist/cli/commands/deploy.js +205 -0
- package/dist/cli/commands/emulator-orphans.js +440 -0
- package/dist/cli/commands/emulator.js +1829 -0
- package/dist/cli/commands/firebase-init.js +101 -0
- package/dist/cli/commands/firestore.js +261 -0
- package/dist/cli/commands/indexes.js +51 -0
- package/dist/cli/commands/install.js +151 -0
- package/dist/cli/commands/logs.js +358 -0
- package/dist/cli/commands/mcp.js +41 -0
- package/dist/cli/commands/migrate-markers.js +176 -0
- package/dist/cli/commands/migrate-rules.js +90 -0
- package/dist/cli/commands/migrate.js +59 -0
- package/dist/cli/commands/serve.js +208 -0
- package/dist/cli/commands/setup-tests/base-test.js +174 -0
- package/dist/cli/commands/setup-tests/emulator-config.js +109 -0
- package/dist/cli/commands/setup-tests/env-runtime-config-deprecated.js +47 -0
- package/dist/cli/commands/setup-tests/firebase-admin.js +49 -0
- package/dist/cli/commands/setup-tests/firebase-auth.js +26 -0
- package/dist/cli/commands/setup-tests/firebase-cli.js +26 -0
- package/dist/cli/commands/setup-tests/firebase-functions.js +49 -0
- package/dist/cli/commands/setup-tests/firestore-indexes-file.js +72 -0
- package/dist/cli/commands/setup-tests/firestore-indexes-in-json.js +20 -0
- package/dist/cli/commands/setup-tests/firestore-indexes-required.js +108 -0
- package/dist/cli/commands/setup-tests/firestore-indexes-synced.js +236 -0
- package/dist/cli/commands/setup-tests/firestore-rules-file.js +115 -0
- package/dist/cli/commands/setup-tests/firestore-rules-in-json.js +49 -0
- package/dist/cli/commands/setup-tests/functions-package.js +44 -0
- package/dist/cli/commands/setup-tests/gcloud-cli.js +26 -0
- package/dist/cli/commands/setup-tests/gitignore.js +122 -0
- package/dist/cli/commands/setup-tests/helpers/merge-line-files.js +28 -0
- package/dist/cli/commands/setup-tests/helpers/required-indexes.js +109 -0
- package/dist/cli/commands/setup-tests/helpers/seed-campaigns.js +246 -0
- package/dist/cli/commands/setup-tests/helpers.js +72 -0
- package/dist/cli/commands/setup-tests/hosting-folder.js +23 -0
- package/dist/cli/commands/setup-tests/hosting-rewrites.js +52 -0
- package/dist/cli/commands/setup-tests/index.js +111 -0
- package/dist/cli/commands/setup-tests/is-firebase-project.js +21 -0
- package/dist/cli/commands/setup-tests/java-installed.js +27 -0
- package/dist/cli/commands/setup-tests/legacy-tests-cleanup.js +43 -0
- package/dist/cli/commands/setup-tests/marketing-campaigns-seeded.js +230 -0
- package/dist/cli/commands/setup-tests/node-version.js +60 -0
- package/dist/cli/commands/setup-tests/npm-project-scripts.js +42 -0
- package/dist/cli/commands/setup-tests/nvmrc-version.js +36 -0
- package/dist/cli/commands/setup-tests/omega-backend.js +42 -0
- package/dist/cli/commands/setup-tests/omega-config.js +84 -0
- package/dist/cli/commands/setup-tests/project-directories.js +34 -0
- package/dist/cli/commands/setup-tests/project-id-consistency.js +201 -0
- package/dist/cli/commands/setup-tests/public-html-files.js +22 -0
- package/dist/cli/commands/setup-tests/realtime-rules-file.js +69 -0
- package/dist/cli/commands/setup-tests/realtime-rules-in-json.js +20 -0
- package/dist/cli/commands/setup-tests/remoteconfig-template-file.js +32 -0
- package/dist/cli/commands/setup-tests/remoteconfig-template-in-json.js +31 -0
- package/dist/cli/commands/setup-tests/service-account.js +86 -0
- package/dist/cli/commands/setup-tests/storage-lifecycle-policy.js +81 -0
- package/dist/cli/commands/setup-tests/storage-rules-file.js +32 -0
- package/dist/cli/commands/setup-tests/storage-rules-in-json.js +20 -0
- package/dist/cli/commands/stripe.js +14 -0
- package/dist/cli/commands/test-lanes/stripe-live.js +440 -0
- package/dist/cli/commands/test.js +976 -0
- package/dist/cli/commands/update.js +27 -0
- package/dist/cli/commands/version.js +10 -0
- package/dist/cli/commands/watch.js +196 -0
- package/dist/cli/flags.js +13 -0
- package/dist/cli/index.js +173 -0
- package/dist/cli/run.js +33 -0
- package/dist/cli/utils/attach-log-file.js +4 -0
- package/dist/cli/utils/compile-rules.js +1155 -0
- package/dist/cli/utils/ensure-target.js +314 -0
- package/dist/cli/utils/project-type.js +140 -0
- package/dist/cli/utils/public-files.js +49 -0
- package/dist/cli/utils/safe-install.js +4 -0
- package/dist/cli/utils/spawn-shell.js +94 -0
- package/dist/cli/utils/stage-functions.js +338 -0
- package/dist/cli/utils/stage-local-packages.js +210 -0
- package/dist/cli/utils/target-checks.js +161 -0
- package/dist/cli/utils/target.js +24 -0
- package/dist/cli/utils/ui.js +292 -0
- package/dist/defaults/AGENTS.md +125 -0
- package/dist/defaults/CHANGELOG.md +15 -0
- package/dist/defaults/CLAUDE.md +1 -0
- package/dist/defaults/_.gitignore +65 -0
- package/dist/defaults/docs/README.md +17 -0
- package/dist/defaults/test/README.md +65 -0
- package/dist/defaults/test/_init.js +14 -0
- package/dist/defaults/test/helpers/connect-trap.js +72 -0
- package/dist/defaults/test/unit/registration.test.js +143 -0
- package/dist/defaults/test/unit/rules-posture.test.js +101 -0
- package/dist/defaults/test/unit/socket-free.test.js +43 -0
- package/dist/manager/events/auth/before-create.js +89 -0
- package/dist/manager/events/auth/before-signin.js +76 -0
- package/dist/manager/events/auth/on-create.js +112 -0
- package/dist/manager/events/auth/on-delete.js +141 -0
- package/dist/manager/events/auth/utils.js +93 -0
- package/dist/manager/events/cron/daily/blog-auto-publisher.js +287 -0
- package/dist/manager/events/cron/daily/data-requests.js +57 -0
- package/dist/manager/events/cron/daily/expire-paypal-cancellations.js +191 -0
- package/dist/manager/events/cron/daily/marketing-prune.js +486 -0
- package/dist/manager/events/cron/daily/reset-usage.js +188 -0
- package/dist/manager/events/cron/daily/trial-lapse-sweep.js +355 -0
- package/dist/manager/events/cron/daily.js +3 -0
- package/dist/manager/events/cron/frequent/abandoned-carts.js +204 -0
- package/dist/manager/events/cron/frequent/email-queue.js +71 -0
- package/dist/manager/events/cron/frequent/marketing-campaigns.js +463 -0
- package/dist/manager/events/cron/frequent/retry-failed-webhooks.js +70 -0
- package/dist/manager/events/cron/frequent.js +3 -0
- package/dist/manager/events/cron/runner.js +64 -0
- package/dist/manager/events/firestore/notifications/on-write.js +123 -0
- package/dist/manager/events/firestore/payments-disputes/on-write.js +256 -0
- package/dist/manager/events/firestore/payments-disputes/providers/stripe.js +218 -0
- package/dist/manager/events/firestore/payments-disputes/providers/test.js +231 -0
- package/dist/manager/events/firestore/payments-webhooks/analytics.js +687 -0
- package/dist/manager/events/firestore/payments-webhooks/on-write.js +1224 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/index.js +395 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +58 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +28 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-refunded.js +30 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/send-email.js +74 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-removed.js +14 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +28 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/checkout-declined.js +18 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +67 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +25 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +26 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-refunded.js +34 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +42 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +33 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-winback.js +17 -0
- package/dist/manager/functions/_legacy/actions/create-post-handler.js +188 -0
- package/dist/manager/functions/_legacy/actions/generate-uuid.js +63 -0
- package/dist/manager/functions/_legacy/actions/sign-up-handler.js +205 -0
- package/dist/manager/functions/_legacy/admin/create-post.js +207 -0
- package/dist/manager/functions/_legacy/admin/firestore-write.js +72 -0
- package/dist/manager/functions/_legacy/admin/get-stats.js +218 -0
- package/dist/manager/functions/_legacy/admin/query.js +198 -0
- package/dist/manager/functions/_legacy/admin/send-notification.js +206 -0
- package/dist/manager/functions/_legacy/template.js +33 -0
- package/dist/manager/functions/_legacy/test/authenticate.js +40 -0
- package/dist/manager/functions/_legacy/test/webhook.js +37 -0
- package/dist/manager/functions/wrappers/mailchimp/addToList.js +25 -0
- package/dist/manager/helpers/analytics.js +498 -0
- package/dist/manager/helpers/api-manager.js +313 -0
- package/dist/manager/helpers/backend-router.js +37 -0
- package/dist/manager/helpers/context/authenticate.js +215 -0
- package/dist/manager/helpers/context/client-info.js +91 -0
- package/dist/manager/helpers/context/index.js +228 -0
- package/dist/manager/helpers/context/logging.js +150 -0
- package/dist/manager/helpers/context/parse.js +160 -0
- package/dist/manager/helpers/context/respond.js +247 -0
- package/dist/manager/helpers/event-middleware.js +109 -0
- package/dist/manager/helpers/metadata.js +32 -0
- package/dist/manager/helpers/middleware.js +541 -0
- package/dist/manager/helpers/redact-secret.js +25 -0
- package/dist/manager/helpers/resolved-config.js +34 -0
- package/dist/manager/helpers/roles.js +69 -0
- package/dist/manager/helpers/safe-compare.js +31 -0
- package/dist/manager/helpers/schema-engine.js +314 -0
- package/dist/manager/helpers/schema-zod.js +203 -0
- package/dist/manager/helpers/settings.js +215 -0
- package/dist/manager/helpers/usage.js +541 -0
- package/dist/manager/helpers/user.js +57 -0
- package/dist/manager/helpers/utilities.js +565 -0
- package/dist/manager/index.js +1404 -0
- package/dist/manager/libraries/abandoned-cart-config.js +12 -0
- package/dist/manager/libraries/ai/index.js +321 -0
- package/dist/manager/libraries/ai/prompt.js +85 -0
- package/dist/manager/libraries/ai/providers/anthropic-format.js +297 -0
- package/dist/manager/libraries/ai/providers/anthropic.js +169 -0
- package/dist/manager/libraries/ai/providers/claude-code.js +183 -0
- package/dist/manager/libraries/ai/providers/openai.js +1272 -0
- package/dist/manager/libraries/ai/providers/test.js +257 -0
- package/dist/manager/libraries/ai/tokens.js +54 -0
- package/dist/manager/libraries/analytics/conversions.js +478 -0
- package/dist/manager/libraries/analytics/match-data.js +573 -0
- package/dist/manager/libraries/analytics/signup.js +120 -0
- package/dist/manager/libraries/auth-user.js +46 -0
- package/dist/manager/libraries/content/feed-parser.js +198 -0
- package/dist/manager/libraries/content/ghostii.js +191 -0
- package/dist/manager/libraries/content/source-resolver.js +629 -0
- package/dist/manager/libraries/email/constants.js +532 -0
- package/dist/manager/libraries/email/data/blocked-local-parts.json +55 -0
- package/dist/manager/libraries/email/data/blocked-local-patterns.js +14 -0
- package/dist/manager/libraries/email/data/corporate-domains.json +23 -0
- package/dist/manager/libraries/email/data/custom-disposable-domains.json +58 -0
- package/dist/manager/libraries/email/data/disposable-domains.json +8184 -0
- package/dist/manager/libraries/email/data/typo-domains.js +83 -0
- package/dist/manager/libraries/email/disposable-domains.js +92 -0
- package/dist/manager/libraries/email/generators/lib/filter.js +179 -0
- package/dist/manager/libraries/email/generators/lib/image-host.js +344 -0
- package/dist/manager/libraries/email/generators/lib/image-illustrator.js +154 -0
- package/dist/manager/libraries/email/generators/lib/markdown-renderer.js +294 -0
- package/dist/manager/libraries/email/generators/lib/mjml-template.js +120 -0
- package/dist/manager/libraries/email/generators/lib/structure.js +299 -0
- package/dist/manager/libraries/email/generators/lib/svg-illustrator.js +197 -0
- package/dist/manager/libraries/email/generators/lib/templates/base.js +229 -0
- package/dist/manager/libraries/email/generators/lib/templates/card.js +33 -0
- package/dist/manager/libraries/email/generators/lib/templates/classic-schema.js +59 -0
- package/dist/manager/libraries/email/generators/lib/templates/clean.js +82 -0
- package/dist/manager/libraries/email/generators/lib/templates/editorial/helpers.js +100 -0
- package/dist/manager/libraries/email/generators/lib/templates/editorial/index.js +318 -0
- package/dist/manager/libraries/email/generators/lib/templates/feedback.js +101 -0
- package/dist/manager/libraries/email/generators/lib/templates/field-report/helpers.js +138 -0
- package/dist/manager/libraries/email/generators/lib/templates/field-report/index.js +494 -0
- package/dist/manager/libraries/email/generators/lib/templates/index.js +47 -0
- package/dist/manager/libraries/email/generators/lib/templates/newsletter-shared.js +610 -0
- package/dist/manager/libraries/email/generators/lib/templates/order.js +461 -0
- package/dist/manager/libraries/email/generators/lib/templates/plain.js +83 -0
- package/dist/manager/libraries/email/generators/lib/templates/shared-campaign.js +64 -0
- package/dist/manager/libraries/email/generators/newsletter.js +899 -0
- package/dist/manager/libraries/email/index.js +146 -0
- package/dist/manager/libraries/email/marketing/index.js +754 -0
- package/dist/manager/libraries/email/prepare.js +365 -0
- package/dist/manager/libraries/email/providers/beehiiv.js +589 -0
- package/dist/manager/libraries/email/providers/sendgrid.js +845 -0
- package/dist/manager/libraries/email/transactional/index.js +510 -0
- package/dist/manager/libraries/email/utm.js +86 -0
- package/dist/manager/libraries/email/validation-provider-neverbounce.js +70 -0
- package/dist/manager/libraries/email/validation-provider-zerobounce.js +42 -0
- package/dist/manager/libraries/email/validation.js +255 -0
- package/dist/manager/libraries/env.js +198 -0
- package/dist/manager/libraries/infer-contact.js +101 -0
- package/dist/manager/libraries/load-provider.js +65 -0
- package/dist/manager/libraries/notification.js +256 -0
- package/dist/manager/libraries/openai.js +13 -0
- package/dist/manager/libraries/payment/discount-codes.js +204 -0
- package/dist/manager/libraries/payment/fetch-failure.js +56 -0
- package/dist/manager/libraries/payment/license.js +44 -0
- package/dist/manager/libraries/payment/order-id.js +57 -0
- package/dist/manager/libraries/payment/provider-errors.js +71 -0
- package/dist/manager/libraries/payment/providers/chargebee.js +817 -0
- package/dist/manager/libraries/payment/providers/coinbase.js +330 -0
- package/dist/manager/libraries/payment/providers/paypal.js +987 -0
- package/dist/manager/libraries/payment/providers/stripe.js +755 -0
- package/dist/manager/libraries/payment/providers/test.js +250 -0
- package/dist/manager/libraries/payment/refund-linkage.js +78 -0
- package/dist/manager/libraries/payment/refund-policy.js +113 -0
- package/dist/manager/libraries/payment/winback.js +55 -0
- package/dist/manager/libraries/prompts/infer-contact.md +78 -0
- package/dist/manager/libraries/rate-limits.js +17 -0
- package/dist/manager/libraries/recaptcha.js +58 -0
- package/dist/manager/libraries/user-doc.js +234 -0
- package/dist/manager/routes/admin/backup/post.js +106 -0
- package/dist/manager/routes/admin/cron/post.js +37 -0
- package/dist/manager/routes/admin/database/get.js +35 -0
- package/dist/manager/routes/admin/database/post.js +34 -0
- package/dist/manager/routes/admin/email/post.js +74 -0
- package/dist/manager/routes/admin/firestore/get.js +35 -0
- package/dist/manager/routes/admin/firestore/post.js +56 -0
- package/dist/manager/routes/admin/firestore/query/post.js +128 -0
- package/dist/manager/routes/admin/hook/post.js +104 -0
- package/dist/manager/routes/admin/infer-contact/post.js +35 -0
- package/dist/manager/routes/admin/notification/post.js +37 -0
- package/dist/manager/routes/admin/payment/post.js +56 -0
- package/dist/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
- package/dist/manager/routes/admin/post/dispatch-deploy.js +36 -0
- package/dist/manager/routes/admin/post/post.js +487 -0
- package/dist/manager/routes/admin/post/put.js +146 -0
- package/dist/manager/routes/admin/post/templates/post.html +14 -0
- package/dist/manager/routes/admin/repo/content/post.js +106 -0
- package/dist/manager/routes/admin/stats/get.js +222 -0
- package/dist/manager/routes/admin/users/disable/post.js +50 -0
- package/dist/manager/routes/admin/users/list/get.js +110 -0
- package/dist/manager/routes/admin/users/sync/post.js +119 -0
- package/dist/manager/routes/brand/get.js +26 -0
- package/dist/manager/routes/content/post/get.js +110 -0
- package/dist/manager/routes/general/email/post.js +89 -0
- package/dist/manager/routes/general/email/templates/general/download-app-link.js +42 -0
- package/dist/manager/routes/general/uuid/post.js +31 -0
- package/dist/manager/routes/handler/post/post.js +142 -0
- package/dist/manager/routes/health/get.js +43 -0
- package/dist/manager/routes/index.js +11 -0
- package/dist/manager/routes/marketing/campaign/delete.js +45 -0
- package/dist/manager/routes/marketing/campaign/get.js +69 -0
- package/dist/manager/routes/marketing/campaign/post.js +124 -0
- package/dist/manager/routes/marketing/campaign/put.js +86 -0
- package/dist/manager/routes/marketing/campaign/utils.js +59 -0
- package/dist/manager/routes/marketing/contact/delete.js +100 -0
- package/dist/manager/routes/marketing/contact/post.js +149 -0
- package/dist/manager/routes/marketing/contact/put.js +37 -0
- package/dist/manager/routes/marketing/email-preferences/post.js +275 -0
- package/dist/manager/routes/marketing/webhook/forward/post.js +170 -0
- package/dist/manager/routes/marketing/webhook/post.js +123 -0
- package/dist/manager/routes/marketing/webhook/providers/beehiiv.js +197 -0
- package/dist/manager/routes/marketing/webhook/providers/sendgrid.js +194 -0
- package/dist/manager/routes/payments/cancel/_is-trialing.js +40 -0
- package/dist/manager/routes/payments/cancel/post.js +149 -0
- package/dist/manager/routes/payments/cancel/providers/chargebee.js +41 -0
- package/dist/manager/routes/payments/cancel/providers/paypal.js +56 -0
- package/dist/manager/routes/payments/cancel/providers/stripe.js +33 -0
- package/dist/manager/routes/payments/cancel/providers/test.js +118 -0
- package/dist/manager/routes/payments/discount/get.js +25 -0
- package/dist/manager/routes/payments/dispute-alert/post.js +117 -0
- package/dist/manager/routes/payments/dispute-alert/providers/chargeblast.js +55 -0
- package/dist/manager/routes/payments/intent/post.js +341 -0
- package/dist/manager/routes/payments/intent/providers/chargebee.js +197 -0
- package/dist/manager/routes/payments/intent/providers/coinbase.js +94 -0
- package/dist/manager/routes/payments/intent/providers/paypal.js +244 -0
- package/dist/manager/routes/payments/intent/providers/stripe.js +141 -0
- package/dist/manager/routes/payments/intent/providers/test.js +341 -0
- package/dist/manager/routes/payments/plan/post.js +147 -0
- package/dist/manager/routes/payments/plan/providers/chargebee.js +62 -0
- package/dist/manager/routes/payments/plan/providers/paypal.js +62 -0
- package/dist/manager/routes/payments/plan/providers/stripe.js +59 -0
- package/dist/manager/routes/payments/plan/providers/test.js +113 -0
- package/dist/manager/routes/payments/portal/post.js +113 -0
- package/dist/manager/routes/payments/portal/providers/chargebee.js +62 -0
- package/dist/manager/routes/payments/portal/providers/paypal.js +25 -0
- package/dist/manager/routes/payments/portal/providers/stripe.js +62 -0
- package/dist/manager/routes/payments/portal/providers/test.js +18 -0
- package/dist/manager/routes/payments/refund/post.js +207 -0
- package/dist/manager/routes/payments/refund/providers/chargebee.js +147 -0
- package/dist/manager/routes/payments/refund/providers/coinbase.js +40 -0
- package/dist/manager/routes/payments/refund/providers/paypal.js +239 -0
- package/dist/manager/routes/payments/refund/providers/stripe.js +160 -0
- package/dist/manager/routes/payments/refund/providers/test.js +204 -0
- package/dist/manager/routes/payments/trial-eligibility/get.js +29 -0
- package/dist/manager/routes/payments/uncancel/post.js +106 -0
- package/dist/manager/routes/payments/uncancel/providers/chargebee.js +30 -0
- package/dist/manager/routes/payments/uncancel/providers/paypal.js +25 -0
- package/dist/manager/routes/payments/uncancel/providers/stripe.js +27 -0
- package/dist/manager/routes/payments/uncancel/providers/test.js +114 -0
- package/dist/manager/routes/payments/webhook/post.js +157 -0
- package/dist/manager/routes/payments/webhook/providers/chargebee.js +218 -0
- package/dist/manager/routes/payments/webhook/providers/coinbase.js +91 -0
- package/dist/manager/routes/payments/webhook/providers/paypal.js +214 -0
- package/dist/manager/routes/payments/webhook/providers/stripe.js +182 -0
- package/dist/manager/routes/payments/webhook/providers/test.js +18 -0
- package/dist/manager/routes/payments/winback/post.js +276 -0
- package/dist/manager/routes/payments/winback/providers/chargebee.js +24 -0
- package/dist/manager/routes/payments/winback/providers/paypal.js +22 -0
- package/dist/manager/routes/payments/winback/providers/stripe.js +41 -0
- package/dist/manager/routes/payments/winback/providers/test.js +42 -0
- package/dist/manager/routes/restart/index.js +37 -0
- package/dist/manager/routes/special/electron-client/post.js +59 -0
- package/dist/manager/routes/test/authenticate/get.js +11 -0
- package/dist/manager/routes/test/health/get.js +8 -0
- package/dist/manager/routes/test/index.js +16 -0
- package/dist/manager/routes/test/lab/post.js +17 -0
- package/dist/manager/routes/test/redirect/get.js +27 -0
- package/dist/manager/routes/test/reset-account/post.js +79 -0
- package/dist/manager/routes/test/roster/get.js +40 -0
- package/dist/manager/routes/test/schema/post.js +20 -0
- package/dist/manager/routes/test/usage/post.js +44 -0
- package/dist/manager/routes/test/webhook/post.js +21 -0
- package/dist/manager/routes/user/api-keys/post.js +51 -0
- package/dist/manager/routes/user/connections/_context.js +194 -0
- package/dist/manager/routes/user/connections/_grant.js +247 -0
- package/dist/manager/routes/user/connections/_lease.js +184 -0
- package/dist/manager/routes/user/connections/_providers.js +191 -0
- package/dist/manager/routes/user/connections/_state.js +117 -0
- package/dist/manager/routes/user/connections/delete.js +55 -0
- package/dist/manager/routes/user/connections/get.js +167 -0
- package/dist/manager/routes/user/connections/post.js +370 -0
- package/dist/manager/routes/user/connections/providers/discord.js +41 -0
- package/dist/manager/routes/user/connections/providers/google.js +55 -0
- package/dist/manager/routes/user/connections/providers/kick.js +68 -0
- package/dist/manager/routes/user/connections/providers/spotify.js +40 -0
- package/dist/manager/routes/user/connections/providers/twitch.js +52 -0
- package/dist/manager/routes/user/data-request/delete.js +84 -0
- package/dist/manager/routes/user/data-request/get.js +229 -0
- package/dist/manager/routes/user/data-request/post.js +136 -0
- package/dist/manager/routes/user/delete.js +149 -0
- package/dist/manager/routes/user/feedback/post.js +81 -0
- package/dist/manager/routes/user/get.js +14 -0
- package/dist/manager/routes/user/orders/get.js +95 -0
- package/dist/manager/routes/user/sessions/delete.js +95 -0
- package/dist/manager/routes/user/sessions/get.js +40 -0
- package/dist/manager/routes/user/settings/validate/post.js +86 -0
- package/dist/manager/routes/user/signup/post.js +662 -0
- package/dist/manager/routes/user/subscription/get.js +77 -0
- package/dist/manager/routes/user/token/post.js +30 -0
- package/dist/manager/routes/verts/delete.js +43 -0
- package/dist/manager/routes/verts/get.js +49 -0
- package/dist/manager/routes/verts/post.js +61 -0
- package/dist/manager/routes/verts/put.js +75 -0
- package/dist/manager/routes/verts/redirect/get.js +36 -0
- package/dist/manager/routes/verts/serve/get.js +67 -0
- package/dist/manager/routes/verts/utils.js +436 -0
- package/dist/manager/schemas/admin/backup/post.js +8 -0
- package/dist/manager/schemas/admin/cron/post.js +8 -0
- package/dist/manager/schemas/admin/database/get.js +5 -0
- package/dist/manager/schemas/admin/database/post.js +6 -0
- package/dist/manager/schemas/admin/email/post.js +31 -0
- package/dist/manager/schemas/admin/firestore/get.js +5 -0
- package/dist/manager/schemas/admin/firestore/post.js +8 -0
- package/dist/manager/schemas/admin/firestore/query/post.js +5 -0
- package/dist/manager/schemas/admin/hook/post.js +8 -0
- package/dist/manager/schemas/admin/infer-contact/post.js +6 -0
- package/dist/manager/schemas/admin/notification/post.js +23 -0
- package/dist/manager/schemas/admin/payment/post.js +8 -0
- package/dist/manager/schemas/admin/post/post.js +23 -0
- package/dist/manager/schemas/admin/post/put.js +13 -0
- package/dist/manager/schemas/admin/repo/content/post.js +10 -0
- package/dist/manager/schemas/admin/stats/get.js +5 -0
- package/dist/manager/schemas/admin/users/disable/post.js +9 -0
- package/dist/manager/schemas/admin/users/list/get.js +10 -0
- package/dist/manager/schemas/admin/users/sync/post.js +7 -0
- package/dist/manager/schemas/brand/get.js +3 -0
- package/dist/manager/schemas/content/post/get.js +8 -0
- package/dist/manager/schemas/general/email/post.js +10 -0
- package/dist/manager/schemas/general/uuid/post.js +9 -0
- package/dist/manager/schemas/handler/post/post.js +13 -0
- package/dist/manager/schemas/health/get.js +3 -0
- package/dist/manager/schemas/marketing/campaign/delete.js +8 -0
- package/dist/manager/schemas/marketing/campaign/get.js +13 -0
- package/dist/manager/schemas/marketing/campaign/post.js +43 -0
- package/dist/manager/schemas/marketing/campaign/put.js +37 -0
- package/dist/manager/schemas/marketing/contact/delete.js +8 -0
- package/dist/manager/schemas/marketing/contact/post.js +14 -0
- package/dist/manager/schemas/marketing/contact/put.js +8 -0
- package/dist/manager/schemas/marketing/email-preferences/post.js +15 -0
- package/dist/manager/schemas/marketing/webhook/forward/post.js +8 -0
- package/dist/manager/schemas/marketing/webhook/post.js +7 -0
- package/dist/manager/schemas/payments/cancel/post.js +15 -0
- package/dist/manager/schemas/payments/discount/get.js +8 -0
- package/dist/manager/schemas/payments/dispute-alert/post.js +6 -0
- package/dist/manager/schemas/payments/intent/post.js +28 -0
- package/dist/manager/schemas/payments/plan/post.js +11 -0
- package/dist/manager/schemas/payments/portal/post.js +12 -0
- package/dist/manager/schemas/payments/refund/post.js +14 -0
- package/dist/manager/schemas/payments/trial-eligibility/get.js +7 -0
- package/dist/manager/schemas/payments/uncancel/post.js +9 -0
- package/dist/manager/schemas/payments/webhook/post.js +6 -0
- package/dist/manager/schemas/payments/winback/post.js +12 -0
- package/dist/manager/schemas/restart/index.js +5 -0
- package/dist/manager/schemas/special/electron-client/post.js +11 -0
- package/dist/manager/schemas/test/authenticate/get.js +3 -0
- package/dist/manager/schemas/test/health/get.js +3 -0
- package/dist/manager/schemas/test/index.js +5 -0
- package/dist/manager/schemas/test/lab/post.js +3 -0
- package/dist/manager/schemas/test/redirect/get.js +7 -0
- package/dist/manager/schemas/test/reset-account/post.js +3 -0
- package/dist/manager/schemas/test/roster/get.js +3 -0
- package/dist/manager/schemas/test/schema/post.js +70 -0
- package/dist/manager/schemas/test/usage/post.js +8 -0
- package/dist/manager/schemas/test/webhook/post.js +7 -0
- package/dist/manager/schemas/user/api-keys/post.js +6 -0
- package/dist/manager/schemas/user/connections/delete.js +8 -0
- package/dist/manager/schemas/user/connections/get.js +21 -0
- package/dist/manager/schemas/user/connections/post.js +12 -0
- package/dist/manager/schemas/user/data-request/delete.js +3 -0
- package/dist/manager/schemas/user/data-request/get.js +5 -0
- package/dist/manager/schemas/user/data-request/post.js +6 -0
- package/dist/manager/schemas/user/delete.js +6 -0
- package/dist/manager/schemas/user/feedback/post.js +8 -0
- package/dist/manager/schemas/user/get.js +3 -0
- package/dist/manager/schemas/user/orders/get.js +6 -0
- package/dist/manager/schemas/user/sessions/delete.js +6 -0
- package/dist/manager/schemas/user/sessions/get.js +6 -0
- package/dist/manager/schemas/user/settings/validate/post.js +8 -0
- package/dist/manager/schemas/user/signup/post.js +27 -0
- package/dist/manager/schemas/user/subscription/get.js +5 -0
- package/dist/manager/schemas/user/token/post.js +5 -0
- package/dist/manager/schemas/verts/delete.js +8 -0
- package/dist/manager/schemas/verts/get.js +9 -0
- package/dist/manager/schemas/verts/post.js +23 -0
- package/dist/manager/schemas/verts/put.js +20 -0
- package/dist/manager/schemas/verts/redirect/get.js +10 -0
- package/dist/manager/schemas/verts/serve/get.js +14 -0
- package/dist/manager/server-manager.js +69 -0
- package/dist/mcp/client.js +98 -0
- package/dist/mcp/handler.js +521 -0
- package/dist/mcp/index.js +157 -0
- package/dist/mcp/tools.js +555 -0
- package/dist/mcp/utils.js +108 -0
- package/dist/omega-bin.js +6 -0
- package/dist/require.js +3 -0
- package/dist/test/fixtures/firebase-project/.firebaserc +5 -0
- package/dist/test/fixtures/firebase-project/config/omega.json5 +131 -0
- package/dist/test/fixtures/firebase-project/database.rules.json +6 -0
- package/dist/test/fixtures/firebase-project/firebase.json +49 -0
- package/dist/test/fixtures/firebase-project/firestore.indexes.json +4 -0
- package/dist/test/fixtures/firebase-project/package.json +14 -0
- package/dist/test/fixtures/firebase-project/src/index.js +11 -0
- package/dist/test/fixtures/firebase-project/storage.rules +8 -0
- package/dist/test/parse-audit.js +25 -0
- package/dist/test/run-tests.js +90 -0
- package/dist/test/runner.js +982 -0
- package/dist/test/seed.js +242 -0
- package/dist/test/test-accounts.js +2660 -0
- package/dist/test/utils/assertions.js +189 -0
- package/dist/test/utils/email-capture.js +252 -0
- package/dist/test/utils/extended-mode-warning.js +11 -0
- package/dist/test/utils/firestore-rules-client.js +277 -0
- package/dist/test/utils/http-client.js +228 -0
- package/dist/test/utils/test-mode-file.js +192 -0
- package/dist/utils/merge-line-files.js +6 -0
- package/dist/utils/scaffold-defaults.js +71 -0
- package/dist/utils/test-lanes.js +62 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/analytics/adapters/ga4.js +26 -0
- package/dist/vendor/analytics/adapters/meta.js +26 -0
- package/dist/vendor/analytics/adapters/resolve.js +130 -0
- package/dist/vendor/analytics/adapters/tiktok.js +27 -0
- package/dist/vendor/analytics/catalog.js +908 -0
- package/dist/vendor/analytics/identity.js +136 -0
- package/dist/vendor/config/company.js +31 -0
- package/dist/vendor/config/defaults.js +173 -0
- package/dist/vendor/config/demo.js +18 -0
- package/dist/vendor/config/desktop-artifacts.js +110 -0
- package/dist/vendor/config/edit.js +769 -0
- package/dist/vendor/config/env-delivery.js +145 -0
- package/dist/vendor/config/env-rules.js +93 -0
- package/dist/vendor/config/env-schema.js +1078 -0
- package/dist/vendor/config/env.js +445 -0
- package/dist/vendor/config/hooks.js +97 -0
- package/dist/vendor/config/index.js +237 -0
- package/dist/vendor/config/instances.js +208 -0
- package/dist/vendor/config/load.js +490 -0
- package/dist/vendor/config/merge.js +68 -0
- package/dist/vendor/config/order.js +139 -0
- package/dist/vendor/config/ports.js +374 -0
- package/dist/vendor/config/providers.js +32 -0
- package/dist/vendor/config/repo.js +142 -0
- package/dist/vendor/config/retired-keys.js +430 -0
- package/dist/vendor/config/schema.js +1610 -0
- package/dist/vendor/config/secrets.js +50 -0
- package/dist/vendor/config/seed.js +34 -0
- package/dist/vendor/config/site-global.js +205 -0
- package/dist/vendor/config/validate.js +554 -0
- package/dist/vendor/config/winback.js +61 -0
- package/dist/vendor/devkit/attach-log-file.js +262 -0
- package/dist/vendor/devkit/bare-requires.js +153 -0
- package/dist/vendor/devkit/command-path.js +46 -0
- package/dist/vendor/devkit/defaults-engine.js +372 -0
- package/dist/vendor/devkit/deploy-record.js +180 -0
- package/dist/vendor/devkit/deploy.js +286 -0
- package/dist/vendor/devkit/license.js +155 -0
- package/dist/vendor/devkit/local-https.js +360 -0
- package/dist/vendor/devkit/local.js +1905 -0
- package/dist/vendor/devkit/logger.js +128 -0
- package/dist/vendor/devkit/merge-line-files.js +296 -0
- package/dist/vendor/devkit/npm-registry.js +52 -0
- package/dist/vendor/devkit/omega-bin.js +345 -0
- package/dist/vendor/devkit/parse-audit.js +74 -0
- package/dist/vendor/devkit/safe-install.js +18 -0
- package/dist/vendor/devkit/scaffold-guard.js +96 -0
- package/dist/vendor/devkit/stop-signals.js +28 -0
- package/dist/vendor/devkit/test/assert.js +120 -0
- package/dist/vendor/devkit/test/define-cases.js +104 -0
- package/dist/vendor/devkit/test/runner-core.js +554 -0
- package/dist/vendor/devkit/test/scope.js +162 -0
- package/dist/vendor/devkit/update.js +569 -0
- package/dist/vendor/monitoring/core.js +180 -0
- package/dist/vendor/monitoring/env.js +49 -0
- package/dist/vendor/monitoring/logger.js +39 -0
- package/dist/vendor/monitoring/node.js +72 -0
- package/docs/admin-post-route.md +57 -0
- package/docs/ai-library.md +171 -0
- package/docs/architecture.md +62 -0
- package/docs/audit.md +70 -0
- package/docs/auth-hooks.md +74 -0
- package/docs/build-system.md +65 -0
- package/docs/cdp-debugging.md +29 -0
- package/docs/cli-firestore-auth.md +85 -0
- package/docs/cli-logs.md +67 -0
- package/docs/cli-output.md +146 -0
- package/docs/code-patterns.md +77 -0
- package/docs/common-mistakes.md +12 -0
- package/docs/common-operations.md +60 -0
- package/docs/connections.md +212 -0
- package/docs/consent.md +362 -0
- package/docs/directory-structure.md +150 -0
- package/docs/email-system.md +459 -0
- package/docs/environment-detection.md +93 -0
- package/docs/file-naming.md +10 -0
- package/docs/firestore.md +134 -0
- package/docs/ghostii.md +240 -0
- package/docs/index.md +299 -0
- package/docs/key-files.md +37 -0
- package/docs/logging.md +60 -0
- package/docs/marketing-campaigns.md +407 -0
- package/docs/marketing-fields.md +25 -0
- package/docs/mcp.md +222 -0
- package/docs/migration.md +129 -0
- package/docs/payment-system.md +856 -0
- package/docs/paypal-sandbox-qa.md +105 -0
- package/docs/response-headers.md +9 -0
- package/docs/routes.md +211 -0
- package/docs/sanitization.md +71 -0
- package/docs/schemas.md +163 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/stripe-webhook-forwarding.md +20 -0
- package/docs/test-boot-layer.md +67 -0
- package/docs/test-framework.md +583 -0
- package/docs/usage-rate-limiting.md +121 -0
- package/docs/verts.md +29 -0
- package/package.json +143 -0
- package/templates/config/omega.json5 +322 -0
- package/templates/database.rules.json +82 -0
- package/templates/firebase.json +67 -0
- package/templates/firestore.framework.rules +191 -0
- package/templates/firestore.indexes.json +4 -0
- package/templates/firestore.rules +59 -0
- package/templates/index.js +12 -0
- package/templates/public/404.html +26 -0
- package/templates/public/index.html +24 -0
- package/templates/remoteconfig.template.json +1 -0
- package/templates/storage-lifecycle-config-1-day.json +9 -0
- package/templates/storage-lifecycle-config-30-days.json +9 -0
- package/templates/storage.rules +11 -0
- package/test/_init/accounts-validation.js +58 -0
- package/test/ai/tools-live.test.js +171 -0
- package/test/analytics/conversion-delivery.test.js +1013 -0
- package/test/analytics/match-normalization.test.js +238 -0
- package/test/analytics/signup-conversion.test.js +135 -0
- package/test/boot/cli-dispatch.test.js +142 -0
- package/test/boot/defaults-scaffold.test.js +248 -0
- package/test/boot/deploy-staging.test.js +446 -0
- package/test/boot/disposable-domains.test.js +154 -0
- package/test/boot/emulator-boots.test.js +39 -0
- package/test/boot/emulator-port-preflight.test.js +126 -0
- package/test/boot/emulator-ready-timeout.test.js +46 -0
- package/test/boot/project-id-refresh.test.js +73 -0
- package/test/boot/suite-portability.test.js +278 -0
- package/test/boot/update-command.test.js +63 -0
- package/test/cli/cross-platform.test.js +168 -0
- package/test/cli/custom-project-type.test.js +426 -0
- package/test/cli/deploy-license-env.test.js +100 -0
- package/test/cli/emulator-adoption.test.js +254 -0
- package/test/cli/emulator-orphans.test.js +658 -0
- package/test/cli/emulator-ownership.test.js +177 -0
- package/test/cli/emulator-port-retry.test.js +395 -0
- package/test/cli/emulator-shutdown.test.js +788 -0
- package/test/cli/emulator-stale-reap.test.js +574 -0
- package/test/cli/ensure-target.test.js +325 -0
- package/test/cli/flags.test.js +64 -0
- package/test/cli/https-trust.test.js +38 -0
- package/test/cli/install-update-dispatch.test.js +47 -0
- package/test/cli/lane-environments.test.js +159 -0
- package/test/cli/marketing-campaigns-seeded.test.js +194 -0
- package/test/cli/migrate-bare-requires.test.js +128 -0
- package/test/cli/migrate-markers.test.js +530 -0
- package/test/cli/required-indexes.test.js +64 -0
- package/test/cli/rules-compile.test.js +622 -0
- package/test/cli/rules-migration-deferral.test.js +259 -0
- package/test/cli/rules-version.test.js +104 -0
- package/test/cli/setup-load-files.test.js +66 -0
- package/test/cli/setup-offline-mode.test.js +276 -0
- package/test/cli/setup-retired.test.js +64 -0
- package/test/cli/setup-shared-project-indexes.test.js +262 -0
- package/test/cli/setup-tests-requires.test.js +45 -0
- package/test/cli/stage-env-compose.test.js +316 -0
- package/test/cli/stripe-live-lane.test.js +303 -0
- package/test/cli/target.test.js +182 -0
- package/test/cli/templates.test.js +66 -0
- package/test/cli/test-runner-env.test.js +115 -0
- package/test/cli/test-stack-shutdown.test.js +275 -0
- package/test/cli/test-target-no-match.test.js +128 -0
- package/test/cli/verb-logs.test.js +118 -0
- package/test/cli/version-dispatch.test.js +39 -0
- package/test/content/blog-generate.test.js +164 -0
- package/test/email/campaign-config-fault.test.js +376 -0
- package/test/email/campaign-cron-pipeline.test.js +536 -0
- package/test/email/campaign-send.test.js +47 -0
- package/test/email/consent-lifecycle.test.js +258 -0
- package/test/email/content-html-policy.test.js +270 -0
- package/test/email/feedback-and-plain-send.test.js +54 -0
- package/test/email/fixtures/clean.json +30 -0
- package/test/email/fixtures/editorial.json +30 -0
- package/test/email/fixtures/field-report.json +53 -0
- package/test/email/identity.test.js +321 -0
- package/test/email/marketing/consent-gate.test.js +265 -0
- package/test/email/marketing/custom-fields-catalog.test.js +259 -0
- package/test/email/marketing/prune-per-provider.test.js +762 -0
- package/test/email/marketing/remove-log-privacy.test.js +225 -0
- package/test/email/marketing-lifecycle.test.js +139 -0
- package/test/email/newsletter-generate.test.js +853 -0
- package/test/email/newsletter-svg-tokens.test.js +51 -0
- package/test/email/newsletter-templates.test.js +492 -0
- package/test/email/order-one-time-cta.test.js +97 -0
- package/test/email/render-content.test.js +248 -0
- package/test/email/safe-url.test.js +0 -0
- package/test/email/sanitize-images.test.js +66 -0
- package/test/email/send-log-privacy.test.js +107 -0
- package/test/email/templates.test.js +288 -0
- package/test/email/testing-capture.test.js +260 -0
- package/test/email/transactional-send.test.js +36 -0
- package/test/email/transactional.test.js +562 -0
- package/test/email/unsubscribe-groups.test.js +155 -0
- package/test/email/unsubscribe-key.test.js +91 -0
- package/test/email/validation-cases.test.js +152 -0
- package/test/email/validation.test.js +746 -0
- package/test/events/auth-delete-conversion.test.js +138 -0
- package/test/events/auth-delete-race.test.js +211 -0
- package/test/events/auth-on-create-log.test.js +164 -0
- package/test/events/auth-signup-conversion.test.js +141 -0
- package/test/events/auth-signup-limit.test.js +142 -0
- package/test/events/auth-trigger-log-privacy.test.js +256 -0
- package/test/events/cron-job-doc-shape.test.js +101 -0
- package/test/events/cron-reset-usage.test.js +116 -0
- package/test/events/notification-conversion.test.js +182 -0
- package/test/events/payments/_webhook-harness.js +307 -0
- package/test/events/payments/abandoned-cart-activity.test.js +135 -0
- package/test/events/payments/analytics-payment-events.test.js +945 -0
- package/test/events/payments/dispute-email-status.test.js +55 -0
- package/test/events/payments/journey-payments-abandoned.test.js +126 -0
- package/test/events/payments/journey-payments-cancel-endpoint.test.js +102 -0
- package/test/events/payments/journey-payments-cancel-no-order.test.js +76 -0
- package/test/events/payments/journey-payments-cancel.test.js +184 -0
- package/test/events/payments/journey-payments-decline.test.js +225 -0
- package/test/events/payments/journey-payments-discount.test.js +91 -0
- package/test/events/payments/journey-payments-dispute.test.js +190 -0
- package/test/events/payments/journey-payments-failure.test.js +148 -0
- package/test/events/payments/journey-payments-legacy-product.test.js +151 -0
- package/test/events/payments/journey-payments-one-time-decline.test.js +127 -0
- package/test/events/payments/journey-payments-one-time-failure.test.js +113 -0
- package/test/events/payments/journey-payments-one-time-refund.test.js +186 -0
- package/test/events/payments/journey-payments-one-time.test.js +175 -0
- package/test/events/payments/journey-payments-plan-change.test.js +146 -0
- package/test/events/payments/journey-payments-plan-switch-trial.test.js +161 -0
- package/test/events/payments/journey-payments-plan-switch.test.js +182 -0
- package/test/events/payments/journey-payments-refund-no-order.test.js +76 -0
- package/test/events/payments/journey-payments-refund-webhook.test.js +225 -0
- package/test/events/payments/journey-payments-suspend.test.js +183 -0
- package/test/events/payments/journey-payments-trial-cancel.test.js +132 -0
- package/test/events/payments/journey-payments-trial.test.js +173 -0
- package/test/events/payments/journey-payments-uid-resolution.test.js +134 -0
- package/test/events/payments/journey-payments-uncancel.test.js +157 -0
- package/test/events/payments/journey-payments-upgrade.test.js +137 -0
- package/test/events/payments/journey-payments-winback-decline.test.js +171 -0
- package/test/events/payments/journey-payments-winback.test.js +195 -0
- package/test/events/payments/paypal-expiry-cron.test.js +166 -0
- package/test/events/payments/purchase-failed-handler.test.js +80 -0
- package/test/events/payments/purchase-refunded-handler.test.js +108 -0
- package/test/events/payments/test-processor-doc-shape.test.js +162 -0
- package/test/events/payments/transition-order-emails.test.js +185 -0
- package/test/events/payments/transition-promo-lines.test.js +192 -0
- package/test/events/payments/transitions-detect.test.js +767 -0
- package/test/events/payments/trial-lapse-sweep-staleness.test.js +173 -0
- package/test/events/payments/trial-lapse-sweep.test.js +359 -0
- package/test/events/payments/webhook-atomic-writes.test.js +69 -0
- package/test/events/payments/webhook-chargebee-unreachable.test.js +106 -0
- package/test/events/payments/webhook-discount-clear.test.js +189 -0
- package/test/events/payments/webhook-failure-intent.test.js +93 -0
- package/test/events/payments/webhook-hosted-page-uid-trust.test.js +212 -0
- package/test/events/payments/webhook-ordering.test.js +326 -0
- package/test/events/payments/webhook-provider-lookup-trust.test.js +206 -0
- package/test/events/payments/webhook-refund-amount-trust.test.js +223 -0
- package/test/events/payments/webhook-refund-envelope-shape.test.js +148 -0
- package/test/events/payments/webhook-refund-linkage-trust.test.js +404 -0
- package/test/events/payments/webhook-refund-transaction-fallback.test.js +184 -0
- package/test/events/payments/webhook-refund-without-order.test.js +170 -0
- package/test/events/payments/webhook-refusal-reporting.test.js +194 -0
- package/test/events/payments/webhook-refused-intent.test.js +142 -0
- package/test/events/payments/webhook-retry-sweep.test.js +252 -0
- package/test/events/payments/webhook-transition-claim.test.js +389 -0
- package/test/events/payments/webhook-uid-trust.test.js +203 -0
- package/test/events/payments/webhook-user-without-auth.test.js +161 -0
- package/test/fixtures/chargebee/invoice-one-time.json +27 -0
- package/test/fixtures/chargebee/subscription-active.json +44 -0
- package/test/fixtures/chargebee/subscription-cancelled.json +42 -0
- package/test/fixtures/chargebee/subscription-in-trial.json +41 -0
- package/test/fixtures/chargebee/subscription-legacy-plan.json +41 -0
- package/test/fixtures/chargebee/subscription-non-renewing.json +41 -0
- package/test/fixtures/chargebee/subscription-paused.json +42 -0
- package/test/fixtures/chargebee/webhook-payment-failed.json +51 -0
- package/test/fixtures/chargebee/webhook-subscription-created.json +47 -0
- package/test/fixtures/coinbase/charge-confirmed.json +50 -0
- package/test/fixtures/coinbase/charge-failed.json +34 -0
- package/test/fixtures/coinbase/charge-pending.json +34 -0
- package/test/fixtures/migrate/ported-route.js.txt +11 -0
- package/test/fixtures/paypal/capture-completed.json +51 -0
- package/test/fixtures/paypal/capture-refunded.json +43 -0
- package/test/fixtures/paypal/order-approved.json +62 -0
- package/test/fixtures/paypal/order-completed.json +110 -0
- package/test/fixtures/paypal/sale-refunded.json +38 -0
- package/test/fixtures/paypal/subscription-active.json +76 -0
- package/test/fixtures/paypal/subscription-cancelled.json +50 -0
- package/test/fixtures/paypal/subscription-suspended.json +65 -0
- package/test/fixtures/stripe/checkout-session-completed.json +130 -0
- package/test/fixtures/stripe/invoice-payment-failed.json +148 -0
- package/test/fixtures/stripe/invoice-subscription-payment-failed.json +28 -0
- package/test/fixtures/stripe/invoice-subscription-payment-succeeded.json +28 -0
- package/test/fixtures/stripe/subscription-active.json +161 -0
- package/test/fixtures/stripe/subscription-canceled.json +161 -0
- package/test/fixtures/stripe/subscription-trialing.json +161 -0
- package/test/helpers/_shared-config.js +15 -0
- package/test/helpers/ai-request-payload.test.js +619 -0
- package/test/helpers/ai-schema-resolve.test.js +125 -0
- package/test/helpers/ai-test-provider.test.js +227 -0
- package/test/helpers/ai-token-accounting.test.js +150 -0
- package/test/helpers/ai-tools-format.test.js +384 -0
- package/test/helpers/analytics-no-id-notice.test.js +146 -0
- package/test/helpers/analytics-user-data.test.js +194 -0
- package/test/helpers/api-manager.test.js +311 -0
- package/test/helpers/backend-router.test.js +136 -0
- package/test/helpers/content/blog-auto-publisher.test.js +482 -0
- package/test/helpers/content/feed-parser.test.js +529 -0
- package/test/helpers/content/ghostii-blocks.test.js +135 -0
- package/test/helpers/content/ghostii-feed-integration.test.js +405 -0
- package/test/helpers/content/ghostii-write-article.test.js +244 -0
- package/test/helpers/dev-only-routes.test.js +225 -0
- package/test/helpers/env-reader.test.js +362 -0
- package/test/helpers/environment.test.js +263 -0
- package/test/helpers/event-middleware.test.js +407 -0
- package/test/helpers/infer-contact.test.js +157 -0
- package/test/helpers/lane-url.test.js +90 -0
- package/test/helpers/merge-line-files.test.js +272 -0
- package/test/helpers/metadata.test.js +122 -0
- package/test/helpers/middleware-request-log.test.js +134 -0
- package/test/helpers/middleware-user-log.test.js +124 -0
- package/test/helpers/payment/chargebee/parse-webhook.test.js +490 -0
- package/test/helpers/payment/chargebee/refund-details.test.js +198 -0
- package/test/helpers/payment/chargebee/to-unified-one-time.test.js +148 -0
- package/test/helpers/payment/chargebee/to-unified-subscription.test.js +649 -0
- package/test/helpers/payment/coinbase/create-intent.test.js +247 -0
- package/test/helpers/payment/coinbase/parse-webhook.test.js +195 -0
- package/test/helpers/payment/coinbase/refund-unsupported.test.js +141 -0
- package/test/helpers/payment/coinbase/to-unified-one-time.test.js +201 -0
- package/test/helpers/payment/discount-codes.test.js +141 -0
- package/test/helpers/payment/extract-resource.test.js +113 -0
- package/test/helpers/payment/fetch-failure.test.js +215 -0
- package/test/helpers/payment/license-gate.test.js +108 -0
- package/test/helpers/payment/order-id.test.js +99 -0
- package/test/helpers/payment/paypal/create-intent.test.js +382 -0
- package/test/helpers/payment/paypal/fetch-capture.test.js +123 -0
- package/test/helpers/payment/paypal/fetch-sale.test.js +183 -0
- package/test/helpers/payment/paypal/parse-webhook.test.js +678 -0
- package/test/helpers/payment/paypal/refund-details.test.js +202 -0
- package/test/helpers/payment/paypal/resolve-plan-id.test.js +108 -0
- package/test/helpers/payment/paypal/switch-plan.test.js +92 -0
- package/test/helpers/payment/paypal/to-unified-one-time.test.js +383 -0
- package/test/helpers/payment/paypal/to-unified-subscription.test.js +884 -0
- package/test/helpers/payment/stripe/fetch-charge.test.js +147 -0
- package/test/helpers/payment/stripe/parse-webhook.test.js +448 -0
- package/test/helpers/payment/stripe/refund-details.test.js +147 -0
- package/test/helpers/payment/stripe/to-unified-one-time.test.js +307 -0
- package/test/helpers/payment/stripe/to-unified-subscription.test.js +709 -0
- package/test/helpers/persona-domain.test.js +87 -0
- package/test/helpers/recaptcha.test.js +166 -0
- package/test/helpers/resolved-config.test.js +106 -0
- package/test/helpers/response-log-redaction.test.js +262 -0
- package/test/helpers/roles.test.js +155 -0
- package/test/helpers/route-context-debug-gate.test.js +118 -0
- package/test/helpers/route-context-logging.test.js +214 -0
- package/test/helpers/route-context.test.js +129 -0
- package/test/helpers/safe-compare.test.js +114 -0
- package/test/helpers/sanitize.test.js +223 -0
- package/test/helpers/schema-engine.test.js +537 -0
- package/test/helpers/schema-zod.test.js +554 -0
- package/test/helpers/seed-accounts-load.test.js +163 -0
- package/test/helpers/seed-google-personas.test.js +114 -0
- package/test/helpers/seeded-personas.test.js +823 -0
- package/test/helpers/settings.test.js +292 -0
- package/test/helpers/setup-engines-pin.test.js +68 -0
- package/test/helpers/setup-manifest-sync.test.js +116 -0
- package/test/helpers/slugify.test.js +395 -0
- package/test/helpers/storage.test.js +195 -0
- package/test/helpers/test-banner-latch.test.js +139 -0
- package/test/helpers/usage-consume.test.js +460 -0
- package/test/helpers/usage-log-privacy.test.js +121 -0
- package/test/helpers/user-doc-heal.test.js +540 -0
- package/test/helpers/user.test.js +771 -0
- package/test/helpers/webhook-forward.test.js +419 -0
- package/test/helpers/wipe-auth-project.test.js +143 -0
- package/test/mcp/discovery.test.js +52 -0
- package/test/mcp/oauth.test.js +160 -0
- package/test/mcp/protocol.test.js +278 -0
- package/test/mcp/roles.test.js +203 -0
- package/test/mcp/utils.test.js +245 -0
- package/test/notification/identity.test.js +155 -0
- package/test/routes/admin/create-post.test.js +362 -0
- package/test/routes/admin/database.test.js +133 -0
- package/test/routes/admin/deduplicate-image-alts.test.js +191 -0
- package/test/routes/admin/email-content-html.test.js +58 -0
- package/test/routes/admin/email-request-log.test.js +86 -0
- package/test/routes/admin/email.test.js +116 -0
- package/test/routes/admin/firestore-query.test.js +206 -0
- package/test/routes/admin/firestore.test.js +129 -0
- package/test/routes/admin/infer-contact.test.js +220 -0
- package/test/routes/admin/notification.test.js +199 -0
- package/test/routes/admin/post-convert-image.test.js +159 -0
- package/test/routes/admin/post-deploy-flag.test.js +81 -0
- package/test/routes/admin/post-download-error.test.js +90 -0
- package/test/routes/admin/post-resize-image.test.js +185 -0
- package/test/routes/admin/post.test.js +369 -0
- package/test/routes/admin/repo-content.test.js +223 -0
- package/test/routes/admin/stats.test.js +114 -0
- package/test/routes/admin/users-disable.test.js +63 -0
- package/test/routes/admin/users-list.test.js +71 -0
- package/test/routes/content/post.test.js +60 -0
- package/test/routes/general/uuid.test.js +133 -0
- package/test/routes/health.test.js +110 -0
- package/test/routes/marketing/campaign.test.js +184 -0
- package/test/routes/marketing/contact.test.js +416 -0
- package/test/routes/marketing/email-preferences.test.js +293 -0
- package/test/routes/marketing/push-send.test.js +34 -0
- package/test/routes/marketing/webhook-forward.test.js +63 -0
- package/test/routes/marketing/webhook.test.js +641 -0
- package/test/routes/payments/_route-harness.js +145 -0
- package/test/routes/payments/cancel-provider-errors.test.js +227 -0
- package/test/routes/payments/cancel-skip-guards.test.js +126 -0
- package/test/routes/payments/cancel-trialing.test.js +428 -0
- package/test/routes/payments/cancel.test.js +165 -0
- package/test/routes/payments/dedup-race.test.js +186 -0
- package/test/routes/payments/discount.test.js +82 -0
- package/test/routes/payments/dispute-alert.test.js +324 -0
- package/test/routes/payments/intent-discount-amounts.test.js +550 -0
- package/test/routes/payments/intent-discount-coupons.test.js +293 -0
- package/test/routes/payments/intent-one-time-metadata.test.js +122 -0
- package/test/routes/payments/intent-purchaser-guard.test.js +139 -0
- package/test/routes/payments/intent-zero-total.test.js +260 -0
- package/test/routes/payments/intent.test.js +413 -0
- package/test/routes/payments/plan.test.js +367 -0
- package/test/routes/payments/portal-return-url.test.js +107 -0
- package/test/routes/payments/portal.test.js +94 -0
- package/test/routes/payments/refund-one-time.test.js +254 -0
- package/test/routes/payments/refund-paypal-proration.test.js +190 -0
- package/test/routes/payments/refund.test.js +181 -0
- package/test/routes/payments/trial-eligibility.test.js +73 -0
- package/test/routes/payments/uncancel.test.js +270 -0
- package/test/routes/payments/webhook-stripe-invoice.test.js +129 -0
- package/test/routes/payments/webhook-stripe-refund-one-time.test.js +130 -0
- package/test/routes/payments/webhook-test-provider.test.js +156 -0
- package/test/routes/payments/webhook.test.js +115 -0
- package/test/routes/payments/winback-stripe-coupon.test.js +126 -0
- package/test/routes/payments/winback.test.js +597 -0
- package/test/routes/test/authenticate.test.js +79 -0
- package/test/routes/test/redirect.test.js +66 -0
- package/test/routes/test/reset-account.test.js +126 -0
- package/test/routes/test/roster.test.js +101 -0
- package/test/routes/test/schema.test.js +556 -0
- package/test/routes/test/usage.test.js +374 -0
- package/test/routes/user/api-keys.test.js +158 -0
- package/test/routes/user/connections-grant.test.js +759 -0
- package/test/routes/user/connections-identity.test.js +260 -0
- package/test/routes/user/connections-log-privacy.test.js +206 -0
- package/test/routes/user/connections-refresh-lease.test.js +696 -0
- package/test/routes/user/connections-return.test.js +261 -0
- package/test/routes/user/connections-uid.test.js +516 -0
- package/test/routes/user/delete.test.js +138 -0
- package/test/routes/user/feedback.test.js +106 -0
- package/test/routes/user/orders.test.js +233 -0
- package/test/routes/user/sessions.test.js +231 -0
- package/test/routes/user/settings-validate.test.js +84 -0
- package/test/routes/user/signup-emails.test.js +113 -0
- package/test/routes/user/signup-location.test.js +84 -0
- package/test/routes/user/signup-log-privacy.test.js +112 -0
- package/test/routes/user/signup.test.js +579 -0
- package/test/routes/user/subscription.test.js +101 -0
- package/test/routes/user/token.test.js +111 -0
- package/test/routes/user/user.test.js +158 -0
- package/test/routes/verts/cache.test.js +78 -0
- package/test/routes/verts/click-destination.test.js +76 -0
- package/test/routes/verts/crud.test.js +194 -0
- package/test/routes/verts/redirect.test.js +147 -0
- package/test/routes/verts/selection.test.js +237 -0
- package/test/routes/verts/serve.test.js +185 -0
- package/test/routes/verts/unit-document.test.js +117 -0
- package/test/rules/_environment.js +66 -0
- package/test/rules/brand-merge.test.js +195 -0
- package/test/rules/field-helpers.test.js +309 -0
- package/test/rules/notifications.test.js +577 -0
- package/test/rules/payments-carts.test.js +406 -0
- package/test/rules/sessions.test.js +170 -0
- package/test/rules/user.test.js +517 -0
- package/test/rules/verts.test.js +101 -0
- package/test/security/fetch-log-secrets.test.js +124 -0
- package/test/security/primitives.test.js +105 -0
- package/test/security/repo-pinning.test.js +77 -0
- package/test/stripe-live/subscription-lifecycle.test.js +141 -0
|
@@ -0,0 +1,1829 @@
|
|
|
1
|
+
const BaseCommand = require('./base-command');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { spawnShell } = require('../utils/spawn-shell');
|
|
5
|
+
const chalk = require('chalk').default;
|
|
6
|
+
const jetpack = require('fs-jetpack');
|
|
7
|
+
const JSON5 = require('json5');
|
|
8
|
+
const powertools = require('node-powertools');
|
|
9
|
+
const WatchCommand = require('./watch');
|
|
10
|
+
const { loadEmulatorPorts } = require('./setup-tests/emulator-config');
|
|
11
|
+
const { resolvePorts, writePortsFile, clearPortsFile, portsToEnv, isPortFree } = require('../../vendor/config/index.js');
|
|
12
|
+
const { EXTENDED_MODE_WARNING } = require('../../test/utils/extended-mode-warning');
|
|
13
|
+
const { writeTestMode, captureSyncedEnv } = require('../../test/utils/test-mode-file');
|
|
14
|
+
const { seed } = require('../../test/seed.js');
|
|
15
|
+
const { createChildLog } = require('../utils/attach-log-file');
|
|
16
|
+
const { refuseWhenCustom } = require('../utils/project-type');
|
|
17
|
+
const emulatorOrphans = require('./emulator-orphans');
|
|
18
|
+
const { STOP_SIGNALS } = require('../../vendor/devkit/stop-signals.js');
|
|
19
|
+
|
|
20
|
+
// Used by both `npx omega emulator` and `npx omega test` auto-start path.
|
|
21
|
+
// Note: `emulators:start` enables the UI by default (controlled by firebase.json's
|
|
22
|
+
// `emulators.ui.enabled`), so no `--ui` flag here — that flag only exists on `:exec`.
|
|
23
|
+
const EMULATOR_FLAGS = '--only functions,firestore,auth,database,hosting,pubsub';
|
|
24
|
+
|
|
25
|
+
// The pid record this run writes when its stack is up — the sweep's primary
|
|
26
|
+
// ownership proof ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)).
|
|
27
|
+
const PID_RECORD_FILE = 'emulator-pids.json';
|
|
28
|
+
|
|
29
|
+
// How long a recorded pid stays evidence. The record is rewritten at boot and
|
|
30
|
+
// again at shutdown, so a live run's is always seconds old; anything older
|
|
31
|
+
// belongs to a run that died without teardown. Pids get RECYCLED, and a
|
|
32
|
+
// recorded pid is a kill order the sweep acts on without further proof — past
|
|
33
|
+
// this bound the number is no longer known to name the process it named.
|
|
34
|
+
const PID_RECORD_MAX_AGE_MS = 12 * 60 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
// A command line that belongs to emulator machinery. Deliberately broad: it
|
|
37
|
+
// only ever narrows a candidate that ALREADY named this project id.
|
|
38
|
+
const EMULATOR_COMMAND = /emulator|firebase/i;
|
|
39
|
+
|
|
40
|
+
// How long a recorded emulator process gets to leave on SIGTERM before the
|
|
41
|
+
// stop path escalates, and how long its ports get to come back free after the
|
|
42
|
+
// last one is gone (a java emulator releases its listener as it unwinds).
|
|
43
|
+
const STOP_GRACE_MS = 2000;
|
|
44
|
+
const PORT_RELEASE_TIMEOUT_MS = 5000;
|
|
45
|
+
const POLL_INTERVAL_MS = 100;
|
|
46
|
+
|
|
47
|
+
// How long the port→pid lookup gets to answer before a sweep gives up on it.
|
|
48
|
+
// lsof stats every mounted filesystem first, so a machine with a network mount
|
|
49
|
+
// pays for the walk on every call — measured at ~90ms a call here, and a stall
|
|
50
|
+
// on the smbfs volume runs to MINUTES
|
|
51
|
+
// ([#332](https://github.com/Omega-JS-Stack/omega/issues/332)). The call is
|
|
52
|
+
// synchronous, so an unbounded one freezes the whole run: the boot path
|
|
53
|
+
// absorbed that in its ready window, and the stop path simply sat there until
|
|
54
|
+
// a ship gate's lane budget was gone
|
|
55
|
+
// ([#459](https://github.com/Omega-JS-Stack/omega/issues/459)). This is ~50x a
|
|
56
|
+
// healthy answer, and the same window the port verdict already waits.
|
|
57
|
+
const PORT_LOOKUP_TIMEOUT_MS = 5000;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Does this command line name the given project as an ARGUMENT?
|
|
61
|
+
*
|
|
62
|
+
* Substring matching would be wrong twice over: `demo-x` appears inside
|
|
63
|
+
* `demo-x-staging` (a different project), and a repo path containing the brand
|
|
64
|
+
* name is not a project id at all. Only `--project <id>` / `--project_id <id>`
|
|
65
|
+
* (space or `=`) counts — the form the firestore emulator actually publishes.
|
|
66
|
+
* @param {string} command - Full command line from ps.
|
|
67
|
+
* @param {string} projectId - The project id to look for.
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
function commandNamesProject(command, projectId) {
|
|
71
|
+
const escaped = projectId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
72
|
+
|
|
73
|
+
return new RegExp(`--project(?:_id)?[= ]${escaped}(?:\\s|$)`).test(command);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The project a command line names as an argument, or null when it names none.
|
|
78
|
+
*
|
|
79
|
+
* Same argument form as commandNamesProject(), read the other way round: not
|
|
80
|
+
* "is it this project" but "which project does it say it is". A jar that names
|
|
81
|
+
* one is self-identifying evidence that outranks a recorded pid number.
|
|
82
|
+
* @param {string} command - Full command line from ps.
|
|
83
|
+
* @returns {string|null}
|
|
84
|
+
*/
|
|
85
|
+
function commandProjectId(command) {
|
|
86
|
+
const match = /--project(?:_id)?[= ](\S+)/.exec(String(command || ''));
|
|
87
|
+
|
|
88
|
+
return match ? match[1] : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// How long a boot may take before the run declares it dead and shuts the
|
|
92
|
+
// partial stack down. The default absorbs a slow port sweep — lsof stalling on
|
|
93
|
+
// a network mount takes a real boot past the old 60s cap, which failed every
|
|
94
|
+
// self-booting lane on such a machine
|
|
95
|
+
// ([#332](https://github.com/Omega-JS-Stack/omega/issues/332)).
|
|
96
|
+
const DEFAULT_READY_TIMEOUT_MS = 180000;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve the emulator ready deadline from the environment.
|
|
100
|
+
* @param {string} [raw] - The OMEGA_EMULATOR_READY_TIMEOUT value, in ms.
|
|
101
|
+
* @returns {number} The deadline in milliseconds.
|
|
102
|
+
*/
|
|
103
|
+
function resolveReadyTimeout(raw) {
|
|
104
|
+
if (raw === undefined || raw === '') {
|
|
105
|
+
return DEFAULT_READY_TIMEOUT_MS;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const parsed = Number(raw);
|
|
109
|
+
|
|
110
|
+
// A junk override fails loudly instead of silently racing an unknown
|
|
111
|
+
// deadline — the same rule the #211 lane multiplier follows.
|
|
112
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
113
|
+
throw new Error(`OMEGA_EMULATOR_READY_TIMEOUT must be a positive number of milliseconds, got "${raw}"`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The subset of `ports` something is actually listening on.
|
|
121
|
+
*
|
|
122
|
+
* The sweeps below need a port to name a pid, and `lsof` is the only tool that
|
|
123
|
+
* maps one. It stats every mounted filesystem before it answers, so a machine
|
|
124
|
+
* with a network mount (an smbfs Time Machine volume) pays seconds per call,
|
|
125
|
+
* and a boot ran one call per port whether or not anything was there: ~96s of
|
|
126
|
+
* pure waste on a run with every port free, which pushed boot past the ready
|
|
127
|
+
* deadline ([#332](https://github.com/Omega-JS-Stack/omega/issues/332)).
|
|
128
|
+
*
|
|
129
|
+
* A port nothing holds cannot have a holder to name, so its lookup could only
|
|
130
|
+
* ever come back empty. Probe first with the same bind primitive the allocator
|
|
131
|
+
* uses (in-process, no shell, no filesystem walk) and look up only the ports
|
|
132
|
+
* that answer. Same ports considered, same decisions, minus the empty calls.
|
|
133
|
+
* @param {number[]} ports - The ports a sweep is about to consider.
|
|
134
|
+
* @param {(port: number) => Promise<boolean>} [isFree] - The probe (injectable).
|
|
135
|
+
* @returns {Promise<number[]>} The held subset, deduped.
|
|
136
|
+
*/
|
|
137
|
+
async function heldPorts(ports, isFree = isPortFree) {
|
|
138
|
+
const unique = [...new Set(ports || [])].filter((port) => Number.isInteger(port));
|
|
139
|
+
const free = await Promise.all(unique.map((port) => isFree(port)));
|
|
140
|
+
|
|
141
|
+
return unique.filter((port, index) => !free[index]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The pids LISTENING on a port.
|
|
146
|
+
*
|
|
147
|
+
* The one step with no Node primitive: only lsof maps a port to a process. It
|
|
148
|
+
* is reached solely for a port heldPorts() already proved is held, so a normal
|
|
149
|
+
* boot never shells here at all.
|
|
150
|
+
*
|
|
151
|
+
* And it is BOUNDED, because the tool can stall indefinitely on a network
|
|
152
|
+
* mount (PORT_LOOKUP_TIMEOUT_MS). A lookup that overruns names nobody, which
|
|
153
|
+
* is what every caller already does with a port whose holder it cannot
|
|
154
|
+
* identify: report it, leave it running. The bound can only ever spare a
|
|
155
|
+
* process, never take one.
|
|
156
|
+
* @param {number} port - A port something is known to hold.
|
|
157
|
+
* @returns {string[]} The listening pids, or [] when the read fails or overruns.
|
|
158
|
+
*/
|
|
159
|
+
function listListeningPids(port) {
|
|
160
|
+
const { execFileSync } = require('child_process');
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
// No shell: the bound has to land on lsof ITSELF, and an `sh -c` wrapper
|
|
164
|
+
// is one more process for the kill to hit instead. stderr is dropped by
|
|
165
|
+
// the stdio map, which is all the old `2>/dev/null` was doing. SIGKILL
|
|
166
|
+
// because a process wedged in a filesystem call is exactly the one that
|
|
167
|
+
// would ignore a polite signal and hold the run past its own timeout.
|
|
168
|
+
return execFileSync('lsof', ['-ti', `TCP:${port}`, '-sTCP:LISTEN'], {
|
|
169
|
+
encoding: 'utf8',
|
|
170
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
171
|
+
timeout: PORT_LOOKUP_TIMEOUT_MS,
|
|
172
|
+
killSignal: 'SIGKILL',
|
|
173
|
+
}).trim().split('\n').filter(Boolean);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Every port the firebase config tells the emulator child to bind.
|
|
181
|
+
*
|
|
182
|
+
* Read from the config, never hard-coded: a brand may declare emulators this
|
|
183
|
+
* CLI's allocator knows nothing about (eventarc, tasks), and those are exactly
|
|
184
|
+
* the ones a boot cannot relocate.
|
|
185
|
+
* @param {object} firebaseConfig - The parsed firebase config the child reads.
|
|
186
|
+
* @returns {object} name to port, for every emulator that names a port.
|
|
187
|
+
*/
|
|
188
|
+
function declaredEmulatorPorts(firebaseConfig) {
|
|
189
|
+
const declared = {};
|
|
190
|
+
|
|
191
|
+
for (const [name, entry] of Object.entries(firebaseConfig?.emulators || {})) {
|
|
192
|
+
if (Number.isInteger(entry?.port)) {
|
|
193
|
+
declared[name] = entry.port;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return declared;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The boot's port plan: what the emulator child is about to bind.
|
|
202
|
+
*
|
|
203
|
+
* The resolved map wins over the declared value, because a bumped port is what
|
|
204
|
+
* the child actually receives (via firebase.resolved.json). `https` is left
|
|
205
|
+
* out: that is THIS process's TLS proxy, not the child's listener.
|
|
206
|
+
* @param {object} declared - name to port, from the firebase config.
|
|
207
|
+
* @param {object} resolved - This run's allocated map (already bumped).
|
|
208
|
+
* @returns {Array<{name: string, port: number}>}
|
|
209
|
+
*/
|
|
210
|
+
function plannedEmulatorPorts(declared, resolved) {
|
|
211
|
+
const plan = [];
|
|
212
|
+
|
|
213
|
+
for (const [name, port] of Object.entries({ ...declared, ...resolved })) {
|
|
214
|
+
if (name === 'https') {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
plan.push({ name: name, port: port });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return plan;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The fail-fast report for a boot whose plan cannot work.
|
|
226
|
+
* @param {Array<{name: string, port: number}>} blocked - The held planned ports.
|
|
227
|
+
* @returns {string}
|
|
228
|
+
*/
|
|
229
|
+
function formatPortPreflightFailure(blocked) {
|
|
230
|
+
const named = blocked.map(({ name, port }) => `port ${port} (${name})`).join(', ');
|
|
231
|
+
const numbers = blocked.map(({ port }) => port).join(', ');
|
|
232
|
+
|
|
233
|
+
return [
|
|
234
|
+
`Port preflight failed: this emulator must bind ${named}, and something already holds ${blocked.length > 1 ? 'them' : 'it'}.`,
|
|
235
|
+
'Another dev stack is in the way: a tower app on that port, another brand\'s `omega dev`, or an emulator a crashed run left behind.',
|
|
236
|
+
`Stop whatever holds ${numbers} and run this again. Nothing was spawned, so there is no partial stack to clean up.`,
|
|
237
|
+
].join('\n ');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Stop the boot BEFORE firebase is spawned when the plan cannot work.
|
|
242
|
+
*
|
|
243
|
+
* The allocator already relocates around a busy port (N7 bump-if-taken), so a
|
|
244
|
+
* plan that still names a held port names one this run cannot move off. Left
|
|
245
|
+
* to firebase, that boot never prints its ready marker and the run burns the
|
|
246
|
+
* whole ready deadline before anything says why
|
|
247
|
+
* ([#332](https://github.com/Omega-JS-Stack/omega/issues/332)).
|
|
248
|
+
* @param {Array<{name: string, port: number}>} planned - What the child will bind.
|
|
249
|
+
* @param {(port: number) => Promise<boolean>} [isFree] - The probe (injectable).
|
|
250
|
+
* @returns {Promise<void>} Rejects with the report when a planned port is held.
|
|
251
|
+
*/
|
|
252
|
+
async function assertPlannedPortsFree(planned, isFree = isPortFree) {
|
|
253
|
+
const held = await heldPorts(planned.map(({ port }) => port), isFree);
|
|
254
|
+
|
|
255
|
+
if (held.length === 0) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
throw new Error(formatPortPreflightFailure(planned.filter(({ port }) => held.includes(port))));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The allocated port firebase-tools just failed to bind, or null.
|
|
264
|
+
*
|
|
265
|
+
* The probe and the bind are two moments, and on a machine running a second
|
|
266
|
+
* brand's stack a foreign listener can take a port between them, and the boot
|
|
267
|
+
* then dies on a port the allocator read as free seconds earlier
|
|
268
|
+
* ([#778](https://github.com/Omega-JS-Stack/omega/issues/778)). The child says
|
|
269
|
+
* so in one line, and the port is read out of it by matching against THIS
|
|
270
|
+
* run's allocation rather than by parsing an address shape: node writes the
|
|
271
|
+
* same failure as `127.0.0.1:9099`, `:::9099` and `port 9099` depending on
|
|
272
|
+
* which layer reports it, and only a number this run actually asked for can be
|
|
273
|
+
* the one it could not bind.
|
|
274
|
+
* @param {string} text - A chunk of the child's output.
|
|
275
|
+
* @param {number[]} ports - This attempt's allocated ports.
|
|
276
|
+
* @returns {number|null}
|
|
277
|
+
*/
|
|
278
|
+
function addressInUsePort(text, ports) {
|
|
279
|
+
const allocated = new Set(ports);
|
|
280
|
+
|
|
281
|
+
for (const line of String(text || '').split('\n')) {
|
|
282
|
+
if (!/EADDRINUSE/i.test(line)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const [number] of line.matchAll(/\d+/g)) {
|
|
287
|
+
if (allocated.has(Number(number))) {
|
|
288
|
+
return Number(number);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The report for a boot whose retry hit the same wall (#778).
|
|
298
|
+
*
|
|
299
|
+
* The holder is NAMED, never signalled: it is another session's live stack,
|
|
300
|
+
* and this run has no proof of ownership over it, which is the same bar every
|
|
301
|
+
* sweep in this file clears before it sends a signal.
|
|
302
|
+
* @param {{port: number, name: string|null}} failed - The port the retry could not bind.
|
|
303
|
+
* @param {Array<{pid: number, command: string}>} holders - Who holds it now.
|
|
304
|
+
* @returns {string}
|
|
305
|
+
*/
|
|
306
|
+
function formatAddressInUseFailure(failed, holders) {
|
|
307
|
+
const named = `port ${failed.port}${failed.name ? ` (${failed.name})` : ''}`;
|
|
308
|
+
const report = [`Emulator boot failed: ${named} was taken while the stack was coming up, and the bumped retry hit the same wall.`];
|
|
309
|
+
|
|
310
|
+
for (const { pid, command } of holders) {
|
|
311
|
+
report.push(`Port ${failed.port} is held by pid ${pid}${command ? `: ${command}` : ''}.`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (holders.length === 0) {
|
|
315
|
+
report.push(`Nothing answers for port ${failed.port} now: whatever took it came and went inside this boot.`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
report.push(`Stop whatever keeps taking ${failed.port} and run this again. This run's stack was shut down; nothing of it is left running.`);
|
|
319
|
+
|
|
320
|
+
return report.join('\n ');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The allocation's name for a port ('auth' for 9099), or null.
|
|
325
|
+
* @param {object} ports - The resolved name to port map.
|
|
326
|
+
* @param {number} port - The port to name.
|
|
327
|
+
* @returns {string|null}
|
|
328
|
+
*/
|
|
329
|
+
function portName(ports, port) {
|
|
330
|
+
const found = Object.entries(ports || {}).find(([, value]) => value === port);
|
|
331
|
+
|
|
332
|
+
return found ? found[0] : null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Can this process be PROVEN to be an emulator process of THIS project?
|
|
337
|
+
*
|
|
338
|
+
* The sweep used to signal whatever was listening on its ports, which
|
|
339
|
+
* terminated another project's live emulator sharing the hub/storage ports
|
|
340
|
+
* ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)). Occupying a
|
|
341
|
+
* port is not ownership; only these two proofs are:
|
|
342
|
+
*
|
|
343
|
+
* 1. The pid record this run wrote when it spawned the stack. Primary,
|
|
344
|
+
* because the java emulators name no project on their own — and they
|
|
345
|
+
* reparent to PID 1 when orphaned, so the record made while they were
|
|
346
|
+
* still attached is the only surviving link.
|
|
347
|
+
* 2. A command line that is emulator machinery AND names this project id
|
|
348
|
+
* (the firestore emulator's `--project_id`), which covers a leftover from
|
|
349
|
+
* an earlier run of the same project that no live record mentions.
|
|
350
|
+
*
|
|
351
|
+
* Anything else is somebody else's process and is left running.
|
|
352
|
+
* @param {object} candidate - { pid, command } as read from ps.
|
|
353
|
+
* @param {object} [ownership] - { pids: number[], projectId: string|null }.
|
|
354
|
+
* @returns {boolean}
|
|
355
|
+
*/
|
|
356
|
+
function isOwnedEmulatorProcess(candidate, ownership) {
|
|
357
|
+
const pid = Number(candidate?.pid);
|
|
358
|
+
const command = String(candidate?.command || '');
|
|
359
|
+
|
|
360
|
+
// PID 1 is init, and a non-numeric row is a parse failure — never candidates.
|
|
361
|
+
if (!Number.isInteger(pid) || pid <= 1) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if ((ownership?.pids || []).some((recorded) => Number(recorded) === pid)) {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const projectId = ownership?.projectId;
|
|
370
|
+
|
|
371
|
+
if (!projectId) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return EMULATOR_COMMAND.test(command) && commandNamesProject(command, projectId);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Should the STOP path signal this recorded process?
|
|
380
|
+
*
|
|
381
|
+
* The stop path has no port to narrow by — it walks the record itself — so the
|
|
382
|
+
* recorded pid alone would be the whole proof, and pids get RECYCLED. The live
|
|
383
|
+
* command line is the second half: a recorded number that now names something
|
|
384
|
+
* that is not emulator machinery is a stranger the OS handed our pid to, and
|
|
385
|
+
* it is left running ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)).
|
|
386
|
+
*
|
|
387
|
+
* Recycling inside the SAME machinery is the harder half: a recorded number
|
|
388
|
+
* that now runs the neighbouring brand's firestore jar reads as ours on the pid
|
|
389
|
+
* alone. A jar that names a project id names it truthfully, so when this run
|
|
390
|
+
* knows its own project id and the live command line disagrees, the number is
|
|
391
|
+
* recycled and the process is left running. A jar that names none (the pubsub
|
|
392
|
+
* emulator) is decided by the record, as before.
|
|
393
|
+
* @param {{pid: number|string, command: string}} candidate - One ps row.
|
|
394
|
+
* @param {{pids: number[], projectId: string|null}} ownership - This run's record.
|
|
395
|
+
* @returns {boolean}
|
|
396
|
+
*/
|
|
397
|
+
function isStoppableEmulatorProcess(candidate, ownership) {
|
|
398
|
+
const command = String(candidate?.command || '');
|
|
399
|
+
|
|
400
|
+
if (!EMULATOR_COMMAND.test(command)) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const named = commandProjectId(command);
|
|
405
|
+
|
|
406
|
+
if (named && ownership?.projectId && named !== ownership.projectId) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return isOwnedEmulatorProcess(candidate, ownership);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Should the STALE-RECORD reap signal this recorded process?
|
|
415
|
+
*
|
|
416
|
+
* The stop path's bar first (isStoppableEmulatorProcess): emulator machinery
|
|
417
|
+
* that does not name a different project. That bar decides a jar naming NO
|
|
418
|
+
* project — the pubsub emulator — on record membership alone, and a record is a
|
|
419
|
+
* list of NUMBERS: a recycled number now running the NEIGHBOUR brand's pubsub
|
|
420
|
+
* jar reads exactly like our own leftover
|
|
421
|
+
* ([#730](https://github.com/Omega-JS-Stack/omega/issues/730)).
|
|
422
|
+
*
|
|
423
|
+
* So the live PARENT is the second proof, and it is only ever asked of a record
|
|
424
|
+
* whose root is already proven dead. A dead run's survivors are reparented (PID
|
|
425
|
+
* 1, where every orphan lands) or still hang off another member of that same
|
|
426
|
+
* dead run — the firebase parent that outlived the `sh` root, say. A number
|
|
427
|
+
* handed to somebody else's LIVE stack has a live parent that this record has
|
|
428
|
+
* never heard of, and it is left running.
|
|
429
|
+
* @param {{pid: number|string, ppid: number|string, command: string}} candidate - One ps row.
|
|
430
|
+
* @param {{pids: number[], projectId: string|null, rootPid: number|null}} ownership - The PREVIOUS record.
|
|
431
|
+
* @returns {boolean}
|
|
432
|
+
*/
|
|
433
|
+
function isReapableRecordedProcess(candidate, ownership) {
|
|
434
|
+
if (!isStoppableEmulatorProcess(candidate, ownership)) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const parent = Number(candidate?.ppid);
|
|
439
|
+
|
|
440
|
+
if (parent === 1 || parent === Number(ownership?.rootPid)) {
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return (ownership?.pids || []).some((recorded) => Number(recorded) === parent);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The ownership a pid record still supports, given its age.
|
|
449
|
+
*
|
|
450
|
+
* The pids are only evidence while they are known to name the processes they
|
|
451
|
+
* named at spawn: the record is never deleted, so a run days later reads the
|
|
452
|
+
* last one, and by then the OS may have handed those numbers to anything. The
|
|
453
|
+
* project id is not pid-based — a command line naming this project proves
|
|
454
|
+
* itself at any age — so it survives an expiry.
|
|
455
|
+
*
|
|
456
|
+
* The ports ride with the pids: they are the map the recorded run bound, and
|
|
457
|
+
* they are only ever read to settle ports those very pids just released.
|
|
458
|
+
* @param {object|null} record - The parsed pid-record file.
|
|
459
|
+
* @param {number} [now] - Epoch ms to age against (defaults to Date.now()).
|
|
460
|
+
* @returns {{pids: number[], projectId: string|null, rootPid: number|null, ports: object}}
|
|
461
|
+
*/
|
|
462
|
+
function ownershipFromRecord(record, now) {
|
|
463
|
+
const startedAt = Date.parse(record?.startedAt);
|
|
464
|
+
const fresh = Number.isFinite(startedAt) && (now || Date.now()) - startedAt < PID_RECORD_MAX_AGE_MS;
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
pids: fresh && Array.isArray(record?.pids) ? record.pids : [],
|
|
468
|
+
projectId: record?.projectId || null,
|
|
469
|
+
rootPid: record?.rootPid || null,
|
|
470
|
+
ports: fresh && record?.ports ? record.ports : {},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The command line a pid is running right now, or '' when it is gone.
|
|
476
|
+
* @param {number|string} pid - The pid to read.
|
|
477
|
+
* @returns {string}
|
|
478
|
+
*/
|
|
479
|
+
function readProcessCommand(pid) {
|
|
480
|
+
const { execSync } = require('child_process');
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
return execSync(`ps -o command= -p ${Number(pid)} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
484
|
+
} catch (error) {
|
|
485
|
+
return '';
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The command line a pid is running right now AND who forked it, or null when
|
|
491
|
+
* it is gone.
|
|
492
|
+
*
|
|
493
|
+
* The parent is half of the stale reap's identity proof, and it is only
|
|
494
|
+
* readable in the same breath as the command line: two `ps` calls describe two
|
|
495
|
+
* moments, and a number that changed hands between them reads as one process
|
|
496
|
+
* that never existed ([#730](https://github.com/Omega-JS-Stack/omega/issues/730)).
|
|
497
|
+
* @param {number|string} pid - The pid to read.
|
|
498
|
+
* @returns {{ppid: number, command: string}|null}
|
|
499
|
+
*/
|
|
500
|
+
function readProcessInfo(pid) {
|
|
501
|
+
const { execSync } = require('child_process');
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const row = execSync(`ps -o ppid=,command= -p ${Number(pid)} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
505
|
+
const match = row.match(/^\s*(\d+)\s+(.*)$/s);
|
|
506
|
+
|
|
507
|
+
return match ? { ppid: Number(match[1]), command: match[2] } : null;
|
|
508
|
+
} catch (error) {
|
|
509
|
+
// `ps` exiting 1 is the one provable answer: the pid does not exist. Any
|
|
510
|
+
// other failure (fork pressure, a spawn error) proves NOTHING — and it
|
|
511
|
+
// must read as alive-but-unprovable, never as gone, because the reap's
|
|
512
|
+
// root gate turns "gone" into a kill order. The sentinel is truthy (the
|
|
513
|
+
// root gate leaves the record alone) and matches no identity proof (a
|
|
514
|
+
// member read spares the pid) ([#730](https://github.com/Omega-JS-Stack/omega/issues/730)).
|
|
515
|
+
if (error.status === 1) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return { ppid: -1, command: '' };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Every descendant pid of `rootPid`, from one ps snapshot.
|
|
525
|
+
*
|
|
526
|
+
* Taken while the stack is UP: firebase-tools puts each java emulator in its
|
|
527
|
+
* own process group and they reparent to PID 1 once orphaned, so neither the
|
|
528
|
+
* group nor the parent link survives the moment the sweep needs it.
|
|
529
|
+
* @param {number} rootPid - The spawned child's pid.
|
|
530
|
+
* @returns {number[]} rootPid plus every descendant, deduped.
|
|
531
|
+
*/
|
|
532
|
+
function collectDescendantPids(rootPid) {
|
|
533
|
+
const { execSync } = require('child_process');
|
|
534
|
+
const children = new Map();
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const rows = execSync('ps -A -o pid=,ppid=', { encoding: 'utf8' }).trim().split('\n');
|
|
538
|
+
|
|
539
|
+
for (const row of rows) {
|
|
540
|
+
const [pid, ppid] = row.trim().split(/\s+/).map(Number);
|
|
541
|
+
|
|
542
|
+
if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue;
|
|
543
|
+
|
|
544
|
+
if (!children.has(ppid)) children.set(ppid, []);
|
|
545
|
+
children.get(ppid).push(pid);
|
|
546
|
+
}
|
|
547
|
+
} catch (error) {
|
|
548
|
+
return [rootPid];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const collected = new Set([rootPid]);
|
|
552
|
+
const queue = [rootPid];
|
|
553
|
+
|
|
554
|
+
while (queue.length > 0) {
|
|
555
|
+
for (const child of children.get(queue.shift()) || []) {
|
|
556
|
+
if (collected.has(child)) continue;
|
|
557
|
+
|
|
558
|
+
collected.add(child);
|
|
559
|
+
queue.push(child);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return [...collected];
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
class EmulatorCommand extends BaseCommand {
|
|
567
|
+
async execute() {
|
|
568
|
+
// Custom-server mode exports no Cloud Functions to emulate (#584)
|
|
569
|
+
if (refuseWhenCustom(this.main.firebaseProjectPath, 'emulator')) return;
|
|
570
|
+
|
|
571
|
+
// The emulator IS the backend's dev leg under brand-root `omega dev`, so it
|
|
572
|
+
// shares the dev-log lane with `omega serve` (they never run together — same
|
|
573
|
+
// ports). The firebase CHILD keeps its own dist/emulator.log (#197).
|
|
574
|
+
this.attachVerbLog('dev');
|
|
575
|
+
|
|
576
|
+
this.log(chalk.cyan('\n Starting Firebase emulator (keep-alive mode)...\n'));
|
|
577
|
+
this.log(chalk.gray(' Emulator will stay running until you press Ctrl+C\n'));
|
|
578
|
+
|
|
579
|
+
// Boot-time: seed the shared state file with whatever this emulator was
|
|
580
|
+
// started with. Two flows are supported:
|
|
581
|
+
// - Recommended: start emulator without the flag, set TEST_EXTENDED_MODE
|
|
582
|
+
// on `npx omega test` instead. The test command writes the file; the
|
|
583
|
+
// emulator's function workers watch it and flip live.
|
|
584
|
+
// - Also supported: start emulator with TEST_EXTENDED_MODE=true. We
|
|
585
|
+
// write the file here as a boot default. Useful for inspecting the
|
|
586
|
+
// emulator before any tests fire. Note: the next `npx omega test`
|
|
587
|
+
// overwrites the file regardless of how the emulator booted.
|
|
588
|
+
{
|
|
589
|
+
const projectDir = this.main.firebaseProjectPath;
|
|
590
|
+
const envSubset = captureSyncedEnv(process.env);
|
|
591
|
+
writeTestMode(projectDir, envSubset);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Show the standard warning if the emulator boots in extended mode.
|
|
595
|
+
if (process.env.TEST_EXTENDED_MODE) {
|
|
596
|
+
this.log(chalk.yellow.bold(`\n ${EXTENDED_MODE_WARNING[0]}`));
|
|
597
|
+
EXTENDED_MODE_WARNING.slice(1).forEach((line) => this.log(chalk.yellow(` ${line}`)));
|
|
598
|
+
this.log(chalk.gray(` (Tip: you can also flip mode per-run by setting TEST_EXTENDED_MODE on \`npx omega test\`.)`));
|
|
599
|
+
this.log('');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Start @omega.js/backend watcher in background. Keep the child handle —
|
|
603
|
+
// interactive Ctrl+C kills it via the terminal process group, but a
|
|
604
|
+
// PROGRAMMATIC signal to this process alone (e2e harness, kill -INT)
|
|
605
|
+
// doesn't, leaving an immortal nodemon re-creating the reload trigger
|
|
606
|
+
// (found live by the cp88 two-emulator proof).
|
|
607
|
+
const watcher = new WatchCommand(this.main);
|
|
608
|
+
const watcherChild = watcher.startBackground();
|
|
609
|
+
|
|
610
|
+
// The watcher hangs off THIS process, not off the emulator child, so the pid
|
|
611
|
+
// record's descendant walk from the emulator's root pid cannot see it — name
|
|
612
|
+
// it for the record explicitly, or a run that dies without teardown leaves a
|
|
613
|
+
// nodemon no later stop path can even name
|
|
614
|
+
// ([#629](https://github.com/Omega-JS-Stack/omega/issues/629)).
|
|
615
|
+
this.backgroundWatcherPid = watcherChild ? watcherChild.pid : null;
|
|
616
|
+
|
|
617
|
+
// Keep-alive: boot emulators and wait for Ctrl+C. No "command" subprocess —
|
|
618
|
+
// the emulator child IS the foreground process from the user's perspective.
|
|
619
|
+
// HTTPS default-on for the interactive command (--no-https disables); the
|
|
620
|
+
// `omega test` auto-start path never passes it — the harness talks plain
|
|
621
|
+
// http to hosting directly.
|
|
622
|
+
// Kept outside the try so the failure path below can stop whatever came up.
|
|
623
|
+
let started = null;
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
started = await this.startEmulators({
|
|
627
|
+
https: this.argv.https !== false,
|
|
628
|
+
});
|
|
629
|
+
const { shutdown, emulatorPorts, exitPromise } = started;
|
|
630
|
+
|
|
631
|
+
// Start Stripe webhook forwarding in background — AFTER boot so it
|
|
632
|
+
// targets the RESOLVED hosting port, not a classic that may have bumped
|
|
633
|
+
this.startStripeWebhookForwarding(emulatorPorts.hosting);
|
|
634
|
+
|
|
635
|
+
// Seed personas unless --no-seed was passed (yargs boolean negation:
|
|
636
|
+
// `--no-seed` parses as argv.seed === false). seedPersonas is fully
|
|
637
|
+
// non-fatal — any failure logs a warning and the emulator keeps running.
|
|
638
|
+
if (this.argv.seed !== false) {
|
|
639
|
+
await this.seedPersonas(emulatorPorts);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// NOTE: this line is a readiness MARKER for external drivers (the devkit
|
|
643
|
+
// e2e harness waits for it) — it must print AFTER seeding so drivers
|
|
644
|
+
// don't race the seed wipe. Change the text in lockstep with
|
|
645
|
+
// @omega.js/devkit/test/e2e-harness.js.
|
|
646
|
+
this.log(chalk.gray('\n Emulator ready. Press Ctrl+C to shut down...\n'));
|
|
647
|
+
|
|
648
|
+
// Synchronous stop handler — must NOT be async. In Node, registering a
|
|
649
|
+
// listener for one of these signals suppresses the default "exit on signal"
|
|
650
|
+
// behavior, but only if the listener is present when the signal fires. An
|
|
651
|
+
// async handler loses the race: Node starts it, doesn't await it, and the
|
|
652
|
+
// process dies mid-shutdown. So: set a flag synchronously, kick off shutdown
|
|
653
|
+
// (no await), and let the main `await exitPromise` below resolve naturally
|
|
654
|
+
// once shutdown kills the child.
|
|
655
|
+
let stopCount = 0;
|
|
656
|
+
// The handler cannot await, but the run must not exit ahead of the
|
|
657
|
+
// shutdown it started: the jar reap runs AFTER the child is gone, and
|
|
658
|
+
// process.exit() below would cut it off mid-signal
|
|
659
|
+
// ([#304](https://github.com/Omega-JS-Stack/omega/issues/304)). Keep the
|
|
660
|
+
// promise and join it once the child has exited.
|
|
661
|
+
let shutdownRun = null;
|
|
662
|
+
const onStopSignal = () => {
|
|
663
|
+
stopCount++;
|
|
664
|
+
if (stopCount === 1) {
|
|
665
|
+
this.log(chalk.gray('\n Shutting down emulator... (Ctrl+C again to force kill)'));
|
|
666
|
+
shutdownRun = shutdown();
|
|
667
|
+
} else {
|
|
668
|
+
this.log(chalk.gray(' Force killing emulator...'));
|
|
669
|
+
shutdownRun = shutdown();
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
// ONE handler, three signals: the stop path is the same whether the ask is
|
|
673
|
+
// Ctrl+C, `omega dev`'s SIGTERM, or the SIGHUP of a closed terminal
|
|
674
|
+
// ([#629](https://github.com/Omega-JS-Stack/omega/issues/629)).
|
|
675
|
+
STOP_SIGNALS.forEach((signal) => process.on(signal, onStopSignal));
|
|
676
|
+
|
|
677
|
+
// Resolve when the emulator exits (via shutdown or crash)
|
|
678
|
+
await exitPromise;
|
|
679
|
+
// A child that exited on its own (a crash, firebase-tools stopping
|
|
680
|
+
// itself) started no shutdown — run one anyway, so the same reap covers
|
|
681
|
+
// both ways this line is reached. shutdown() owns the whole teardown now:
|
|
682
|
+
// the recorded jars, the orphan sweep, and the port verdict. The stop
|
|
683
|
+
// listeners stay active across it so Ctrl+C spam can't cut it short.
|
|
684
|
+
await (shutdownRun || shutdown());
|
|
685
|
+
// Reap the background watcher — it is NOT in the firebase child's
|
|
686
|
+
// process group, so nothing else kills it on a programmatic shutdown.
|
|
687
|
+
if (watcherChild) {
|
|
688
|
+
try { watcherChild.kill('SIGTERM'); } catch (e) { /* already gone */ }
|
|
689
|
+
}
|
|
690
|
+
STOP_SIGNALS.forEach((signal) => process.removeListener(signal, onStopSignal));
|
|
691
|
+
this.log(chalk.gray(' Emulator stopped.\n'));
|
|
692
|
+
if (stopCount > 0) {
|
|
693
|
+
process.exit(0);
|
|
694
|
+
}
|
|
695
|
+
} catch (error) {
|
|
696
|
+
this.logError(`Emulator error: ${error.message || error}`);
|
|
697
|
+
// A failed boot spawned things too: the stack (startEmulators stops its
|
|
698
|
+
// own before it throws) and this frame's background watcher, which
|
|
699
|
+
// nothing else signals — exiting here left an immortal nodemon behind
|
|
700
|
+
// ([#304](https://github.com/Omega-JS-Stack/omega/issues/304)). Anything
|
|
701
|
+
// that got as far as a handle gets the same stop path.
|
|
702
|
+
if (started) {
|
|
703
|
+
try { await started.shutdown(); } catch (e) { /* the stop path already reported */ }
|
|
704
|
+
}
|
|
705
|
+
if (watcherChild) {
|
|
706
|
+
try { watcherChild.kill('SIGTERM'); } catch (e) { /* already gone */ }
|
|
707
|
+
}
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Seed the running emulator with test personas so a developer can sign in
|
|
714
|
+
* manually on any emulator-connected dev site (email + TEST_ACCOUNT_PASSWORD).
|
|
715
|
+
* Fully non-fatal — the ENTIRE body is guarded; any failure (config load,
|
|
716
|
+
* firebase-admin init, account creation) logs a warning and the emulator
|
|
717
|
+
* keeps running. execute()'s catch would otherwise process.exit(1).
|
|
718
|
+
*/
|
|
719
|
+
async seedPersonas(emulatorPorts) {
|
|
720
|
+
try {
|
|
721
|
+
this.log(chalk.cyan('\n Seeding test personas...\n'));
|
|
722
|
+
|
|
723
|
+
const projectDir = this.main.firebaseProjectPath;
|
|
724
|
+
const functionsDir = path.join(projectDir, 'dist');
|
|
725
|
+
|
|
726
|
+
// Load project config (same pattern as test.js loadProjectConfig)
|
|
727
|
+
const { hasOmegaConfig, loadConfig, loadEnv, resolvedBrandHost } = require('../../vendor/config/index.js');
|
|
728
|
+
loadEnv(functionsDir);
|
|
729
|
+
|
|
730
|
+
let config = {};
|
|
731
|
+
let domain = '';
|
|
732
|
+
if (hasOmegaConfig(functionsDir)) {
|
|
733
|
+
config = loadConfig(functionsDir, 'backend').config;
|
|
734
|
+
// A persona lives on the brand's HOST — the one derivation, shared with
|
|
735
|
+
// the dev palette that signs in as one
|
|
736
|
+
// ([#708](https://github.com/Omega-JS-Stack/omega/issues/708)). The
|
|
737
|
+
// brand's contact address is a support inbox — often on the apex while
|
|
738
|
+
// the site is a subdomain of it — so it shapes no persona identity.
|
|
739
|
+
domain = resolvedBrandHost(config);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Persona emails are `_test.<id>@{domain}` — without a domain every
|
|
743
|
+
// createUser call fails with an invalid email. Skip cleanly instead.
|
|
744
|
+
if (!domain) {
|
|
745
|
+
this.logWarning('Skipping persona seeding: no brand.url in config/omega.json5 (personas are seeded on the brand host)');
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Point firebase-admin at the emulators. Set AFTER the firebase child was
|
|
750
|
+
// spawned, so only THIS process (and the seed module's emulator-only
|
|
751
|
+
// guards) see them — the emulator child's env is unaffected.
|
|
752
|
+
process.env.FIRESTORE_EMULATOR_HOST = `127.0.0.1:${emulatorPorts.firestore}`;
|
|
753
|
+
process.env.FIREBASE_AUTH_EMULATOR_HOST = `127.0.0.1:${emulatorPorts.auth}`;
|
|
754
|
+
process.env.GCLOUD_PROJECT = config.cloud?.config?.projectId || 'demo-test';
|
|
755
|
+
|
|
756
|
+
const firebaseAdmin = require('firebase-admin');
|
|
757
|
+
if (firebaseAdmin.apps.length === 0) {
|
|
758
|
+
firebaseAdmin.initializeApp({
|
|
759
|
+
projectId: process.env.GCLOUD_PROJECT,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const { TEST_ACCOUNT_PASSWORD } = require('../../test/test-accounts.js');
|
|
764
|
+
const result = await seed({
|
|
765
|
+
admin: firebaseAdmin,
|
|
766
|
+
domain,
|
|
767
|
+
config,
|
|
768
|
+
projectDir,
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
if (result.accounts) {
|
|
772
|
+
this.log(chalk.green(`\n ✓ Personas seeded (${result.created} accounts)`));
|
|
773
|
+
this.log(chalk.gray(` Sign in on any emulator-connected dev site with email + password: ${TEST_ACCOUNT_PASSWORD}\n`));
|
|
774
|
+
} else {
|
|
775
|
+
this.logWarning('Persona seeding completed with errors (see above)');
|
|
776
|
+
}
|
|
777
|
+
} catch (e) {
|
|
778
|
+
this.logWarning(`Persona seeding failed: ${e.message}`);
|
|
779
|
+
this.log(chalk.gray(' The emulator is still running — seeding is non-fatal. Re-run with --no-seed to skip.\n'));
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Boot Firebase emulators as a long-running child process.
|
|
785
|
+
* Stdout/stderr are teed to console + emulator.log.
|
|
786
|
+
* Resolves once the emulator hub is listening (i.e., emulators are ready).
|
|
787
|
+
* Caller is responsible for calling shutdown() to send SIGTERM and wait for exit.
|
|
788
|
+
*
|
|
789
|
+
* @param {object} [options]
|
|
790
|
+
* @param {boolean} [options.https] - Front the public hosting port with the
|
|
791
|
+
* shared mkcert TLS proxy (interactive `omega emulator` default)
|
|
792
|
+
* @param {string} [options.environment] - The environment the stage composes
|
|
793
|
+
* dist/.env for (#586). Omitted = `development`, the local lane's pin —
|
|
794
|
+
* never the shell's answer, so an exported ENVIRONMENT can't stage
|
|
795
|
+
* production credentials into a local boot. `omega test`'s auto-start
|
|
796
|
+
* names `testing`.
|
|
797
|
+
*
|
|
798
|
+
* The STAGED overlay and the emulator RUNTIME's own environment answer
|
|
799
|
+
* differ on purpose: functions running under the emulator report `testing`
|
|
800
|
+
* (OMEGA_TEST_MODE), while the artifact they read was composed from
|
|
801
|
+
* `.env.development` — the file lane is the developer's local credentials,
|
|
802
|
+
* the runtime lane is "don't do real side effects".
|
|
803
|
+
* @returns {Promise<{ child: ChildProcess, shutdown: () => Promise<void>, emulatorPorts: object }>}
|
|
804
|
+
*/
|
|
805
|
+
async startEmulators(options) {
|
|
806
|
+
const projectDir = this.main.firebaseProjectPath;
|
|
807
|
+
|
|
808
|
+
// dist/ is staged output (src/dist pillar): stage fresh (including
|
|
809
|
+
// dist/public/ for hosting), then keep it fresh — the emulator watches
|
|
810
|
+
// dist/ natively, so a re-stage IS the hot reload. The watcher belongs to
|
|
811
|
+
// the boot ATTEMPT below, which dies with the emulator child it feeds.
|
|
812
|
+
// The local lane's pin (#586): a boot nobody named an environment for is
|
|
813
|
+
// `development`, so the composed dist/.env can never follow the shell.
|
|
814
|
+
const environment = options?.environment || 'development';
|
|
815
|
+
this.ensureStaged({ environment });
|
|
816
|
+
|
|
817
|
+
// N7 port allocation: firebase.json values (classic defaults) when free,
|
|
818
|
+
// bump-if-taken — a second brand's stack relocates instead of the old
|
|
819
|
+
// behavior of KILLING the incumbent. Explicit config `ports` pins never
|
|
820
|
+
// bump (busy pin = hard error).
|
|
821
|
+
const wanted = loadEmulatorPorts(projectDir);
|
|
822
|
+
|
|
823
|
+
// HTTPS (the classic https://localhost:5002 contract, shared with `omega
|
|
824
|
+
// serve` + web's `omega dev` via @omega.js/devkit/local-https): the PUBLIC
|
|
825
|
+
// hosting port speaks TLS through the mkcert proxy; hosting itself moves
|
|
826
|
+
// to an internal plain-http port. No mkcert → plain http on the classic
|
|
827
|
+
// port, same fallback as serve.
|
|
828
|
+
let httpsCerts = null;
|
|
829
|
+
if (options?.https) {
|
|
830
|
+
const { ensureLocalHttpsCerts, mkcertInstallHint } = require('../../vendor/devkit/local-https.js');
|
|
831
|
+
httpsCerts = await ensureLocalHttpsCerts({
|
|
832
|
+
certsDir: path.join(this.getTempPath(), 'certs'),
|
|
833
|
+
log: (line) => this.log(chalk.gray(` ${line}`)),
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
if (httpsCerts) {
|
|
837
|
+
wanted.https = wanted.hosting;
|
|
838
|
+
wanted.hosting = 5443;
|
|
839
|
+
} else {
|
|
840
|
+
this.log(chalk.yellow(' HTTPS disabled — could not obtain certificates.'));
|
|
841
|
+
this.log(chalk.yellow(` Install mkcert for trusted local HTTPS: ${mkcertInstallHint()}\n`));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Crash leftovers first, MACHINE-WIDE: a run that died without teardown
|
|
846
|
+
// leaves java emulator grandchildren and functions workers squatting
|
|
847
|
+
// FOREVER, and the port-driven reaper that used to stand here could only
|
|
848
|
+
// ever see the ports THIS run wants and only what it could prove was this
|
|
849
|
+
// project's. A hub on a bumped port, a legacy install's workers, another
|
|
850
|
+
// brand's leftovers: all invisible, all still holding memory and ports. An
|
|
851
|
+
// ORPHAN of the emulator family is nobody's, so it goes whatever project it
|
|
852
|
+
// names ([#781](https://github.com/Omega-JS-Stack/omega/issues/781)). The
|
|
853
|
+
// strict command-shape match is what survives of
|
|
854
|
+
// [#293](https://github.com/Omega-JS-Stack/omega/issues/293): live stacks,
|
|
855
|
+
// reload watchers and other users' processes are never candidates.
|
|
856
|
+
await this.reapMachineOrphans();
|
|
857
|
+
|
|
858
|
+
// Then the leftovers no ORPHAN test can reach: a dead run's members that
|
|
859
|
+
// still hang off another member of that same dead run. The record naming
|
|
860
|
+
// them is about to be overwritten by this boot — read it while it still
|
|
861
|
+
// exists ([#721](https://github.com/Omega-JS-Stack/omega/issues/721)).
|
|
862
|
+
const recorded = this.readEmulatorOwnership();
|
|
863
|
+
const ownership = {
|
|
864
|
+
pids: recorded.pids,
|
|
865
|
+
projectId: this.loadProjectId(projectDir) || recorded.projectId,
|
|
866
|
+
rootPid: recorded.rootPid,
|
|
867
|
+
ports: recorded.ports,
|
|
868
|
+
};
|
|
869
|
+
await this.reapStaleRecordedProcesses(ownership);
|
|
870
|
+
|
|
871
|
+
// ONE bump-and-retry (#778): the allocator probed these ports seconds ago,
|
|
872
|
+
// and on a machine that shares them with another brand's session a foreign
|
|
873
|
+
// listener can take one between that probe and firebase-tools' bind. The
|
|
874
|
+
// port the child could not bind is marked claimed, so the next allocation
|
|
875
|
+
// moves that name off it, republishes the map, and boots again. A second
|
|
876
|
+
// failure is no longer a race: something is sitting on that port, and the
|
|
877
|
+
// report names it (and never signals it).
|
|
878
|
+
const taken = new Set();
|
|
879
|
+
|
|
880
|
+
try {
|
|
881
|
+
return await this.bootEmulatorStack({ projectDir, environment, wanted, httpsCerts, taken });
|
|
882
|
+
} catch (error) {
|
|
883
|
+
if (!error?.addressInUsePort) {
|
|
884
|
+
throw error;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
taken.add(error.addressInUsePort);
|
|
888
|
+
this.log(chalk.yellow(` Port ${error.addressInUsePort}${error.addressInUseName ? ` (${error.addressInUseName})` : ''} was taken while the stack came up; reallocating around it and booting once more.`));
|
|
889
|
+
|
|
890
|
+
try {
|
|
891
|
+
return await this.bootEmulatorStack({ projectDir, environment, wanted, httpsCerts, taken });
|
|
892
|
+
} catch (retryError) {
|
|
893
|
+
if (!retryError?.addressInUsePort) {
|
|
894
|
+
throw retryError;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
throw new Error(formatAddressInUseFailure(
|
|
898
|
+
{ port: retryError.addressInUsePort, name: retryError.addressInUseName },
|
|
899
|
+
this.describePortHolders(retryError.addressInUsePort),
|
|
900
|
+
));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* ONE boot attempt: allocate the ports, publish them, spawn firebase-tools,
|
|
907
|
+
* and wait for the ready marker.
|
|
908
|
+
*
|
|
909
|
+
* Each attempt owns its stage watcher, because the watcher is torn down with
|
|
910
|
+
* the emulator child (exitPromise below), so a failed attempt takes its
|
|
911
|
+
* watcher with it and the retry starts a fresh one, instead of hot-reloading
|
|
912
|
+
* into a stack that no longer exists (#778).
|
|
913
|
+
* @param {object} attempt
|
|
914
|
+
* @param {string} attempt.projectDir - The firebase project directory.
|
|
915
|
+
* @param {string} attempt.environment - The environment each re-stage composes for.
|
|
916
|
+
* @param {object} attempt.wanted - The wanted name to port map (firebase.json + https).
|
|
917
|
+
* @param {object|null} attempt.httpsCerts - The mkcert pair, when HTTPS is on.
|
|
918
|
+
* @param {Set<number>} attempt.taken - Ports this attempt may not allocate:
|
|
919
|
+
* the ones a previous attempt proved something else is holding.
|
|
920
|
+
* @returns {Promise<{ child: ChildProcess, shutdown: () => Promise<void>, emulatorPorts: object, bumped: string[], exitPromise: Promise<object> }>}
|
|
921
|
+
*/
|
|
922
|
+
async bootEmulatorStack({ projectDir, environment, wanted, httpsCerts, taken }) {
|
|
923
|
+
// The stamp for THIS attempt, and the newest one owns the shared artifacts.
|
|
924
|
+
// A first attempt's child can close LATE: after shutdown's SIGTERM/SIGKILL
|
|
925
|
+
// races gave up on it, and after the retry published its own map. Both
|
|
926
|
+
// attempts stamp the same process.pid, so the pid cannot tell them apart
|
|
927
|
+
// ([#778](https://github.com/Omega-JS-Stack/omega/issues/778)).
|
|
928
|
+
const attempt = this.activeAttempt = (this.activeAttempt || 0) + 1;
|
|
929
|
+
const stageWatch = this.startStageWatch({ environment });
|
|
930
|
+
|
|
931
|
+
const { ports: emulatorPorts, bumped } = await resolvePorts({
|
|
932
|
+
wanted,
|
|
933
|
+
pins: this.loadPortPins(projectDir),
|
|
934
|
+
// A FRESH claimed set per attempt: the allocator claims every port it
|
|
935
|
+
// hands out into it, so carrying one over would bump the whole map. The
|
|
936
|
+
// only thing an attempt inherits is what the last one proved is taken.
|
|
937
|
+
claimed: new Set(taken),
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// Preflight: the allocator has relocated around every busy port it owns,
|
|
941
|
+
// so anything still held is a port this run cannot move off. Report it now,
|
|
942
|
+
// by name, instead of spawning a stack that never comes up and burning the
|
|
943
|
+
// whole ready deadline first
|
|
944
|
+
// ([#332](https://github.com/Omega-JS-Stack/omega/issues/332)).
|
|
945
|
+
try {
|
|
946
|
+
await this.preflightEmulatorPorts(projectDir, emulatorPorts);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
// Nothing is spawned yet, but the stage watcher is: it is normally torn
|
|
949
|
+
// down with the emulator child, and this boot will never have one.
|
|
950
|
+
stageWatch.close();
|
|
951
|
+
throw error;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Bumped ports can't ride the committed firebase.json — materialize a
|
|
955
|
+
// patched copy NEXT TO it (same dir, so relative paths keep resolving)
|
|
956
|
+
// and boot with --config. The https flip ALWAYS materializes (hosting
|
|
957
|
+
// moved to the internal port). Defaults-free runs spawn exactly as always.
|
|
958
|
+
let configFlag = '';
|
|
959
|
+
if (bumped.length > 0 || httpsCerts) {
|
|
960
|
+
const resolvedName = 'firebase.resolved.json';
|
|
961
|
+
const firebaseConfig = JSON5.parse(jetpack.read(path.join(projectDir, 'firebase.json')));
|
|
962
|
+
for (const [name, port] of Object.entries(emulatorPorts)) {
|
|
963
|
+
if (firebaseConfig.emulators?.[name]) {
|
|
964
|
+
firebaseConfig.emulators[name].port = port;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
jetpack.write(path.join(projectDir, resolvedName), JSON.stringify(firebaseConfig, null, 2));
|
|
968
|
+
configFlag = ` --config ${resolvedName}`;
|
|
969
|
+
if (bumped.length > 0) {
|
|
970
|
+
this.log(chalk.yellow(` Ports in use — bumped: ${bumped.map((name) => `${name}→${emulatorPorts[name]}`).join(', ')} (booting via ${resolvedName})`));
|
|
971
|
+
}
|
|
972
|
+
if (httpsCerts) {
|
|
973
|
+
this.log(chalk.gray(` Hosting moves to internal :${emulatorPorts.hosting} under the HTTPS proxy on :${emulatorPorts.https} (booting via ${resolvedName})`));
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Publish the resolved map: env for our own children (functions workers
|
|
978
|
+
// inherit through the firebase spawn; URL getters read OMEGA_*_PORT) and
|
|
979
|
+
// the ports file for sibling processes of this brand (`omega test`
|
|
980
|
+
// against a running emulator, `omega dev`, the e2e harness).
|
|
981
|
+
Object.assign(process.env, portsToEnv(emulatorPorts));
|
|
982
|
+
writePortsFile(projectDir, emulatorPorts);
|
|
983
|
+
|
|
984
|
+
// Start the TLS terminator now — requests 502 until hosting is up, then
|
|
985
|
+
// https://localhost:<https> serves the whole surface. Torn down with the
|
|
986
|
+
// emulator child (close handler below).
|
|
987
|
+
let httpsProxy = null;
|
|
988
|
+
if (httpsCerts) {
|
|
989
|
+
const { startLocalHttpsProxy } = require('../../vendor/devkit/local-https.js');
|
|
990
|
+
httpsProxy = startLocalHttpsProxy({
|
|
991
|
+
port: emulatorPorts.https,
|
|
992
|
+
targetPort: emulatorPorts.hosting,
|
|
993
|
+
certs: httpsCerts,
|
|
994
|
+
log: (line) => this.log(chalk.green(` ${line}`)),
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Wipe stale firebase-tools debug logs + any leftover @omega.js/backend logs from older versions.
|
|
999
|
+
this.sweepStaleLogs();
|
|
1000
|
+
|
|
1001
|
+
// The emulator child's own log, beside firebase-tools' *-debug.log files.
|
|
1002
|
+
// The reset sentinel lets `omega test` ask this long-lived log for a fresh
|
|
1003
|
+
// slate mid-run; the poll + roll live in the shared child-log sink.
|
|
1004
|
+
const logPath = this.getLogsPath('emulator.log');
|
|
1005
|
+
const childLog = createChildLog({
|
|
1006
|
+
logPath,
|
|
1007
|
+
resetPath: this.getTempPath('emulator.log.reset'),
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
// Write pre-emulator info to log file
|
|
1011
|
+
if (process.env.TEST_EXTENDED_MODE) {
|
|
1012
|
+
EXTENDED_MODE_WARNING.forEach((line) => childLog.write(`${line}\n`));
|
|
1013
|
+
childLog.write('\n');
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
this.log(chalk.gray(` Logs saving to: ${logPath}`));
|
|
1017
|
+
|
|
1018
|
+
// OMEGA_TEST_MODE=true is passed so Functions skip external API calls (emails, SendGrid)
|
|
1019
|
+
// hosting is included so localhost:5002 rewrites work (e.g., /omega -> omega_api)
|
|
1020
|
+
// pubsub is included so scheduled functions (omega_cronDaily) can be triggered in tests
|
|
1021
|
+
const env = {
|
|
1022
|
+
...process.env,
|
|
1023
|
+
FORCE_COLOR: '1',
|
|
1024
|
+
OMEGA_TEST_MODE: 'true',
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
// Internal calls (Manager.getApiUrl) loop through the HTTPS proxy under the
|
|
1028
|
+
// local mkcert certificate. The child TRUSTS that root (#795) instead of
|
|
1029
|
+
// switching verification off wholesale — same handoff as `omega serve` and
|
|
1030
|
+
// `omega dev`'s legs, and Node prints no warning for it. A shell-set value
|
|
1031
|
+
// wins verbatim; only a host whose mkcert root vanished under existing
|
|
1032
|
+
// certs still takes the old bypass.
|
|
1033
|
+
if (httpsCerts) {
|
|
1034
|
+
const { mkcertCaRootPem } = require('../../vendor/devkit/local-https.js');
|
|
1035
|
+
const caPem = process.env.NODE_EXTRA_CA_CERTS || mkcertCaRootPem();
|
|
1036
|
+
|
|
1037
|
+
if (caPem) {
|
|
1038
|
+
env.NODE_EXTRA_CA_CERTS = caPem;
|
|
1039
|
+
} else {
|
|
1040
|
+
env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
1041
|
+
this.log(chalk.yellow(' mkcert root CA not found — internal calls fall back to NODE_TLS_REJECT_UNAUTHORIZED=0 (Node will warn)'));
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Spawn `firebase emulators:start` as a background child through the host's
|
|
1046
|
+
// own shell (spawnShell owns which shell that is), so its PATH resolves
|
|
1047
|
+
// `firebase` the way the interactive shell does.
|
|
1048
|
+
//
|
|
1049
|
+
// `detached: true` puts the child into its own process group. We need this so that
|
|
1050
|
+
// shutdown() can kill the entire group (sh → firebase → java emulators) by
|
|
1051
|
+
// signalling the negative pgid. Without it, SIGTERM to the shell doesn't propagate
|
|
1052
|
+
// to firebase or its java grandchildren, leaving orphan firestore/pubsub processes.
|
|
1053
|
+
const child = spawnShell(`firebase emulators:start ${EMULATOR_FLAGS}${configFlag}`, {
|
|
1054
|
+
cwd: projectDir,
|
|
1055
|
+
env,
|
|
1056
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1057
|
+
detached: true,
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
// Wire readiness detection into the stdout/stderr handlers.
|
|
1061
|
+
//
|
|
1062
|
+
// We watch for firebase-tools' explicit "All emulators ready!" line — that's the
|
|
1063
|
+
// signal that function discovery + load is complete and the runtime can serve HTTP.
|
|
1064
|
+
// Port-listening alone isn't enough: firebase-tools binds the functions socket
|
|
1065
|
+
// ~5-10s before user functions are actually loadable, so HTTP requests fail with
|
|
1066
|
+
// ECONNREFUSED / "fetch failed" if we proceed when only the port is open.
|
|
1067
|
+
let readyResolve;
|
|
1068
|
+
let readyReject;
|
|
1069
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
1070
|
+
readyResolve = resolve;
|
|
1071
|
+
readyReject = reject;
|
|
1072
|
+
});
|
|
1073
|
+
let ready = false;
|
|
1074
|
+
const READY_MARKER = /All emulators ready/i;
|
|
1075
|
+
|
|
1076
|
+
// The other end a boot can come to: firebase-tools failing to bind a port
|
|
1077
|
+
// the allocator handed it, because something took it in between (#778).
|
|
1078
|
+
// Read here rather than left to the ready deadline, so the retry starts in
|
|
1079
|
+
// the second the child gave up instead of three minutes later, and TAGGED,
|
|
1080
|
+
// so the caller retries this failure and no other.
|
|
1081
|
+
let inUsePort = null;
|
|
1082
|
+
const noteAddressInUse = (text) => {
|
|
1083
|
+
if (ready || inUsePort) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const port = addressInUsePort(text, Object.values(emulatorPorts));
|
|
1088
|
+
|
|
1089
|
+
if (!port) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
inUsePort = port;
|
|
1094
|
+
const name = portName(emulatorPorts, port);
|
|
1095
|
+
|
|
1096
|
+
readyReject(Object.assign(
|
|
1097
|
+
new Error(`Emulator could not bind port ${port}${name ? ` (${name})` : ''}: something took it between the port probe and the bind`),
|
|
1098
|
+
{ addressInUsePort: port, addressInUseName: name },
|
|
1099
|
+
));
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
child.stdout.on('data', (data) => {
|
|
1103
|
+
const text = data.toString();
|
|
1104
|
+
process.stdout.write(data);
|
|
1105
|
+
childLog.write(data);
|
|
1106
|
+
if (!ready && READY_MARKER.test(text)) {
|
|
1107
|
+
ready = true;
|
|
1108
|
+
readyResolve();
|
|
1109
|
+
}
|
|
1110
|
+
noteAddressInUse(text);
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
child.stderr.on('data', (data) => {
|
|
1114
|
+
const text = data.toString();
|
|
1115
|
+
process.stderr.write(data);
|
|
1116
|
+
childLog.write(data);
|
|
1117
|
+
// firebase-tools prints the ready line to stderr sometimes — watch both.
|
|
1118
|
+
if (!ready && READY_MARKER.test(text)) {
|
|
1119
|
+
ready = true;
|
|
1120
|
+
readyResolve();
|
|
1121
|
+
}
|
|
1122
|
+
noteAddressInUse(text);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
// Track exit state so shutdown() can resolve when the process is gone
|
|
1126
|
+
let exitPromiseResolve;
|
|
1127
|
+
const exitPromise = new Promise((resolve) => {
|
|
1128
|
+
exitPromiseResolve = resolve;
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
child.on('close', (code, signal) => {
|
|
1132
|
+
childLog.close();
|
|
1133
|
+
// The TLS proxy lives in THIS process — release the public port with
|
|
1134
|
+
// the stack (and drop any keep-alive sockets holding it open). Never
|
|
1135
|
+
// gated on the attempt: this handle is the attempt's OWN server, it can
|
|
1136
|
+
// reach no other attempt's proxy, and a retry that found this port still
|
|
1137
|
+
// held simply allocated around it. Skipping the close would strand the
|
|
1138
|
+
// port for the life of the CLI process (#778).
|
|
1139
|
+
if (httpsProxy) {
|
|
1140
|
+
httpsProxy.close();
|
|
1141
|
+
httpsProxy.closeAllConnections?.();
|
|
1142
|
+
}
|
|
1143
|
+
// Retract the published port map (clean shutdown). The resolved
|
|
1144
|
+
// firebase config is per-run scratch — remove it too.
|
|
1145
|
+
//
|
|
1146
|
+
// Both are shared BY NAME (one of each per project), and a retried boot
|
|
1147
|
+
// has already replaced both by the time a late first attempt closes:
|
|
1148
|
+
// retracting them here would unpublish the stack that is actually up and
|
|
1149
|
+
// delete the config it is running on. Only the attempt still in charge
|
|
1150
|
+
// cleans up (#778).
|
|
1151
|
+
if (this.activeAttempt === attempt) {
|
|
1152
|
+
clearPortsFile(projectDir);
|
|
1153
|
+
try { fs.unlinkSync(path.join(projectDir, 'firebase.resolved.json')); } catch (e) { /* not a bumped run */ }
|
|
1154
|
+
}
|
|
1155
|
+
exitPromiseResolve({ code, signal });
|
|
1156
|
+
// If we exited before becoming ready, fail the readiness wait too
|
|
1157
|
+
if (!ready) {
|
|
1158
|
+
readyReject(new Error(`Emulator child exited before ready (code=${code}, signal=${signal})`));
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
// The stage watcher lives exactly as long as the emulator child — bound
|
|
1163
|
+
// here, before the readiness wait, so a boot that never comes up takes it
|
|
1164
|
+
// down too ([#304](https://github.com/Omega-JS-Stack/omega/issues/304)).
|
|
1165
|
+
exitPromise.then(() => stageWatch.close());
|
|
1166
|
+
|
|
1167
|
+
// shutdown() signals the entire emulator process group (sh + firebase + java
|
|
1168
|
+
// grandchildren), waits up to 10s for clean exit, then escalates to SIGKILL.
|
|
1169
|
+
//
|
|
1170
|
+
// We use `process.kill(-pgid, ...)` instead of `child.kill(...)` because firebase
|
|
1171
|
+
// tools spawns several Java subprocesses (firestore + pubsub) that survive if
|
|
1172
|
+
// only the sh wrapper is killed. The negative PID targets the whole process group
|
|
1173
|
+
// (made possible by `detached: true` above).
|
|
1174
|
+
const killGroup = (signal) => {
|
|
1175
|
+
try {
|
|
1176
|
+
process.kill(-child.pid, signal);
|
|
1177
|
+
} catch (e) {
|
|
1178
|
+
// ESRCH = group already dead. EPERM = macOS kill(2) quirk when the
|
|
1179
|
+
// group's remaining members are zombies awaiting reap. Either way the
|
|
1180
|
+
// group is unreachable — and killGroup must NEVER throw out of
|
|
1181
|
+
// shutdown(): the un-awaited shutdown() in the SIGINT handlers turns
|
|
1182
|
+
// a throw into an unhandled rejection that crashes the CLI BEFORE
|
|
1183
|
+
// the orphan sweep + ports-file cleanup run (this exact crash leaked
|
|
1184
|
+
// 3 java emulators per e2e run). The port-based sweep after shutdown
|
|
1185
|
+
// is the safety net for anything a failed signal left behind.
|
|
1186
|
+
if (e.code !== 'ESRCH' && e.code !== 'EPERM') {
|
|
1187
|
+
this.log(chalk.yellow(` killGroup(${signal}) failed: ${e.message} — relying on the orphan sweep`));
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
let shutdownDone = false;
|
|
1193
|
+
const shutdown = async () => {
|
|
1194
|
+
if (shutdownDone) {
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// 0. Re-record the pid set while the tree is still attached. The boot
|
|
1199
|
+
// snapshot misses anything spawned since (function runtime workers come
|
|
1200
|
+
// and go), and once the parent is gone the descendants reparent to PID 1
|
|
1201
|
+
// — this is the last moment ownership is readable
|
|
1202
|
+
// ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)).
|
|
1203
|
+
try {
|
|
1204
|
+
this.writeEmulatorPidRecord(child.pid, this.loadProjectId(projectDir), emulatorPorts);
|
|
1205
|
+
} catch (error) { /* the boot record still stands */ }
|
|
1206
|
+
|
|
1207
|
+
// 1. Signal the process group (sh + firebase + direct children)
|
|
1208
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
1209
|
+
killGroup('SIGTERM');
|
|
1210
|
+
|
|
1211
|
+
// Wait up to 5s for clean exit, then SIGKILL the group
|
|
1212
|
+
const exited = await Promise.race([
|
|
1213
|
+
exitPromise.then(() => true),
|
|
1214
|
+
new Promise((r) => setTimeout(() => r(false), 5000)),
|
|
1215
|
+
]);
|
|
1216
|
+
|
|
1217
|
+
if (!exited) {
|
|
1218
|
+
killGroup('SIGKILL');
|
|
1219
|
+
await Promise.race([
|
|
1220
|
+
exitPromise.then(() => true),
|
|
1221
|
+
new Promise((r) => setTimeout(() => r(false), 3000)),
|
|
1222
|
+
]);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// 2. Take the jars the group signal cannot reach, by pid, sweep whatever
|
|
1227
|
+
// orphaned outside the record, and prove the ports came back
|
|
1228
|
+
// ([#304](https://github.com/Omega-JS-Stack/omega/issues/304)). The
|
|
1229
|
+
// shared hub/storage ports belong to this run only when nothing bumped.
|
|
1230
|
+
await this.terminateRecordedEmulatorProcesses(emulatorPorts, { sweepShared: bumped.length === 0 });
|
|
1231
|
+
|
|
1232
|
+
shutdownDone = true;
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
// Race the readiness marker against a deadline. A boot that never comes
|
|
1236
|
+
// up has still spawned: the shell, firebase, and any jar it got as far as.
|
|
1237
|
+
// Throwing straight out of here left them running and handed the caller no
|
|
1238
|
+
// handle to stop them, so a failed boot takes the same stop path a normal
|
|
1239
|
+
// one does before it propagates
|
|
1240
|
+
// ([#304](https://github.com/Omega-JS-Stack/omega/issues/304)). The
|
|
1241
|
+
// deadline is env-tunable and defaults high enough to absorb a slow port
|
|
1242
|
+
// sweep ([#332](https://github.com/Omega-JS-Stack/omega/issues/332)).
|
|
1243
|
+
const readyTimeoutMs = resolveReadyTimeout(process.env.OMEGA_EMULATOR_READY_TIMEOUT);
|
|
1244
|
+
try {
|
|
1245
|
+
await Promise.race([
|
|
1246
|
+
readyPromise,
|
|
1247
|
+
new Promise((_, reject) => setTimeout(
|
|
1248
|
+
() => reject(new Error(`Emulator did not print "All emulators ready" within ${readyTimeoutMs}ms`)),
|
|
1249
|
+
readyTimeoutMs,
|
|
1250
|
+
)),
|
|
1251
|
+
]);
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
await shutdown();
|
|
1254
|
+
throw error;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// The stack is UP and every java emulator exists — record the pids now, so
|
|
1258
|
+
// the post-shutdown sweep can prove which orphans are its own instead of
|
|
1259
|
+
// signaling whatever holds a port
|
|
1260
|
+
// ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)). Non-fatal:
|
|
1261
|
+
// without a record the sweep falls back to command-line proof and spares
|
|
1262
|
+
// anything it cannot place.
|
|
1263
|
+
try {
|
|
1264
|
+
this.writeEmulatorPidRecord(child.pid, this.loadProjectId(projectDir), emulatorPorts);
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
this.logWarning(`Could not record emulator pids (${error.message}) — the orphan sweep will only spare, never over-reach`);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
return { child, shutdown, emulatorPorts, bumped, exitPromise };
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Boot emulators and run a single command against them. Sends SIGTERM to the emulator
|
|
1274
|
+
* when the command exits (or this process is interrupted) and waits for clean shutdown.
|
|
1275
|
+
*
|
|
1276
|
+
* No current caller — `npx omega emulator`'s keep-alive flow boots through
|
|
1277
|
+
* execute() now; kept for programmatic embedding.
|
|
1278
|
+
* `npx omega test`'s auto-start path uses startEmulators() directly so it can tee the
|
|
1279
|
+
* test command's output to its own log (test.log) separate from emulator.log.
|
|
1280
|
+
*
|
|
1281
|
+
* @param {string} command - shell command to run while emulators are up
|
|
1282
|
+
*/
|
|
1283
|
+
async runWithEmulator(command) {
|
|
1284
|
+
// shutdown() carries the whole teardown (recorded jars, orphan sweep, port
|
|
1285
|
+
// verdict), so both paths below just join it.
|
|
1286
|
+
const { shutdown, exitPromise } = await this.startEmulators();
|
|
1287
|
+
|
|
1288
|
+
// Same synchronous handler and same three signals as execute() — see the
|
|
1289
|
+
// comment there.
|
|
1290
|
+
let stopCount = 0;
|
|
1291
|
+
const onStopSignal = () => {
|
|
1292
|
+
stopCount++;
|
|
1293
|
+
shutdown();
|
|
1294
|
+
};
|
|
1295
|
+
STOP_SIGNALS.forEach((signal) => process.on(signal, onStopSignal));
|
|
1296
|
+
|
|
1297
|
+
try {
|
|
1298
|
+
// Run the user command; when it exits we tear down the emulator.
|
|
1299
|
+
const cmdChild = spawnShell(command, {
|
|
1300
|
+
cwd: this.main.firebaseProjectPath,
|
|
1301
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
1302
|
+
stdio: 'inherit',
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
const cmdExit = await new Promise((resolve) => {
|
|
1306
|
+
cmdChild.on('close', (code, signal) => resolve({ code, signal }));
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
STOP_SIGNALS.forEach((signal) => process.removeListener(signal, onStopSignal));
|
|
1310
|
+
await shutdown();
|
|
1311
|
+
await exitPromise;
|
|
1312
|
+
|
|
1313
|
+
if (cmdExit.code !== 0) {
|
|
1314
|
+
throw Object.assign(new Error(`Command exited with code ${cmdExit.code}`), { code: cmdExit.code });
|
|
1315
|
+
}
|
|
1316
|
+
} catch (e) {
|
|
1317
|
+
STOP_SIGNALS.forEach((signal) => process.removeListener(signal, onStopSignal));
|
|
1318
|
+
await shutdown();
|
|
1319
|
+
throw e;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Sweep the ports this boot is about to bind and fail fast when one is held.
|
|
1325
|
+
*
|
|
1326
|
+
* The set is read from the firebase config, never hard-coded: the resolved
|
|
1327
|
+
* map covers everything the allocator owns, and the config's own `emulators`
|
|
1328
|
+
* block covers anything it does not (an eventarc or tasks port a brand
|
|
1329
|
+
* declared), which is the half that cannot bump. A lenient read matches
|
|
1330
|
+
* loadPortPins(): an unreadable config means no extra ports, never a boot
|
|
1331
|
+
* failure on its own.
|
|
1332
|
+
* @param {string} projectDir - The firebase project directory.
|
|
1333
|
+
* @param {object} emulatorPorts - This run's resolved port map.
|
|
1334
|
+
* @param {Function} [isFree] - Port probe (injectable).
|
|
1335
|
+
* @returns {Promise<Array<{name: string, port: number}>>} The plan that cleared.
|
|
1336
|
+
*/
|
|
1337
|
+
async preflightEmulatorPorts(projectDir, emulatorPorts, isFree = isPortFree) {
|
|
1338
|
+
let declared = {};
|
|
1339
|
+
|
|
1340
|
+
try {
|
|
1341
|
+
declared = declaredEmulatorPorts(JSON5.parse(jetpack.read(path.join(projectDir, 'firebase.json'))));
|
|
1342
|
+
} catch (error) { /* no readable config, so the resolved map is the whole plan */ }
|
|
1343
|
+
|
|
1344
|
+
const planned = plannedEmulatorPorts(declared, emulatorPorts);
|
|
1345
|
+
await assertPlannedPortsFree(planned, isFree);
|
|
1346
|
+
|
|
1347
|
+
return planned;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* Who is listening on a port right now: pid and command line, for a report.
|
|
1352
|
+
*
|
|
1353
|
+
* READ ONLY, deliberately: a port this run could not bind is held by someone
|
|
1354
|
+
* else's live process, and nothing in this file signals a process it cannot
|
|
1355
|
+
* prove is its own ([#274](https://github.com/Omega-JS-Stack/omega/issues/274),
|
|
1356
|
+
* [#778](https://github.com/Omega-JS-Stack/omega/issues/778)).
|
|
1357
|
+
* @param {number} port - The port to name a holder for.
|
|
1358
|
+
* @param {object} [seams]
|
|
1359
|
+
* @param {Function} [seams.listPids] - Port to pid lookup (injectable).
|
|
1360
|
+
* @param {Function} [seams.readCommand] - The `ps` read (injectable).
|
|
1361
|
+
* @returns {Array<{pid: number, command: string}>}
|
|
1362
|
+
*/
|
|
1363
|
+
describePortHolders(port, { listPids = listListeningPids, readCommand = readProcessCommand } = {}) {
|
|
1364
|
+
return listPids(port).map((pid) => ({ pid: Number(pid), command: readCommand(pid) }));
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Read explicit port pins from the brand config's `ports` section (N7).
|
|
1369
|
+
* Lenient — a missing/broken config means no pins, never a boot failure.
|
|
1370
|
+
*/
|
|
1371
|
+
loadPortPins(projectDir) {
|
|
1372
|
+
try {
|
|
1373
|
+
const { hasOmegaConfig, loadConfig } = require('../../vendor/config/index.js');
|
|
1374
|
+
const functionsDir = path.join(projectDir, 'dist');
|
|
1375
|
+
if (!hasOmegaConfig(functionsDir)) {
|
|
1376
|
+
return {};
|
|
1377
|
+
}
|
|
1378
|
+
const config = loadConfig(functionsDir, 'backend').config;
|
|
1379
|
+
return config.ports && typeof config.ports === 'object' ? config.ports : {};
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
return {};
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* The project this stack serves, read from the brand config. Lenient like
|
|
1387
|
+
* loadPortPins() — a missing/broken config means no project id, which the
|
|
1388
|
+
* ownership matcher treats as "no evidence", never a boot failure.
|
|
1389
|
+
*/
|
|
1390
|
+
loadProjectId(projectDir) {
|
|
1391
|
+
try {
|
|
1392
|
+
const { hasOmegaConfig, loadConfig } = require('../../vendor/config/index.js');
|
|
1393
|
+
const functionsDir = path.join(projectDir, 'dist');
|
|
1394
|
+
if (!hasOmegaConfig(functionsDir)) {
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
return loadConfig(functionsDir, 'backend').config.cloud?.config?.projectId || null;
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* The ownership evidence this run can offer the sweep: the pids it recorded
|
|
1405
|
+
* when its stack came up, and the project it belongs to. Recorded pids age
|
|
1406
|
+
* out — see ownershipFromRecord().
|
|
1407
|
+
* @returns {{pids: number[], projectId: string|null, rootPid: number|null}}
|
|
1408
|
+
*/
|
|
1409
|
+
readEmulatorOwnership() {
|
|
1410
|
+
return ownershipFromRecord(jetpack.read(this.getTempPath(PID_RECORD_FILE), 'json'));
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* Record the stack's pids while it is UP, so the post-shutdown sweep can
|
|
1415
|
+
* prove which orphans are its own
|
|
1416
|
+
* ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)).
|
|
1417
|
+
*
|
|
1418
|
+
* The emulator child's descendants cover firebase-tools' own tree — the java
|
|
1419
|
+
* emulators and every functions runtime worker it forks. The background
|
|
1420
|
+
* watcher is the one member that hangs off the CLI process instead, so it is
|
|
1421
|
+
* named separately ([#629](https://github.com/Omega-JS-Stack/omega/issues/629)).
|
|
1422
|
+
* The resolved port map rides along: it is what the NEXT boot's stale reap
|
|
1423
|
+
* waits on after it takes these pids down, since a pid it killed names no
|
|
1424
|
+
* port of its own ([#730](https://github.com/Omega-JS-Stack/omega/issues/730)).
|
|
1425
|
+
* @param {number} rootPid - The spawned child's pid.
|
|
1426
|
+
* @param {string|null} projectId - The project this stack serves.
|
|
1427
|
+
* @param {object} [ports] - This run's resolved port map.
|
|
1428
|
+
*/
|
|
1429
|
+
writeEmulatorPidRecord(rootPid, projectId, ports) {
|
|
1430
|
+
const existing = this.readEmulatorOwnership();
|
|
1431
|
+
|
|
1432
|
+
// UNION with what this run already recorded: a later snapshot can only see
|
|
1433
|
+
// what is still attached, and a child that orphaned in between is exactly
|
|
1434
|
+
// the one the sweep exists for. Pids of processes that have since exited
|
|
1435
|
+
// cost nothing — signaling one is a caught ESRCH.
|
|
1436
|
+
const previous = existing.pids.length > 0 && existing.rootPid === rootPid ? existing.pids : [];
|
|
1437
|
+
// NAME DEPENDENCY: if this record misses the watcher, the command-line fallback
|
|
1438
|
+
// (EMULATOR_COMMAND, /emulator|firebase/i) only catches it because watch.js bakes
|
|
1439
|
+
// the `emulator.log.reset` sentinel PATH into nodemon's --exec argv. Rename that
|
|
1440
|
+
// sentinel and the watcher stops matching — spared by every stop path, immortal.
|
|
1441
|
+
const watcher = this.backgroundWatcherPid ? collectDescendantPids(this.backgroundWatcherPid) : [];
|
|
1442
|
+
const pids = [...new Set([...previous, ...collectDescendantPids(rootPid), ...watcher])];
|
|
1443
|
+
|
|
1444
|
+
jetpack.write(this.getTempPath(PID_RECORD_FILE), {
|
|
1445
|
+
pids: pids,
|
|
1446
|
+
projectId: projectId || null,
|
|
1447
|
+
rootPid: rootPid,
|
|
1448
|
+
ports: ports || {},
|
|
1449
|
+
startedAt: new Date().toISOString(),
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
return pids;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Terminate the stack this run RECORDED, then prove its ports came back.
|
|
1457
|
+
*
|
|
1458
|
+
* The group signal in shutdown() reaches `sh`/firebase and its direct
|
|
1459
|
+
* children only: firebase-tools starts each java emulator in its OWN process
|
|
1460
|
+
* group, so the jars never see it, and they reparent to PID 1 the moment
|
|
1461
|
+
* their parent goes. A stop that escalates to SIGKILL (a firestore emulator
|
|
1462
|
+
* that does not finish its graceful stop inside the grace window) therefore
|
|
1463
|
+
* leaves the jars running, and the next brand's boot bumps around a squatter
|
|
1464
|
+
* that answers on the classic ports forever
|
|
1465
|
+
* ([#304](https://github.com/Omega-JS-Stack/omega/issues/304)). The pid
|
|
1466
|
+
* record written while the tree was still attached is the only surviving
|
|
1467
|
+
* link to them, so the stop path signals what it names, BY PID.
|
|
1468
|
+
*
|
|
1469
|
+
* Ownership stays proven per process — a recorded number whose live command
|
|
1470
|
+
* line is not emulator machinery is a recycled pid, never our jar — and the
|
|
1471
|
+
* port check only REPORTS: nothing is signaled for occupying a port
|
|
1472
|
+
* ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)).
|
|
1473
|
+
*
|
|
1474
|
+
* The owned-orphan sweep runs BEFORE that check, because it can still free a
|
|
1475
|
+
* port: a jar the record never named (spawned after the last snapshot, then
|
|
1476
|
+
* reparented to PID 1) is the sweep's case, not the record's. Verifying
|
|
1477
|
+
* first spent the whole release window on a port the next step was about to
|
|
1478
|
+
* clear, then reported it as somebody else's.
|
|
1479
|
+
*
|
|
1480
|
+
* @param {object} emulatorPorts - This run's resolved port map.
|
|
1481
|
+
* @param {object} [options]
|
|
1482
|
+
* @param {boolean} [options.sweepShared] - Look at the shared hub/storage
|
|
1483
|
+
* ports too (a defaults run owns them; a bumped run does not).
|
|
1484
|
+
*/
|
|
1485
|
+
async terminateRecordedEmulatorProcesses(emulatorPorts, { sweepShared = true } = {}) {
|
|
1486
|
+
const ownership = this.readEmulatorOwnership();
|
|
1487
|
+
const targets = [];
|
|
1488
|
+
|
|
1489
|
+
for (const recorded of ownership.pids) {
|
|
1490
|
+
const pid = Number(recorded);
|
|
1491
|
+
const command = readProcessCommand(pid);
|
|
1492
|
+
|
|
1493
|
+
// Gone already (the group signal took it) or not ours anymore.
|
|
1494
|
+
if (!command || !isStoppableEmulatorProcess({ pid: pid, command: command }, ownership)) {
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
targets.push(pid);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
for (const pid of targets) {
|
|
1502
|
+
try { process.kill(pid, 'SIGTERM'); } catch (e) { /* exited between the read and the signal */ }
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Give them the grace window, then take whatever is still standing.
|
|
1506
|
+
const survivors = await this.waitForProcessesToExit(targets, STOP_GRACE_MS);
|
|
1507
|
+
|
|
1508
|
+
for (const pid of survivors) {
|
|
1509
|
+
try { process.kill(pid, 'SIGKILL'); } catch (e) { /* gone in the meantime */ }
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
if (targets.length > 0) {
|
|
1513
|
+
this.log(chalk.gray(` Stopped ${targets.length} recorded emulator process${targets.length > 1 ? 'es' : ''}${survivors.length > 0 ? ` (${survivors.length} needed SIGKILL)` : ''}.`));
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// Anything of ours the record could not name is the sweep's to take, and
|
|
1517
|
+
// it runs while the ports still matter — before the verdict below.
|
|
1518
|
+
await this.terminateOrphanedEmulatorProcesses(emulatorPorts, { sweepShared: sweepShared });
|
|
1519
|
+
|
|
1520
|
+
// The ports are the only proof that matters to the NEXT boot.
|
|
1521
|
+
const { https: _httpsPort, ...checkable } = emulatorPorts || {};
|
|
1522
|
+
const held = await this.waitForPortsReleased(Object.values(checkable));
|
|
1523
|
+
|
|
1524
|
+
if (held.length > 0) {
|
|
1525
|
+
this.logWarning(`Emulator ports still in use after shutdown: ${held.join(', ')} (everything this run could prove it owns is stopped; whatever holds them did not prove to be ours)`);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Poll a pid set until every one is gone or the window closes.
|
|
1531
|
+
* @param {number[]} pids - The pids to watch.
|
|
1532
|
+
* @param {number} timeoutMs - How long to wait in total.
|
|
1533
|
+
* @param {object} [seams]
|
|
1534
|
+
* @param {Function} [seams.readCommand] - The `ps` read (injectable).
|
|
1535
|
+
* @returns {Promise<number[]>} The pids still alive when the window closed.
|
|
1536
|
+
*/
|
|
1537
|
+
async waitForProcessesToExit(pids, timeoutMs, { readCommand = readProcessCommand } = {}) {
|
|
1538
|
+
const deadline = Date.now() + timeoutMs;
|
|
1539
|
+
let alive = pids;
|
|
1540
|
+
|
|
1541
|
+
while (alive.length > 0 && Date.now() < deadline) {
|
|
1542
|
+
await powertools.wait(POLL_INTERVAL_MS);
|
|
1543
|
+
alive = alive.filter((pid) => !!readCommand(pid));
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
return alive;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Poll a port set until nothing is listening or the window closes.
|
|
1551
|
+
* @param {number[]} ports - The ports this run resolved.
|
|
1552
|
+
* @returns {Promise<number[]>} The ports still held when the window closed.
|
|
1553
|
+
*/
|
|
1554
|
+
async waitForPortsReleased(ports, { isFree = isPortFree } = {}) {
|
|
1555
|
+
const deadline = Date.now() + PORT_RELEASE_TIMEOUT_MS;
|
|
1556
|
+
let held = [...new Set(ports)];
|
|
1557
|
+
|
|
1558
|
+
while (held.length > 0 && Date.now() < deadline) {
|
|
1559
|
+
const free = await Promise.all(held.map((port) => isFree(port)));
|
|
1560
|
+
held = held.filter((port, index) => !free[index]);
|
|
1561
|
+
|
|
1562
|
+
if (held.length > 0) {
|
|
1563
|
+
await powertools.wait(POLL_INTERVAL_MS);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
return held;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* Terminate processes still listening on THIS RUN's emulator ports after
|
|
1572
|
+
* shutdown. Firebase-tools spawns Java emulators (Firestore, Database,
|
|
1573
|
+
* PubSub) that often survive SIGTERM/SIGKILL of the firebase node process.
|
|
1574
|
+
* This sweep runs AFTER the main child exits, so anything still on these
|
|
1575
|
+
* ports is orphaned.
|
|
1576
|
+
*
|
|
1577
|
+
* Ownership is PROVEN per process, never inferred from the port: the sweep
|
|
1578
|
+
* used to signal whatever was listening and took down another project's live
|
|
1579
|
+
* emulator on the shared hub/storage ports
|
|
1580
|
+
* ([#274](https://github.com/Omega-JS-Stack/omega/issues/274)). A port only
|
|
1581
|
+
* decides where to LOOK; isOwnedEmulatorProcess() decides what to signal.
|
|
1582
|
+
*
|
|
1583
|
+
* N7: the sweep takes the RESOLVED port map — no map → no sweep. The shared
|
|
1584
|
+
* hub (4400) + storage (9199) ports are looked at only on a defaults run
|
|
1585
|
+
* (`sweepShared`); on a bumped run they belong to the incumbent.
|
|
1586
|
+
* @param {object} emulatorPorts - This run's resolved port map.
|
|
1587
|
+
* @param {object} [options]
|
|
1588
|
+
* @param {boolean} [options.sweepShared] - Include the shared hub/storage ports.
|
|
1589
|
+
* @param {Function} [options.isFree] - Port probe (injectable).
|
|
1590
|
+
* @param {Function} [options.listPids] - Port to pid lookup (injectable).
|
|
1591
|
+
*/
|
|
1592
|
+
async terminateOrphanedEmulatorProcesses(emulatorPorts, { sweepShared = true, isFree = isPortFree, listPids = listListeningPids } = {}) {
|
|
1593
|
+
if (!emulatorPorts) {
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// `https` is OUR in-process TLS proxy (closed with the child), never an
|
|
1598
|
+
// orphaned java emulator — sweeping it would signal this very process
|
|
1599
|
+
// on a close-timing race.
|
|
1600
|
+
const { https: _httpsPort, ...sweepable } = emulatorPorts;
|
|
1601
|
+
const ports = Object.values(sweepable);
|
|
1602
|
+
if (sweepShared) {
|
|
1603
|
+
ports.push(4400, 9199);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const ownership = this.readEmulatorOwnership();
|
|
1607
|
+
|
|
1608
|
+
// The probe is the only await, and it runs before a single signal is sent.
|
|
1609
|
+
// The kill loop itself stays synchronous, so Ctrl+C spam cannot land
|
|
1610
|
+
// between two signals of the same sweep.
|
|
1611
|
+
const { execSync } = require('child_process');
|
|
1612
|
+
let terminated = 0;
|
|
1613
|
+
let spared = 0;
|
|
1614
|
+
|
|
1615
|
+
for (const port of await heldPorts(ports, isFree)) {
|
|
1616
|
+
for (const pid of listPids(port)) {
|
|
1617
|
+
try {
|
|
1618
|
+
const command = execSync(`ps -o command= -p ${pid} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
1619
|
+
|
|
1620
|
+
if (!isOwnedEmulatorProcess({ pid: pid, command: command }, ownership)) {
|
|
1621
|
+
spared++;
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
process.kill(Number(pid), 'SIGKILL');
|
|
1626
|
+
terminated++;
|
|
1627
|
+
} catch (e) { /* vanished mid-check, or already gone */ }
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
if (terminated > 0) {
|
|
1632
|
+
this.log(chalk.gray(` Cleaned up ${terminated} orphaned emulator process${terminated > 1 ? 'es' : ''}.`));
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
if (spared > 0) {
|
|
1636
|
+
this.log(chalk.gray(` Left ${spared} process${spared > 1 ? 'es' : ''} on these ports alone — not this project's emulator.`));
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Pre-boot reap of every ORPHANED emulator-family process on the MACHINE
|
|
1642
|
+
* ([#781](https://github.com/Omega-JS-Stack/omega/issues/781)).
|
|
1643
|
+
*
|
|
1644
|
+
* The verdict, the escalation and the settle wait all live in
|
|
1645
|
+
* cli/commands/emulator-orphans.js, which reads the world through seams; this
|
|
1646
|
+
* is the wiring: the boot's own settle windows and the boot's voice.
|
|
1647
|
+
* @param {object} [seams] - The sweep's seams (tests state a process table here).
|
|
1648
|
+
*/
|
|
1649
|
+
async reapMachineOrphans(seams) {
|
|
1650
|
+
await emulatorOrphans.reapMachineOrphans({
|
|
1651
|
+
graceMs: STOP_GRACE_MS,
|
|
1652
|
+
portReleaseTimeoutMs: PORT_RELEASE_TIMEOUT_MS,
|
|
1653
|
+
pollIntervalMs: POLL_INTERVAL_MS,
|
|
1654
|
+
log: (message) => this.log(chalk.gray(` ${message}`)),
|
|
1655
|
+
...seams,
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/**
|
|
1660
|
+
* Pre-boot reap of a STALE pid record: the leftovers of a previous run that
|
|
1661
|
+
* died without teardown, taken BY PID, before this boot overwrites the file
|
|
1662
|
+
* that names them ([#721](https://github.com/Omega-JS-Stack/omega/issues/721)).
|
|
1663
|
+
*
|
|
1664
|
+
* The sibling sweep above only ever considers an ORPHAN, so it cannot reach a
|
|
1665
|
+
* dead run's member that still hangs off another member of that same dead run
|
|
1666
|
+
* — the reload watcher's usual shape while its root is unwinding — and
|
|
1667
|
+
* writeEmulatorPidRecord drops the previous record the moment a new root pid
|
|
1668
|
+
* appears. Between the two, a dead boot's nodemon survived every stop path
|
|
1669
|
+
* this framework has, forever.
|
|
1670
|
+
*
|
|
1671
|
+
* STALE is the first proof: the record's ROOT pid is gone, so the run that
|
|
1672
|
+
* wrote it is not around to own its own processes. A live root means a LIVE
|
|
1673
|
+
* record — this boot is a second stack beside a running one — and nothing in
|
|
1674
|
+
* it is anyone's to reap. A record that names no usable root at all proves
|
|
1675
|
+
* neither, and an unprovable root is not a kill order.
|
|
1676
|
+
*
|
|
1677
|
+
* IDENTITY is the second, and it is the whole safety story. A record is a
|
|
1678
|
+
* list of NUMBERS, the OS recycles numbers, and the run that could vouch for
|
|
1679
|
+
* them is by definition gone. So nothing is signalled on the number: the LIVE
|
|
1680
|
+
* row must still read as emulator machinery of this project AND hang off the
|
|
1681
|
+
* dead run itself (isReapableRecordedProcess). A pid whose row cannot be read
|
|
1682
|
+
* has no identity to prove and is never signalled. The escalation re-proves
|
|
1683
|
+
* it, because the grace window is long enough for a number to be freed and
|
|
1684
|
+
* handed out again — and there is no apologising to a SIGKILL.
|
|
1685
|
+
*
|
|
1686
|
+
* @param {{pids: number[], projectId: string|null, rootPid: number|null, ports: object}} ownership - The PREVIOUS record.
|
|
1687
|
+
* @param {object} [seams]
|
|
1688
|
+
* @param {Function} [seams.readProcess] - The `ps -o ppid=,command=` read (injectable).
|
|
1689
|
+
* @param {Function} [seams.kill] - The signal (injectable).
|
|
1690
|
+
* @param {Function} [seams.isFree] - Port probe for the settle wait (injectable).
|
|
1691
|
+
*/
|
|
1692
|
+
async reapStaleRecordedProcesses(ownership, { readProcess = readProcessInfo, kill = process.kill, isFree = isPortFree } = {}) {
|
|
1693
|
+
const rootPid = Number(ownership?.rootPid);
|
|
1694
|
+
|
|
1695
|
+
// Staleness is proved BY the root, so a record that names no usable one
|
|
1696
|
+
// proves nothing — and an unprovable root is not a kill order. Read the
|
|
1697
|
+
// other way ("no root answering"), a record written without a rootPid over
|
|
1698
|
+
// a LIVE stack of this same project reads as pure leftovers and the reap
|
|
1699
|
+
// takes the running firebase parent with it.
|
|
1700
|
+
if (!Number.isInteger(rootPid) || rootPid <= 1) {
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// A root still answering is a run still up. A root whose number was
|
|
1705
|
+
// recycled onto a stranger reads the same and is also left alone: erring
|
|
1706
|
+
// toward "somebody is using this" is the only safe direction here.
|
|
1707
|
+
if (readProcess(rootPid)) {
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
const targets = [];
|
|
1712
|
+
let spared = 0;
|
|
1713
|
+
|
|
1714
|
+
for (const recorded of ownership?.pids || []) {
|
|
1715
|
+
const pid = Number(recorded);
|
|
1716
|
+
|
|
1717
|
+
// Never this run or its parent. `omega emulator` matches the machinery
|
|
1718
|
+
// signature by NAME, so a recorded number recycled onto this very process
|
|
1719
|
+
// would otherwise make the boot signal itself.
|
|
1720
|
+
if (pid === process.pid || pid === process.ppid) {
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
const live = readProcess(pid);
|
|
1725
|
+
|
|
1726
|
+
// Gone (nothing to do) or unreadable (no proof, so no kill order).
|
|
1727
|
+
if (!live) {
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
if (!isReapableRecordedProcess({ pid: pid, ppid: live.ppid, command: live.command }, ownership)) {
|
|
1732
|
+
spared++;
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
targets.push(pid);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
for (const pid of targets) {
|
|
1740
|
+
try { kill(pid, 'SIGTERM'); } catch (e) { /* exited between the read and the signal */ }
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// The exit watch reads through the same `ps` seam the identity proof does.
|
|
1744
|
+
const readCommand = (pid) => {
|
|
1745
|
+
const live = readProcess(pid);
|
|
1746
|
+
return live ? live.command : '';
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
const survivors = await this.waitForProcessesToExit(targets, STOP_GRACE_MS, { readCommand: readCommand });
|
|
1750
|
+
const killed = [];
|
|
1751
|
+
|
|
1752
|
+
for (const pid of survivors) {
|
|
1753
|
+
const live = readProcess(pid);
|
|
1754
|
+
|
|
1755
|
+
if (!live || !isReapableRecordedProcess({ pid: pid, ppid: live.ppid, command: live.command }, ownership)) {
|
|
1756
|
+
spared++;
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
try {
|
|
1761
|
+
kill(pid, 'SIGKILL');
|
|
1762
|
+
killed.push(pid);
|
|
1763
|
+
} catch (e) { /* gone in the meantime */ }
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// Only what actually went down: a target that outlived the grace window and
|
|
1767
|
+
// came back as somebody else's number is spared, so counting the kill
|
|
1768
|
+
// ORDERS would report it reaped and left alone in the same breath.
|
|
1769
|
+
const downed = targets.length - survivors.length + killed.length;
|
|
1770
|
+
|
|
1771
|
+
if (downed > 0) {
|
|
1772
|
+
this.log(chalk.gray(` Reaped ${downed} recorded process${downed > 1 ? 'es' : ''} left by a previous run${killed.length > 0 ? ` (${killed.length} needed SIGKILL)` : ''}.`));
|
|
1773
|
+
|
|
1774
|
+
// A signal is not an exit: kill() returns while the kernel is still
|
|
1775
|
+
// unwinding the process, so a probe in the same tick asks about a
|
|
1776
|
+
// shutdown that has not happened yet, and a jar mid-unwind can answer
|
|
1777
|
+
// FREE on a port it is about to keep holding, which is exactly the
|
|
1778
|
+
// "probed free, then failed to bind" the retry above exists to catch.
|
|
1779
|
+
// The numbers go first, THEN the ports are asked
|
|
1780
|
+
// ([#778](https://github.com/Omega-JS-Stack/omega/issues/778)).
|
|
1781
|
+
const lingering = await this.waitForProcessesToExit(killed, STOP_GRACE_MS, { readCommand: readCommand });
|
|
1782
|
+
|
|
1783
|
+
if (lingering.length > 0) {
|
|
1784
|
+
this.logWarning(`Pid${lingering.length > 1 ? 's' : ''} ${lingering.join(', ')} outlived SIGKILL; the port check below is the last word on ${lingering.length > 1 ? 'them' : 'it'}`);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// Same race the port-driven reaper answers above: a SIGKILLed JVM holds
|
|
1788
|
+
// its listener for a moment after the process is gone, and the allocator
|
|
1789
|
+
// probes these very ports a few lines later. A reap here is BY PID, so
|
|
1790
|
+
// nothing says which port each one held — the record's own map is the
|
|
1791
|
+
// set, and the worst case is the release window spent on a port a live
|
|
1792
|
+
// neighbour took over, which the log names before the allocator bumps
|
|
1793
|
+
// around it. `https` is not in it: that port belonged to the dead run's
|
|
1794
|
+
// in-process TLS proxy, never to a jar this reap could take.
|
|
1795
|
+
const { https: _httpsPort, ...settleable } = ownership?.ports || {};
|
|
1796
|
+
|
|
1797
|
+
if (Object.keys(settleable).length === 0) {
|
|
1798
|
+
this.log(chalk.gray(' The record named no ports, so nothing was waited on; the allocation probe is the only check on what this reap just freed.'));
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
const held = await this.waitForPortsReleased(Object.values(settleable), { isFree });
|
|
1802
|
+
|
|
1803
|
+
if (held.length > 0) {
|
|
1804
|
+
this.log(chalk.gray(` Port${held.length > 1 ? 's' : ''} ${held.join(', ')} still closing after the reap; the allocator will bump around ${held.length > 1 ? 'them' : 'it'}.`));
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
if (spared > 0) {
|
|
1809
|
+
this.log(chalk.gray(` Left ${spared} recorded pid${spared > 1 ? 's' : ''} alone — the number is no longer this project's emulator.`));
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// Static, alongside Middleware's precedent — the ownership decision is pure,
|
|
1815
|
+
// so tests exercise it directly with real `ps` rows instead of live processes.
|
|
1816
|
+
EmulatorCommand.resolveReadyTimeout = resolveReadyTimeout;
|
|
1817
|
+
EmulatorCommand.heldPorts = heldPorts;
|
|
1818
|
+
EmulatorCommand.listListeningPids = listListeningPids;
|
|
1819
|
+
EmulatorCommand.PORT_LOOKUP_TIMEOUT_MS = PORT_LOOKUP_TIMEOUT_MS;
|
|
1820
|
+
EmulatorCommand.plannedEmulatorPorts = plannedEmulatorPorts;
|
|
1821
|
+
EmulatorCommand.assertPlannedPortsFree = assertPlannedPortsFree;
|
|
1822
|
+
EmulatorCommand.isOwnedEmulatorProcess = isOwnedEmulatorProcess;
|
|
1823
|
+
EmulatorCommand.isStoppableEmulatorProcess = isStoppableEmulatorProcess;
|
|
1824
|
+
EmulatorCommand.isReapableRecordedProcess = isReapableRecordedProcess;
|
|
1825
|
+
EmulatorCommand.readProcessInfo = readProcessInfo;
|
|
1826
|
+
EmulatorCommand.ownershipFromRecord = ownershipFromRecord;
|
|
1827
|
+
EmulatorCommand.collectDescendantPids = collectDescendantPids;
|
|
1828
|
+
|
|
1829
|
+
module.exports = EmulatorCommand;
|