gina 0.6.2 → 0.6.3
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/CHANGELOG.md +57 -0
- package/README.md +41 -12
- package/ROADMAP.md +3 -2
- package/bin/cli +19 -2
- package/framework/v0.6.3/VERSION +1 -0
- package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/html/statusbar.html +6 -0
- package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/html/statusbar.html.br +0 -0
- package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/html/statusbar.html.gz +0 -0
- package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/inspector/index.html +1 -0
- package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/inspector/inspector.css +50 -1
- package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/inspector/inspector.js +457 -57
- package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/js/gina.js +1457 -58
- package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/js/gina.min.js +625 -0
- package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
- package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/index.js +114 -0
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/connector.js +14 -3
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/session-store.js +14 -3
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/session-store.v3.js +29 -2
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/session-store.v4.js +29 -2
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/mongodb/lib/session-store.js +24 -2
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/redis/lib/session-store.js +33 -9
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/scylladb/lib/session-store.js +25 -2
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/sqlite/lib/session-store.js +26 -2
- package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.js +201 -21
- package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-nunjucks.js +8 -0
- package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-swig.js +85 -4
- package/framework/{v0.6.2 → v0.6.3}/core/gna.js +23 -1
- package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/csrf/src/main.js +9 -5
- package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/hide-powered-by/README.md +20 -0
- package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/validator/src/form-validator.js +119 -43
- package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/validator/src/main.js +451 -9
- package/framework/{v0.6.2 → v0.6.3}/core/server.isaac.js +50 -54
- package/framework/{v0.6.2 → v0.6.3}/core/server.js +240 -3
- package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/index.js +6 -4
- package/framework/{v0.6.2 → v0.6.3}/core/template/conf/settings.json +10 -0
- package/framework/{v0.6.2 → v0.6.3}/lib/cache/README.md +9 -1
- package/framework/{v0.6.2 → v0.6.3}/lib/cmd/audit/verify.js +11 -9
- package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/mcp-start.js +21 -12
- package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/help.txt +1 -1
- package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/init.js +13 -1
- package/framework/{v0.6.2 → v0.6.3}/lib/dto/src/main.js +18 -11
- package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/file/index.js +29 -1
- package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/mq/speaker.js +30 -1
- package/framework/{v0.6.2 → v0.6.3}/lib/math/index.js +57 -6
- package/framework/{v0.6.2 → v0.6.3}/lib/merge/src/main.js +34 -5
- package/framework/{v0.6.2 → v0.6.3}/lib/render-cache/src/main.js +77 -4
- package/framework/{v0.6.2 → v0.6.3}/lib/routing/src/main.js +13 -1
- package/framework/{v0.6.2 → v0.6.3}/lib/routing-introspect/src/main.js +64 -2
- package/framework/v0.6.3/lib/secrets/src/backends/env.js +75 -0
- package/framework/{v0.6.2 → v0.6.3}/package.json +1 -1
- package/gna.js +5 -4
- package/llms.txt +19 -17
- package/package.json +5 -4
- package/schema/connectors.json +2 -2
- package/schema/routing.json +5 -0
- package/schema/settings.json +6 -1
- package/script/generate_gna_types.js +1 -1
- package/script/soak/couchbase-soak.js +1316 -0
- package/types/globals.d.ts +2 -0
- package/types/gna.d.ts +5 -0
- package/types/index.d.ts +1 -0
- package/utils/helper.js +38 -7
- package/framework/v0.6.2/VERSION +0 -1
- package/framework/v0.6.2/core/asset/plugin/dist/vendor/gina/html/statusbar.html.br +0 -0
- package/framework/v0.6.2/core/asset/plugin/dist/vendor/gina/html/statusbar.html.gz +0 -0
- package/framework/v0.6.2/core/asset/plugin/dist/vendor/gina/js/gina.min.js +0 -603
- package/framework/v0.6.2/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
- package/framework/v0.6.2/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
- package/framework/v0.6.2/lib/secrets/src/backends/env.js +0 -60
- /package/framework/{v0.6.2 → v0.6.3}/AUTHORS +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/LICENSE +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/html/nolayout.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/html/static.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/img/android-chrome-192x192.png +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/img/android-chrome-512x512.png +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/img/apple-touch-icon.png +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/img/favicon-16x16.png +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/img/favicon-32x32.png +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/img/favicon.ico +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.css +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/beemaster/index.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/css/gina.min.css +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/css/gina.min.css.br +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/css/gina.min.css.gz +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/inspector/have_heart_one-webfont.woff2 +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/inspector/logo.svg +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.br +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.gz +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/config.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/ai/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/ai/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/connector.v3.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/connector.v4.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/n1ql.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/duckdb/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/duckdb/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/mongodb/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/mongodb/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/mongodb/lib/job-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/mongodb/lib/pipeline-loader.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/mysql/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/mysql/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/postgresql/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/postgresql/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/redis/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/redis/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/redis/lib/job-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/redis/lib/render-cache-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/scylladb/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/scylladb/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/sql-parser.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/sqlite/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/sqlite/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/sqlite/lib/job-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/content.encoding +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.framework.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-json.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-nunjucks-async.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-stream.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-swig-async.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-v1.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/inspector-window-emit.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/release-banner.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/dev/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/dev/lib/class.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/dev/lib/factory.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/dev/lib/tools.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/currency.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/dist/language/en.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/dist/language/fr.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/dist/region/en.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/dist/region/fr.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/mime.types +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/model/entity.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/model/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/model/template/entityFactory.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/model/template/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/csrf/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/csrf/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/coep/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/coep/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/coep/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/coop/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/coop/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/coop/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/corp/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/corp/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/corp/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/csp/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/csp/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/csp/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/hide-powered-by/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/hide-powered-by/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/hsts/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/hsts/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/hsts/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/origin-agent-cluster/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/origin-agent-cluster/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/origin-agent-cluster/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/referrer-policy/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/referrer-policy/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/referrer-policy/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-content-type-options/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-content-type-options/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-content-type-options/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-dns-prefetch-control/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-dns-prefetch-control/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-dns-prefetch-control/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-download-options/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-download-options/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-download-options/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-frame-options/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-frame-options/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-frame-options/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-xss-protection/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-xss-protection/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-xss-protection/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/session/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/session/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/session/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/storage/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/storage/build.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/storage/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/storage/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/validator/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/validator/build.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/validator/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/router.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/server.express.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/status.codes +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/_gitignore +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/app.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/connectors.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/routing.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/settings.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/settings.server.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/templates.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/watchers.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/controllers/controller.content.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/controllers/controller.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/controllers/setup.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/locales/en.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_namespace/controllers/controller.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/css/default.css +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/css/home.css +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/css/vendor/readme.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/favicon.ico +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/js/components/x-checklist.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/js/vendor/readme.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/manifest.webmanifest +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/readme.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/sw.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_templates/handlers/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_templates/html/content/homepage.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_templates/html/includes/error-msg-noscript.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_templates/html/includes/error-msg-outdated-browser.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_templates/html/includes/x-checklist.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_templates/html/layouts/main.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/command/gina.bat.tpl +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/command/gina.tpl +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/conf/env.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/conf/manifest.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/conf/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/conf/statics.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/conf/templates.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/error/client/json/401.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/error/client/json/403.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/error/client/json/404.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/error/server/html/50x.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/error/server/json/500.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/error/server/json/503.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/extensions/logger/config.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/console.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/context.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/data/LICENSE +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/data/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/data/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/data/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/dateFormat.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/json/LICENSE +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/json/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/json/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/json/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/path.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/plugins/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/plugins/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/plugins/src/api-error.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/plugins/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/prototypes.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/task.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/text.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/admin/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/admin/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/archiver/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/archiver/build.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/archiver/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/archiver/src/dep/jszip.min.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/archiver/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/async/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/async/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/audit/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/audit/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/audit-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/authn/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/authn/src/lockout.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/authn/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/authn/src/totp.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/authz-gate/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/authz-gate/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cache/build.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cache/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cache/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/aliases.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/audit/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/audit/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/build.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/copy.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/cp.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/inc/name-rewrite.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/man.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/mcp.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/oas.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/openapi.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/rename.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/restart.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/start.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/status.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/stop.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/types.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/cache/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/cache/clear.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/cache/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/cache/stats.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/infer.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/migrate.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/models.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/test.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/container/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/container/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/container/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/container/man.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/container/ps.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/container/stop.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/inc/args.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/inc/namespace.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/inc/reference-rewrite.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/inc/reference-scan.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/inc/scaffold.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/rename.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/get.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/link-dev.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/set.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/unset.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/use.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/build.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/dot.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/get.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/link-node-modules.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/link.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/man.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/msg.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/open.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/reset.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/restart.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/set.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/start.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/status.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/stop.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/tail.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/update.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/version.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/gina-dev.1.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/gina-framework.1.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/gina.1.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/helper.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/export.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/import.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/scan.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/_host.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/build.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/man.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/run.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/inspector/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/inspector/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/inspector/open.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/man-render.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/minion/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/minion/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/minion/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/minion/kill.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/minion/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/msg.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/port/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/port/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/port/inc/scan.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/port/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/port/reset.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/port/set.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/backup.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/build.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/import.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/man.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/move.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/rename.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/restart.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/restore.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/start.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/status.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/stop.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/protocol/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/protocol/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/protocol/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/protocol/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/protocol/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/protocol/set.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/link-local.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/link-production.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/use.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/secrets/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/secrets/check.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/secrets/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/secrets/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/secrets/scan.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/service/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/service/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/service/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/service/man.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/service/start.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/view/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd-status-format/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd-status-format/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/collection/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/collection/build.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/collection/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/collection/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/config.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/connector-config/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/connector-config/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/connector-error/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/connector-error/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/connector-registry/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/connector-registry/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cron/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cron/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cron/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/domain/LICENSE +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/domain/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/domain/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/domain/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/dto/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/dto-pipe/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/dto-pipe/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/dto-types/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/dto-types/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/generator/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/i18n/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/i18n/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/image-build/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/image-build/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inherits/LICENSE +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inherits/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inherits/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inherits/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inspector-events/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inspector-events/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inspector-redact/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inspector-redact/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/instrument/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/instrument/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/job/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/job/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/job-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/json-config-header/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/json-config-header/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/default/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/file/lib/logrotator/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/file/lib/logrotator/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/mq/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/mq/listener.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/helper.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/mcp-dispatch/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/mcp-dispatch/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/mcp-http/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/mcp-http/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/mcp-server/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/mcp-server/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/merge/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/merge/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/metrics/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/metrics/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/model.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/net-locality/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/net-locality/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/nunjucks-filters/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/nunjucks-filters/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/nunjucks-filters/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/nunjucks-resolver/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/nunjucks-resolver/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/proc.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/release-watch/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/release-watch/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/render-cache/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/render-cache-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/routing/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/routing/build.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/routing/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/routing/src/radix.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/routing-introspect/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/secrets/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/secrets/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/session-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/shell.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/sqlite-driver.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/state.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/swig-filters/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/swig-filters/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/swig-filters/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/swig-resolver/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/swig-resolver/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/template-loaders/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/template-loaders/src/loaders/http.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/template-loaders/src/loaders/memory.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/template-loaders/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/url/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/url/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/url/routing.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/uuid/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/uuid/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/validator.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/watcher/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/watcher/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/ws-framing/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/ws-framing/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/ws-query/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/ws-query/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/ws-session/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/ws-session/src/main.js +0 -0
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* couchbase-soak.js — #CN12: connector-level SDK × Server soak harness
|
|
4
|
+
* (couchbase first).
|
|
5
|
+
*
|
|
6
|
+
* Screens a Couchbase Node SDK candidate (e.g. an SDK bump under evaluation)
|
|
7
|
+
* against a caller-chosen Couchbase Server BEFORE a consuming project adopts
|
|
8
|
+
* it: the harness scaffolds a throwaway gina project in FULL ISOLATION
|
|
9
|
+
* (self-bootstrapping gina home under os.tmpdir(), HOME override — the real
|
|
10
|
+
* ~/.gina and real projects are never touched), installs the CANDIDATE SDK
|
|
11
|
+
* into that project (the connector resolves `couchbase` from the project's
|
|
12
|
+
* own node_modules, so the install IS the version selector), builds and
|
|
13
|
+
* boots the bundle for the prod env, then drives the connector's three
|
|
14
|
+
* surfaces under sustained concurrent load for a fixed duration:
|
|
15
|
+
*
|
|
16
|
+
* query — N1QL entity methods through the connector's real dispatch
|
|
17
|
+
* (`adhoc: false`, positional params, `$scope` substitution),
|
|
18
|
+
* including a `request_plus` scan-consistency arm;
|
|
19
|
+
* kv — KV through the entity `getConnection()` collection handle, in
|
|
20
|
+
* BOTH forms consumers use: the 4-arg optional-callback form
|
|
21
|
+
* (`coll.insert(key, doc, opts, cb)`) and the promise form
|
|
22
|
+
* (insert → get → periodic remove cycle, expiry-bounded);
|
|
23
|
+
* session — the couchbase express-session store (set/get/touch/destroy
|
|
24
|
+
* churn through real HTTP requests with rotating cookie jars).
|
|
25
|
+
*
|
|
26
|
+
* PASS CRITERIA (the whole point — aimed at the silent-death class where a
|
|
27
|
+
* process under sustained SDK load exits cleanly with no JS stack):
|
|
28
|
+
* 1. the bundle process SURVIVES the full duration — ANY premature exit,
|
|
29
|
+
* explicitly INCLUDING a clean `exit 0`, is a FAILURE;
|
|
30
|
+
* 2. no unbounded RSS growth (least-squares slope over the tail half of
|
|
31
|
+
* the samples above --rss-slope MB/min AND total tail growth above
|
|
32
|
+
* --rss-floor MB fails);
|
|
33
|
+
* 3. no error-rate drift (tail-half error rate above --drift-factor × the
|
|
34
|
+
* whole-run rate, with at least --drift-min tail errors, fails);
|
|
35
|
+
* 4. every requested arm actually did work (an arm with zero successful
|
|
36
|
+
* operations fails — a dead arm is not a passing arm).
|
|
37
|
+
*
|
|
38
|
+
* Exit codes: 0 = PASS · 1 = FAIL (criteria) · 2 = harness/setup error.
|
|
39
|
+
*
|
|
40
|
+
* HONEST LIMITATION: a generic soak is a SCREEN, not proof. It exercises the
|
|
41
|
+
* connector's real code paths under load, but not your application's workload
|
|
42
|
+
* shape — run it as the FIRST filter on an SDK-bump candidate, with your own
|
|
43
|
+
* workload-shaped soak as the second gate. Recommended usage is two runs:
|
|
44
|
+
* first your current known-good SDK (baseline — also calibrates the RSS and
|
|
45
|
+
* drift thresholds on your hardware), then the candidate.
|
|
46
|
+
*
|
|
47
|
+
* REQUIREMENTS on the target (use a SCRATCH bucket, never a production one):
|
|
48
|
+
* - the bucket exists and the credentials can read/write it;
|
|
49
|
+
* - N1QL query service available; the harness issues
|
|
50
|
+
* `CREATE PRIMARY INDEX IF NOT EXISTS` on the bucket at setup (the query
|
|
51
|
+
* arm needs an index; USE KEYS and KV arms do not);
|
|
52
|
+
* - network reachability from this machine (preflighted; --skip-preflight
|
|
53
|
+
* to bypass);
|
|
54
|
+
* - `npm` on PATH (installs the candidate SDK + express-session into the
|
|
55
|
+
* throwaway project — network access required unless --sdk-path).
|
|
56
|
+
*
|
|
57
|
+
* Operator/consumer-invoked ONLY — never wired into the test suite or CI
|
|
58
|
+
* (CI has no cluster). Results land in ./tmp/couchbase-soak-<stamp>/
|
|
59
|
+
* (gitignored): samples.ndjson, boot.log, report.json, report.txt.
|
|
60
|
+
*
|
|
61
|
+
* Usage:
|
|
62
|
+
* node script/soak/couchbase-soak.js \
|
|
63
|
+
* --host=127.0.0.1 --database=soakbucket \
|
|
64
|
+
* --username=Administrator --password=secret \
|
|
65
|
+
* --sdk=4.6.1 --duration=15m
|
|
66
|
+
*
|
|
67
|
+
* Options (defaults):
|
|
68
|
+
* --host= (required) cluster host(s), comma-separated
|
|
69
|
+
* --database= (required) bucket name (also the models/ dir name)
|
|
70
|
+
* --username= (required) RBAC user
|
|
71
|
+
* --password= (required) RBAC password
|
|
72
|
+
* --sdk= couchbase npm version/range to install (XOR --sdk-path)
|
|
73
|
+
* --sdk-path= absolute path to an existing couchbase install dir
|
|
74
|
+
* (symlinked into the throwaway project instead of npm)
|
|
75
|
+
* --protocol=couchbase:// couchbases:// for TLS clusters
|
|
76
|
+
* --session-database= bucket for the session store entry (default: --database)
|
|
77
|
+
* --duration=15m soak length (30s / 15m / 1h forms)
|
|
78
|
+
* --concurrency=8 concurrent driver workers (round-robined over arms)
|
|
79
|
+
* --arms=query,kv,session subset selection
|
|
80
|
+
* --durability= `majority` adds DurabilityLevel.Majority to the
|
|
81
|
+
* callback-form KV insert (needs cluster support; off
|
|
82
|
+
* by default — topology-dependent)
|
|
83
|
+
* --kv-expiry=3600 expiry (s) stamped on soak KV documents
|
|
84
|
+
* --sample-interval=5 seconds between /soak/stats samples
|
|
85
|
+
* --rss-slope=5 FAIL threshold: tail RSS slope in MB/min
|
|
86
|
+
* --rss-floor=50 FAIL threshold: minimum tail RSS growth in MB
|
|
87
|
+
* --drift-factor=2 FAIL threshold: tail error rate vs whole-run rate
|
|
88
|
+
* --drift-min=20 FAIL threshold: minimum tail errors to call drift
|
|
89
|
+
* --setup-timeout=90 seconds allowed for the in-bundle setup route
|
|
90
|
+
* --start-port-from=9840 port scan base for the throwaway bundle
|
|
91
|
+
* --skip-preflight skip the TCP reachability check
|
|
92
|
+
* --keep keep the throwaway home + project for forensics
|
|
93
|
+
*
|
|
94
|
+
* @module script/soak/couchbase-soak
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
var fs = require('fs');
|
|
98
|
+
var os = require('os');
|
|
99
|
+
var path = require('path');
|
|
100
|
+
var net = require('net');
|
|
101
|
+
var http = require('http');
|
|
102
|
+
var { spawn, spawnSync } = require('child_process');
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Pure helpers (exported for the unit suite — no I/O, no process state)
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Default option values + failure thresholds (exported so the unit suite and
|
|
110
|
+
* the docs can pin them).
|
|
111
|
+
* @constant
|
|
112
|
+
* @type {object}
|
|
113
|
+
*/
|
|
114
|
+
var DEFAULTS = {
|
|
115
|
+
protocol : 'couchbase://',
|
|
116
|
+
duration : '15m',
|
|
117
|
+
concurrency : 8,
|
|
118
|
+
arms : ['query', 'kv', 'session'],
|
|
119
|
+
kvExpiry : 3600,
|
|
120
|
+
sampleInterval : 5,
|
|
121
|
+
rssSlope : 5, // MB/min over the tail half
|
|
122
|
+
rssFloor : 50, // MB minimum tail growth before slope can fail
|
|
123
|
+
driftFactor : 2, // tail error rate vs whole-run error rate
|
|
124
|
+
driftMin : 20, // minimum tail errors before drift can fail
|
|
125
|
+
setupTimeout : 90, // seconds
|
|
126
|
+
startPortFrom : 9840
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
var VALID_ARMS = ['query', 'kv', 'session'];
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Parses a human duration (`90s`, `15m`, `2h`; a bare number means seconds)
|
|
133
|
+
* into milliseconds.
|
|
134
|
+
*
|
|
135
|
+
* @param {string|number} v
|
|
136
|
+
* @returns {number|null} milliseconds, or null when unparseable/non-positive
|
|
137
|
+
*/
|
|
138
|
+
function parseDuration(v) {
|
|
139
|
+
if (v == null) { return null; }
|
|
140
|
+
var m = String(v).trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h)?$/i);
|
|
141
|
+
if (!m) { return null; }
|
|
142
|
+
var n = parseFloat(m[1]);
|
|
143
|
+
if (!(n > 0)) { return null; }
|
|
144
|
+
var unit = (m[2] || 's').toLowerCase();
|
|
145
|
+
var mult = (unit === 'h') ? 3600000 : (unit === 'm') ? 60000 : 1000;
|
|
146
|
+
return Math.round(n * mult);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parses argv (`--name=value` / bare `--flag` forms) into a validated options
|
|
151
|
+
* object. Pure: no process access, no exit — callers decide what to do with
|
|
152
|
+
* `{ error }`.
|
|
153
|
+
*
|
|
154
|
+
* @param {string[]} argv - arguments AFTER the script path (process.argv.slice(2))
|
|
155
|
+
* @returns {{ok: boolean, error: (string|null), opts: (object|null)}}
|
|
156
|
+
*/
|
|
157
|
+
function parseArgs(argv) {
|
|
158
|
+
var raw = {};
|
|
159
|
+
for (var i = 0; i < argv.length; i++) {
|
|
160
|
+
var a = argv[i];
|
|
161
|
+
if (a.indexOf('--') !== 0) { return { ok: false, error: 'unrecognized argument: ' + a, opts: null }; }
|
|
162
|
+
var eq = a.indexOf('=');
|
|
163
|
+
if (eq === -1) { raw[a.slice(2)] = true; }
|
|
164
|
+
else { raw[a.slice(2, eq)] = a.slice(eq + 1); }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function intOpt(name, dflt) {
|
|
168
|
+
if (raw[name] == null) { return dflt; }
|
|
169
|
+
var n = parseInt(raw[name], 10);
|
|
170
|
+
return isNaN(n) ? NaN : n;
|
|
171
|
+
}
|
|
172
|
+
function numOpt(name, dflt) {
|
|
173
|
+
if (raw[name] == null) { return dflt; }
|
|
174
|
+
var n = parseFloat(raw[name]);
|
|
175
|
+
return isNaN(n) ? NaN : n;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
var required = ['host', 'database', 'username', 'password'];
|
|
179
|
+
for (var r = 0; r < required.length; r++) {
|
|
180
|
+
if (!raw[required[r]] || raw[required[r]] === true) {
|
|
181
|
+
return { ok: false, error: 'missing required option: --' + required[r], opts: null };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
var hasSdk = !!raw.sdk && raw.sdk !== true;
|
|
185
|
+
var hasSdkPath = !!raw['sdk-path'] && raw['sdk-path'] !== true;
|
|
186
|
+
if (hasSdk === hasSdkPath) { // neither, or both
|
|
187
|
+
return { ok: false, error: 'exactly one of --sdk=<version> or --sdk-path=<dir> is required (the SDK candidate under test must be explicit — there is no default)', opts: null };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
var durationMs = parseDuration(raw.duration || DEFAULTS.duration);
|
|
191
|
+
if (durationMs == null || durationMs < 10000) {
|
|
192
|
+
return { ok: false, error: 'invalid --duration (10s minimum; forms: 90s / 15m / 1h): ' + raw.duration, opts: null };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// raw.arms == null means "not given" (defaults apply); an explicit
|
|
196
|
+
// `--arms=` (empty) is a selection of zero arms and must error below.
|
|
197
|
+
var armsRaw = (raw.arms == null) ? DEFAULTS.arms.join(',') : String(raw.arms);
|
|
198
|
+
var arms = armsRaw.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
|
199
|
+
var badArms = arms.filter(function (a) { return VALID_ARMS.indexOf(a) === -1; });
|
|
200
|
+
if (!arms.length || badArms.length) {
|
|
201
|
+
return { ok: false, error: 'invalid --arms (valid: ' + VALID_ARMS.join(',') + '): ' + (badArms.join(',') || '(empty)'), opts: null };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (raw.durability != null && raw.durability !== 'majority') {
|
|
205
|
+
return { ok: false, error: 'invalid --durability (only `majority` is supported): ' + raw.durability, opts: null };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
var opts = {
|
|
209
|
+
host : String(raw.host),
|
|
210
|
+
database : String(raw.database),
|
|
211
|
+
username : String(raw.username),
|
|
212
|
+
password : String(raw.password),
|
|
213
|
+
protocol : String(raw.protocol || DEFAULTS.protocol),
|
|
214
|
+
sdk : hasSdk ? String(raw.sdk) : null,
|
|
215
|
+
sdkPath : hasSdkPath ? String(raw['sdk-path']) : null,
|
|
216
|
+
sessionDatabase : String(raw['session-database'] || raw.database),
|
|
217
|
+
durationMs : durationMs,
|
|
218
|
+
concurrency : intOpt('concurrency', DEFAULTS.concurrency),
|
|
219
|
+
arms : arms,
|
|
220
|
+
durability : raw.durability === 'majority' ? 'majority' : null,
|
|
221
|
+
kvExpiry : intOpt('kv-expiry', DEFAULTS.kvExpiry),
|
|
222
|
+
sampleIntervalMs: numOpt('sample-interval', DEFAULTS.sampleInterval) * 1000,
|
|
223
|
+
thresholds : {
|
|
224
|
+
rssSlopeMBperMin : numOpt('rss-slope', DEFAULTS.rssSlope),
|
|
225
|
+
rssFloorMB : numOpt('rss-floor', DEFAULTS.rssFloor),
|
|
226
|
+
driftFactor : numOpt('drift-factor', DEFAULTS.driftFactor),
|
|
227
|
+
driftMinErrors : intOpt('drift-min', DEFAULTS.driftMin)
|
|
228
|
+
},
|
|
229
|
+
setupTimeoutMs : intOpt('setup-timeout', DEFAULTS.setupTimeout) * 1000,
|
|
230
|
+
startPortFrom : intOpt('start-port-from', DEFAULTS.startPortFrom),
|
|
231
|
+
skipPreflight : !!raw['skip-preflight'],
|
|
232
|
+
keep : !!raw.keep
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
var numeric = ['concurrency', 'kvExpiry', 'sampleIntervalMs', 'setupTimeoutMs', 'startPortFrom'];
|
|
236
|
+
for (var n = 0; n < numeric.length; n++) {
|
|
237
|
+
if (isNaN(opts[numeric[n]]) || opts[numeric[n]] <= 0) {
|
|
238
|
+
return { ok: false, error: 'invalid numeric option near --' + numeric[n], opts: null };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (var t in opts.thresholds) {
|
|
242
|
+
if (isNaN(opts.thresholds[t]) || opts.thresholds[t] < 0) {
|
|
243
|
+
return { ok: false, error: 'invalid threshold option near ' + t, opts: null };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return { ok: true, error: null, opts: opts };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Least-squares slope of RSS over time, in MB per minute.
|
|
251
|
+
*
|
|
252
|
+
* @param {Array<{tMs: number, rssBytes: number}>} points
|
|
253
|
+
* @returns {number} slope in MB/min (0 for fewer than 2 points)
|
|
254
|
+
*/
|
|
255
|
+
function lsqSlopeMBperMin(points) {
|
|
256
|
+
if (!points || points.length < 2) { return 0; }
|
|
257
|
+
var n = points.length, sx = 0, sy = 0, sxx = 0, sxy = 0;
|
|
258
|
+
for (var i = 0; i < n; i++) {
|
|
259
|
+
var x = points[i].tMs / 60000; // minutes
|
|
260
|
+
var y = points[i].rssBytes / (1024 * 1024); // MB
|
|
261
|
+
sx += x; sy += y; sxx += x * x; sxy += x * y;
|
|
262
|
+
}
|
|
263
|
+
var denom = n * sxx - sx * sx;
|
|
264
|
+
if (denom === 0) { return 0; }
|
|
265
|
+
return (n * sxy - sx * sy) / denom;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Evaluates a completed (or prematurely ended) soak run against the filed
|
|
270
|
+
* pass criteria. Pure — feed it the recorded inputs, get the verdict.
|
|
271
|
+
*
|
|
272
|
+
* Sample shape (counters are CUMULATIVE totals at sample time):
|
|
273
|
+
* { tMs, rssBytes, arms: { <arm>: { ok, errTransient, errPermanent, transport } } }
|
|
274
|
+
*
|
|
275
|
+
* @param {object} input
|
|
276
|
+
* @param {number} input.plannedMs - requested soak duration
|
|
277
|
+
* @param {?object} input.endedEarly - null, or {atMs, exit: {code, signal}}
|
|
278
|
+
* @param {Array} input.samples - chronological samples (shape above)
|
|
279
|
+
* @param {string[]} input.armsRequested - arms the run was asked to drive
|
|
280
|
+
* @param {object} input.thresholds - {rssSlopeMBperMin, rssFloorMB, driftFactor, driftMinErrors}
|
|
281
|
+
* @returns {{pass: boolean, failures: Array<{check: string, detail: string}>, warnings: string[], metrics: object}}
|
|
282
|
+
*/
|
|
283
|
+
function evaluateRun(input) {
|
|
284
|
+
var failures = [];
|
|
285
|
+
var warnings = [];
|
|
286
|
+
var metrics = {};
|
|
287
|
+
var th = input.thresholds || {};
|
|
288
|
+
var samples = input.samples || [];
|
|
289
|
+
|
|
290
|
+
// 1 — liveness: the process must survive the full duration. A clean
|
|
291
|
+
// exit 0 before the end IS the failure class this harness exists for.
|
|
292
|
+
if (input.endedEarly) {
|
|
293
|
+
var ex = input.endedEarly.exit || {};
|
|
294
|
+
var how = (ex.code === 0)
|
|
295
|
+
? 'a CLEAN exit 0 — this is the silent-death class, and it counts as FAILURE'
|
|
296
|
+
: (ex.signal ? 'signal ' + ex.signal : 'exit code ' + ex.code);
|
|
297
|
+
failures.push({
|
|
298
|
+
check : 'liveness',
|
|
299
|
+
detail : 'bundle process ended after ' + Math.round(input.endedEarly.atMs / 1000) + 's of a planned '
|
|
300
|
+
+ Math.round(input.plannedMs / 1000) + 's soak (' + how + ')'
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 0 — evidence floor: a run with (almost) no samples proves nothing.
|
|
305
|
+
if (samples.length < 2) {
|
|
306
|
+
failures.push({ check: 'samples', detail: 'only ' + samples.length + ' sample(s) recorded — no basis for a verdict' });
|
|
307
|
+
return { pass: false, failures: failures, warnings: warnings, metrics: metrics };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
var last = samples[samples.length - 1];
|
|
311
|
+
var tailStart = Math.floor(samples.length / 2);
|
|
312
|
+
var tail = samples.slice(tailStart);
|
|
313
|
+
|
|
314
|
+
// 2 — RSS growth over the tail half.
|
|
315
|
+
if (samples.length >= 6) {
|
|
316
|
+
var slope = lsqSlopeMBperMin(tail);
|
|
317
|
+
var growthMB = (tail[tail.length - 1].rssBytes - tail[0].rssBytes) / (1024 * 1024);
|
|
318
|
+
metrics.rssSlopeMBperMin = Math.round(slope * 100) / 100;
|
|
319
|
+
metrics.rssTailGrowthMB = Math.round(growthMB * 10) / 10;
|
|
320
|
+
metrics.rssLastMB = Math.round(last.rssBytes / (1024 * 1024));
|
|
321
|
+
if (slope > th.rssSlopeMBperMin && growthMB > th.rssFloorMB) {
|
|
322
|
+
failures.push({
|
|
323
|
+
check : 'rss',
|
|
324
|
+
detail : 'tail RSS slope ' + metrics.rssSlopeMBperMin + ' MB/min with ' + metrics.rssTailGrowthMB
|
|
325
|
+
+ ' MB tail growth (thresholds: ' + th.rssSlopeMBperMin + ' MB/min AND ' + th.rssFloorMB + ' MB)'
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
warnings.push('rss check skipped — fewer than 6 samples (run longer or lower --sample-interval)');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 3 + 4 — per-arm error drift and dead arms, from cumulative counters.
|
|
333
|
+
var armsRequested = input.armsRequested || [];
|
|
334
|
+
metrics.arms = {};
|
|
335
|
+
for (var a = 0; a < armsRequested.length; a++) {
|
|
336
|
+
var arm = armsRequested[a];
|
|
337
|
+
var endC = (last.arms || {})[arm];
|
|
338
|
+
var midC = (samples[tailStart].arms || {})[arm];
|
|
339
|
+
if (!endC) {
|
|
340
|
+
failures.push({ check: 'dead-arm', detail: 'arm `' + arm + '` reported no counters at all' });
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
var okTotal = endC.ok || 0;
|
|
344
|
+
var errTotal = (endC.errTransient || 0) + (endC.errPermanent || 0) + (endC.transport || 0);
|
|
345
|
+
var opsTotal = okTotal + errTotal;
|
|
346
|
+
if (okTotal === 0) {
|
|
347
|
+
failures.push({ check: 'dead-arm', detail: 'arm `' + arm + '` completed zero successful operations (' + errTotal + ' errors)' });
|
|
348
|
+
}
|
|
349
|
+
var tailErr = 0, tailOps = 0;
|
|
350
|
+
if (midC) {
|
|
351
|
+
var midErr = (midC.errTransient || 0) + (midC.errPermanent || 0) + (midC.transport || 0);
|
|
352
|
+
var midOps = (midC.ok || 0) + midErr;
|
|
353
|
+
tailErr = errTotal - midErr;
|
|
354
|
+
tailOps = opsTotal - midOps;
|
|
355
|
+
}
|
|
356
|
+
var overallRate = opsTotal ? errTotal / opsTotal : 0;
|
|
357
|
+
var tailRate = tailOps ? tailErr / tailOps : 0;
|
|
358
|
+
metrics.arms[arm] = {
|
|
359
|
+
ok: okTotal, errors: errTotal,
|
|
360
|
+
overallErrRate: Math.round(overallRate * 10000) / 10000,
|
|
361
|
+
tailErrRate: Math.round(tailRate * 10000) / 10000
|
|
362
|
+
};
|
|
363
|
+
if (tailErr >= th.driftMinErrors && tailRate > overallRate * th.driftFactor) {
|
|
364
|
+
failures.push({
|
|
365
|
+
check : 'error-drift',
|
|
366
|
+
detail : 'arm `' + arm + '`: tail error rate ' + metrics.arms[arm].tailErrRate
|
|
367
|
+
+ ' vs whole-run ' + metrics.arms[arm].overallErrRate
|
|
368
|
+
+ ' (' + tailErr + ' tail errors; thresholds: ×' + th.driftFactor + ', min ' + th.driftMinErrors + ')'
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { pass: failures.length === 0, failures: failures, warnings: warnings, metrics: metrics };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Harness (I/O from here down — nothing below runs when require()d)
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
module.exports = {
|
|
381
|
+
DEFAULTS : DEFAULTS,
|
|
382
|
+
VALID_ARMS : VALID_ARMS,
|
|
383
|
+
parseDuration : parseDuration,
|
|
384
|
+
parseArgs : parseArgs,
|
|
385
|
+
lsqSlopeMBperMin : lsqSlopeMBperMin,
|
|
386
|
+
evaluateRun : evaluateRun
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/** @param {string} msg */
|
|
390
|
+
function log(msg) { process.stdout.write('[couchbase-soak] ' + msg + '\n'); }
|
|
391
|
+
|
|
392
|
+
/** @param {number} ms @returns {Promise} */
|
|
393
|
+
function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Builds the per-run context (paths, names, child env). Kept out of module
|
|
397
|
+
* scope so require()ing this file for its pure helpers touches nothing.
|
|
398
|
+
*
|
|
399
|
+
* @param {object} opts - parseArgs() output
|
|
400
|
+
* @returns {object} run context
|
|
401
|
+
*/
|
|
402
|
+
function buildRunContext(opts) {
|
|
403
|
+
var STAMP = Date.now();
|
|
404
|
+
var GINA_ROOT = path.resolve(__dirname, '..', '..');
|
|
405
|
+
var HOME_BASE = process.env.GINA_SOAK_HOME_BASE
|
|
406
|
+
? fs.realpathSync(process.env.GINA_SOAK_HOME_BASE)
|
|
407
|
+
: fs.realpathSync(os.tmpdir());
|
|
408
|
+
var FAKE_HOME = path.join(HOME_BASE, 'gina-soak-' + STAMP);
|
|
409
|
+
var ctx = {
|
|
410
|
+
stamp : STAMP,
|
|
411
|
+
proj : 'soak' + STAMP,
|
|
412
|
+
bundle : 'soak',
|
|
413
|
+
ginaRoot : GINA_ROOT,
|
|
414
|
+
cli : path.join(GINA_ROOT, 'bin', 'cli'),
|
|
415
|
+
container : path.join(GINA_ROOT, 'bin', 'gina-container'),
|
|
416
|
+
fakeHome : FAKE_HOME,
|
|
417
|
+
// neutral cwd: NOT the repo (stray `gina` self-symlink), NOT the fake
|
|
418
|
+
// home (dead prod scenes when cwd holds .gina/) — a sibling dir.
|
|
419
|
+
cwdDir : FAKE_HOME + '-cwd',
|
|
420
|
+
ginaHome : path.join(FAKE_HOME, '.gina'),
|
|
421
|
+
projDir : path.join(FAKE_HOME, 'proj'),
|
|
422
|
+
out : path.join(GINA_ROOT, 'tmp', 'couchbase-soak-' + STAMP)
|
|
423
|
+
};
|
|
424
|
+
ctx.src = path.join(ctx.projDir, 'src', ctx.bundle);
|
|
425
|
+
ctx.childEnv = Object.assign({}, process.env, {
|
|
426
|
+
HOME : FAKE_HOME,
|
|
427
|
+
GINA_LOG_STDOUT : 'true'
|
|
428
|
+
});
|
|
429
|
+
delete ctx.childEnv.GINA_HOMEDIR;
|
|
430
|
+
delete ctx.childEnv.NODE_OPTIONS;
|
|
431
|
+
return ctx;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Runs an offline gina CLI command against the isolated home. Exit status is
|
|
436
|
+
* NOT trusted (first-run home bootstrap exits non-zero on a benign MQ
|
|
437
|
+
* teardown) — callers verify on-disk state instead.
|
|
438
|
+
*
|
|
439
|
+
* @param {object} ctx
|
|
440
|
+
* @param {string[]} args
|
|
441
|
+
* @returns {{status: (number|null), stdout: string, stderr: string}}
|
|
442
|
+
*/
|
|
443
|
+
function runCli(ctx, args) {
|
|
444
|
+
var r = spawnSync(process.execPath, [ctx.cli].concat(args), {
|
|
445
|
+
env: ctx.childEnv, cwd: ctx.cwdDir, encoding: 'utf8', timeout: 120000
|
|
446
|
+
});
|
|
447
|
+
return { status: r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** @param {string} file @returns {object} */
|
|
451
|
+
function readJSON(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* TCP-dials one host/port with a short timeout.
|
|
455
|
+
* @param {string} host
|
|
456
|
+
* @param {number} port
|
|
457
|
+
* @param {number} timeoutMs
|
|
458
|
+
* @returns {Promise<boolean>}
|
|
459
|
+
*/
|
|
460
|
+
function dial(host, port, timeoutMs) {
|
|
461
|
+
return new Promise(function (resolve) {
|
|
462
|
+
var sock = new net.Socket();
|
|
463
|
+
var done = false;
|
|
464
|
+
function finish(v) { if (!done) { done = true; try { sock.destroy(); } catch (e) { /* ignore */ } resolve(v); } }
|
|
465
|
+
sock.setTimeout(timeoutMs);
|
|
466
|
+
sock.on('connect', function () { finish(true); });
|
|
467
|
+
sock.on('error', function () { finish(false); });
|
|
468
|
+
sock.on('timeout', function () { finish(false); });
|
|
469
|
+
sock.connect(port, host);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Preflight: the first configured host must accept TCP on the KV port (11210)
|
|
475
|
+
* or the management port (8091) — or on its explicit :port when one is given.
|
|
476
|
+
* Without this, an unreachable cluster leaves the connector in its infinite
|
|
477
|
+
* reconnect backoff and the boot never becomes ready.
|
|
478
|
+
*
|
|
479
|
+
* @param {object} opts
|
|
480
|
+
* @returns {Promise<?string>} null when reachable, else a human explanation
|
|
481
|
+
*/
|
|
482
|
+
async function preflight(opts) {
|
|
483
|
+
var first = opts.host.split(',')[0].trim();
|
|
484
|
+
var m = first.match(/^(.*?):(\d+)$/);
|
|
485
|
+
if (m) {
|
|
486
|
+
return (await dial(m[1], parseInt(m[2], 10), 4000))
|
|
487
|
+
? null
|
|
488
|
+
: first + ' did not accept TCP within 4s';
|
|
489
|
+
}
|
|
490
|
+
if (await dial(first, 11210, 4000)) { return null; }
|
|
491
|
+
if (await dial(first, 8091, 4000)) { return null; }
|
|
492
|
+
return first + ' accepted TCP on neither 11210 (KV) nor 8091 (mgmt) within 4s';
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Scaffolds project + bundle into the isolated home (state-file-verified) and
|
|
497
|
+
* links <proj>/node_modules/gina explicitly (the CLI auto-link is unreliable
|
|
498
|
+
* under an isolated home, and npm installs prune it besides).
|
|
499
|
+
*
|
|
500
|
+
* @param {object} ctx
|
|
501
|
+
* @param {object} opts
|
|
502
|
+
* @returns {void} throws on failure
|
|
503
|
+
*/
|
|
504
|
+
function scaffold(ctx, opts) {
|
|
505
|
+
fs.mkdirSync(ctx.projDir, { recursive: true });
|
|
506
|
+
fs.mkdirSync(ctx.cwdDir, { recursive: true });
|
|
507
|
+
runCli(ctx, ['project:add', '@' + ctx.proj, '--path=' + ctx.projDir]);
|
|
508
|
+
if (!fs.existsSync(path.join(ctx.ginaHome, 'projects.json')) ||
|
|
509
|
+
!readJSON(path.join(ctx.ginaHome, 'projects.json'))[ctx.proj]) {
|
|
510
|
+
throw new Error('project:add did not register @' + ctx.proj);
|
|
511
|
+
}
|
|
512
|
+
relinkGina(ctx);
|
|
513
|
+
runCli(ctx, ['bundle:add', ctx.bundle, '@' + ctx.proj, '--start-port-from=' + opts.startPortFrom]);
|
|
514
|
+
var key = ctx.bundle + '@' + ctx.proj;
|
|
515
|
+
if (!readJSON(path.join(ctx.ginaHome, 'ports.reverse.json'))[key]) {
|
|
516
|
+
throw new Error('bundle:add did not register ' + key);
|
|
517
|
+
}
|
|
518
|
+
log('scaffolded ' + key + ' in ' + ctx.fakeHome);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* (Re)creates the <proj>/node_modules/gina symlink — required for the boot's
|
|
523
|
+
* require('gina'), and PRUNED by every npm install in the project (npm treats
|
|
524
|
+
* it as extraneous).
|
|
525
|
+
* @param {object} ctx
|
|
526
|
+
* @returns {void}
|
|
527
|
+
*/
|
|
528
|
+
function relinkGina(ctx) {
|
|
529
|
+
var nmDir = path.join(ctx.projDir, 'node_modules');
|
|
530
|
+
fs.mkdirSync(nmDir, { recursive: true });
|
|
531
|
+
var link = path.join(nmDir, 'gina');
|
|
532
|
+
try { if (fs.lstatSync(link)) { return; } } catch (e) { /* absent — create */ }
|
|
533
|
+
fs.symlinkSync(ctx.ginaRoot, link);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Installs the SDK candidate + express-session into the throwaway project.
|
|
538
|
+
* The install is the SDK selector: the connector resolves `couchbase` from
|
|
539
|
+
* the project's node_modules AND derives the v3/v4 dispatch from the
|
|
540
|
+
* project package.json's `dependencies.couchbase`, so both must be real.
|
|
541
|
+
* With --sdk-path the install dir is symlinked and the dependency pin is
|
|
542
|
+
* written from that install's own package.json version.
|
|
543
|
+
*
|
|
544
|
+
* @param {object} ctx
|
|
545
|
+
* @param {object} opts
|
|
546
|
+
* @returns {void} throws on failure
|
|
547
|
+
*/
|
|
548
|
+
function installDeps(ctx, opts) {
|
|
549
|
+
var pkgPath = path.join(ctx.projDir, 'package.json');
|
|
550
|
+
if (!fs.existsSync(pkgPath)) {
|
|
551
|
+
// never let npm walk up to an ancestor package.json (worst case: this repo)
|
|
552
|
+
throw new Error('scaffolded project has no package.json at ' + pkgPath + ' — refusing to npm install');
|
|
553
|
+
}
|
|
554
|
+
function npmi(args, label) {
|
|
555
|
+
log('npm install: ' + label + ' (this may take a while — native SDK)');
|
|
556
|
+
var r = spawnSync('npm', ['install'].concat(args, ['--no-audit', '--no-fund', '--loglevel=error']), {
|
|
557
|
+
cwd: ctx.projDir, encoding: 'utf8', timeout: 600000
|
|
558
|
+
});
|
|
559
|
+
if (r.status !== 0) {
|
|
560
|
+
throw new Error('npm install ' + label + ' failed (' + r.status + '):\n' + ((r.stderr || '') + (r.stdout || '')).slice(-2500));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (opts.sdk) {
|
|
564
|
+
npmi(['couchbase@' + opts.sdk], 'couchbase@' + opts.sdk);
|
|
565
|
+
} else {
|
|
566
|
+
var sdkPkg = path.join(opts.sdkPath, 'package.json');
|
|
567
|
+
if (!fs.existsSync(sdkPkg)) { throw new Error('--sdk-path has no package.json: ' + opts.sdkPath); }
|
|
568
|
+
var v = readJSON(sdkPkg).version;
|
|
569
|
+
var nmDir = path.join(ctx.projDir, 'node_modules');
|
|
570
|
+
fs.mkdirSync(nmDir, { recursive: true });
|
|
571
|
+
fs.symlinkSync(fs.realpathSync(opts.sdkPath), path.join(nmDir, 'couchbase'));
|
|
572
|
+
var pkg = readJSON(pkgPath);
|
|
573
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
574
|
+
pkg.dependencies.couchbase = v; // the v3/v4 resolver reads this pin
|
|
575
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
576
|
+
log('symlinked SDK from ' + opts.sdkPath + ' (couchbase@' + v + ')');
|
|
577
|
+
}
|
|
578
|
+
npmi(['express-session'], 'express-session');
|
|
579
|
+
relinkGina(ctx); // npm pruned the extraneous symlink — restore it
|
|
580
|
+
|
|
581
|
+
var resolved = path.join(ctx.projDir, 'node_modules', 'couchbase', 'package.json');
|
|
582
|
+
if (!fs.existsSync(resolved)) { throw new Error('couchbase did not land in the project node_modules'); }
|
|
583
|
+
ctx.sdkVersion = readJSON(resolved).version;
|
|
584
|
+
log('SDK under test: couchbase@' + ctx.sdkVersion);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Writes the soak fixture into the bundle src: connectors.json (data entry
|
|
589
|
+
* named EXACTLY like the bucket + the literally-named `session` entry),
|
|
590
|
+
* entity + N1QL files, routes, the soak controller, and the express-session
|
|
591
|
+
* wire-in (the documented `lib.SessionStore` shape).
|
|
592
|
+
*
|
|
593
|
+
* @param {object} ctx
|
|
594
|
+
* @param {object} opts
|
|
595
|
+
* @returns {void}
|
|
596
|
+
*/
|
|
597
|
+
function fixture(ctx, opts) {
|
|
598
|
+
var db = opts.database;
|
|
599
|
+
var confDir = path.join(ctx.src, 'config');
|
|
600
|
+
|
|
601
|
+
// -- connectors.json — entry key == database name so the model registry
|
|
602
|
+
// (models/<entry>/) and the connector's own walk (models/<database>/)
|
|
603
|
+
// agree on one directory whichever one resolves.
|
|
604
|
+
var connectors = {};
|
|
605
|
+
connectors[db] = {
|
|
606
|
+
connector : 'couchbase',
|
|
607
|
+
protocol : opts.protocol,
|
|
608
|
+
host : opts.host,
|
|
609
|
+
database : db,
|
|
610
|
+
username : opts.username,
|
|
611
|
+
password : opts.password
|
|
612
|
+
};
|
|
613
|
+
connectors.session = {
|
|
614
|
+
connector : 'couchbase',
|
|
615
|
+
protocol : opts.protocol,
|
|
616
|
+
host : opts.host,
|
|
617
|
+
database : opts.sessionDatabase,
|
|
618
|
+
username : opts.username,
|
|
619
|
+
password : opts.password
|
|
620
|
+
};
|
|
621
|
+
fs.writeFileSync(path.join(confDir, 'connectors.json'), JSON.stringify(connectors, null, 2));
|
|
622
|
+
|
|
623
|
+
// -- models/<db>/entities/soak.js + models/<db>/n1ql/soak/*.sql
|
|
624
|
+
var entDir = path.join(ctx.src, 'models', db, 'entities');
|
|
625
|
+
var n1qlDir = path.join(ctx.src, 'models', db, 'n1ql', 'soak');
|
|
626
|
+
fs.mkdirSync(entDir, { recursive: true });
|
|
627
|
+
fs.mkdirSync(n1qlDir, { recursive: true });
|
|
628
|
+
|
|
629
|
+
var durabilityLines = opts.durability === 'majority'
|
|
630
|
+
? [
|
|
631
|
+
' var couchbase = require(' + JSON.stringify(path.join(ctx.projDir, 'node_modules', 'couchbase')) + ');',
|
|
632
|
+
' var KV_OPTS = { expiry: ' + opts.kvExpiry + ', durabilityLevel: couchbase.DurabilityLevel.Majority };'
|
|
633
|
+
]
|
|
634
|
+
: [
|
|
635
|
+
' var KV_OPTS = { expiry: ' + opts.kvExpiry + ' };'
|
|
636
|
+
];
|
|
637
|
+
|
|
638
|
+
fs.writeFileSync(path.join(entDir, 'soak.js'), [
|
|
639
|
+
'/**',
|
|
640
|
+
' * Soak entity — generated by script/soak/couchbase-soak.js.',
|
|
641
|
+
' * Custom KV methods are INSTANCE methods (assigned in the constructor):',
|
|
642
|
+
' * prototype-level user methods do not survive the connector\'s inherits().',
|
|
643
|
+
' */',
|
|
644
|
+
'function Soak(conn, caller) {',
|
|
645
|
+
' var self = this;'
|
|
646
|
+
].concat(durabilityLines, [
|
|
647
|
+
'',
|
|
648
|
+
' /**',
|
|
649
|
+
' * Callback-form KV insert — the 4-arg consumer shape:',
|
|
650
|
+
' * coll.insert(key, doc, opts, cb) on the entity collection handle.',
|
|
651
|
+
' * @param {string} key',
|
|
652
|
+
' * @param {object} doc',
|
|
653
|
+
' * @callback cb - (err, mutationResult)',
|
|
654
|
+
' */',
|
|
655
|
+
' this.kvCbInsert = function(key, doc, cb) {',
|
|
656
|
+
' doc._collection = \'soak\';',
|
|
657
|
+
' doc._scope = self._scope;',
|
|
658
|
+
' var coll = self.getConnection();',
|
|
659
|
+
' coll.insert(key, doc, KV_OPTS, cb);',
|
|
660
|
+
' };',
|
|
661
|
+
'',
|
|
662
|
+
' /**',
|
|
663
|
+
' * Promise-form KV cycle: insert a fresh expiry-bounded doc, read it',
|
|
664
|
+
' * back, remove every 4th (bounds bucket growth alongside expiry).',
|
|
665
|
+
' * @param {string} key',
|
|
666
|
+
' * @param {object} doc',
|
|
667
|
+
' * @param {number} seq',
|
|
668
|
+
' * @returns {Promise<object>} the read-back content',
|
|
669
|
+
' */',
|
|
670
|
+
' this.kvPromiseCycle = async function(key, doc, seq) {',
|
|
671
|
+
' doc._collection = \'soak\';',
|
|
672
|
+
' doc._scope = self._scope;',
|
|
673
|
+
' var coll = self.getConnection();',
|
|
674
|
+
' await coll.insert(key, doc, KV_OPTS);',
|
|
675
|
+
' var got = await coll.get(key);',
|
|
676
|
+
' if (seq % 4 === 0) { await coll.remove(key); }',
|
|
677
|
+
' return got.value;',
|
|
678
|
+
' };',
|
|
679
|
+
'}',
|
|
680
|
+
'',
|
|
681
|
+
'module.exports = Soak;',
|
|
682
|
+
''
|
|
683
|
+
]).join('\n'));
|
|
684
|
+
|
|
685
|
+
var bt = '`'; // N1QL identifier backtick
|
|
686
|
+
fs.writeFileSync(path.join(n1qlDir, 'findByWorker.sql'), [
|
|
687
|
+
'/**',
|
|
688
|
+
' * @param {string} $1 worker tag',
|
|
689
|
+
' * @return {array}',
|
|
690
|
+
' */',
|
|
691
|
+
'SELECT t.worker, t.seq, t.bumped',
|
|
692
|
+
'FROM ' + bt + db + bt + ' t',
|
|
693
|
+
'WHERE t._collection = \'soak\'',
|
|
694
|
+
' AND t._scope = $scope',
|
|
695
|
+
' AND t.worker = $1',
|
|
696
|
+
'LIMIT 20'
|
|
697
|
+
].join('\n'));
|
|
698
|
+
|
|
699
|
+
fs.writeFileSync(path.join(n1qlDir, 'findByWorkerRp.sql'), [
|
|
700
|
+
'/**',
|
|
701
|
+
' * @options { consistency: "request_plus" }',
|
|
702
|
+
' * @param {string} $1 worker tag',
|
|
703
|
+
' * @return {array}',
|
|
704
|
+
' */',
|
|
705
|
+
'SELECT t.worker, t.seq, t.bumped',
|
|
706
|
+
'FROM ' + bt + db + bt + ' t',
|
|
707
|
+
'WHERE t._collection = \'soak\'',
|
|
708
|
+
' AND t._scope = $scope',
|
|
709
|
+
' AND t.worker = $1',
|
|
710
|
+
'LIMIT 20'
|
|
711
|
+
].join('\n'));
|
|
712
|
+
|
|
713
|
+
fs.writeFileSync(path.join(n1qlDir, 'bumpByKey.sql'), [
|
|
714
|
+
'/**',
|
|
715
|
+
' * @param {string} $1 document key',
|
|
716
|
+
' * @param {string} $2 bump tag',
|
|
717
|
+
' * @return {array}',
|
|
718
|
+
' */',
|
|
719
|
+
'UPDATE ' + bt + db + bt + ' t',
|
|
720
|
+
'USE KEYS $1',
|
|
721
|
+
'SET t.bumped = $2',
|
|
722
|
+
'RETURNING t.seq'
|
|
723
|
+
].join('\n'));
|
|
724
|
+
|
|
725
|
+
// -- routes: all param-less GETs/POST — the controller rotates its own
|
|
726
|
+
// worker tags/keys server-side, so no request-param API is involved.
|
|
727
|
+
fs.writeFileSync(path.join(confDir, 'routing.json'), JSON.stringify({
|
|
728
|
+
'$schema' : 'https://gina.io/schema/routing.json',
|
|
729
|
+
'soak-setup' : { url: '/soak/setup', method: 'POST', param: { control: 'soakSetup' } },
|
|
730
|
+
'soak-query' : { url: '/soak/query', method: 'GET', param: { control: 'soakQuery' } },
|
|
731
|
+
'soak-query-rp' : { url: '/soak/query-rp', method: 'GET', param: { control: 'soakQueryRp' } },
|
|
732
|
+
'soak-kv-cb' : { url: '/soak/kv-cb', method: 'GET', param: { control: 'soakKvCb' } },
|
|
733
|
+
'soak-kv-p' : { url: '/soak/kv-p', method: 'GET', param: { control: 'soakKvPromise' } },
|
|
734
|
+
'soak-session' : { url: '/soak/session', method: 'GET', param: { control: 'soakSession' } },
|
|
735
|
+
'soak-session-bye' : { url: '/soak/session-destroy', method: 'GET', param: { control: 'soakSessionDestroy' } },
|
|
736
|
+
'soak-stats' : { url: '/soak/stats', method: 'GET', param: { control: 'soakStats' } }
|
|
737
|
+
}, null, 2));
|
|
738
|
+
|
|
739
|
+
// -- the soak controller (replaces the scaffold default wholesale)
|
|
740
|
+
fs.writeFileSync(path.join(ctx.src, 'controllers', 'controller.js'), [
|
|
741
|
+
'/**',
|
|
742
|
+
' * SoakController — generated by script/soak/couchbase-soak.js.',
|
|
743
|
+
' * Counters live on `global.__soakStats` (prod bundle: load-once, but the',
|
|
744
|
+
' * global guard keeps them restart-safe and hot-reload-safe alike).',
|
|
745
|
+
' */',
|
|
746
|
+
'var db = getModel(' + JSON.stringify(db) + ');',
|
|
747
|
+
'',
|
|
748
|
+
'function SoakController() {',
|
|
749
|
+
' var self = this;',
|
|
750
|
+
'',
|
|
751
|
+
' var STATS = global.__soakStats = global.__soakStats || {',
|
|
752
|
+
' startedAt : Date.now(),',
|
|
753
|
+
' rr : 0,',
|
|
754
|
+
' arms : {',
|
|
755
|
+
' query : { ok: 0, errTransient: 0, errPermanent: 0 },',
|
|
756
|
+
' kv : { ok: 0, errTransient: 0, errPermanent: 0 },',
|
|
757
|
+
' session : { ok: 0, errTransient: 0, errPermanent: 0 }',
|
|
758
|
+
' }',
|
|
759
|
+
' };',
|
|
760
|
+
'',
|
|
761
|
+
' /**',
|
|
762
|
+
' * Books one arm outcome, classified via the #CE1 stamp when present.',
|
|
763
|
+
' * @param {string} arm',
|
|
764
|
+
' * @param {?Error} err',
|
|
765
|
+
' * @returns {void}',
|
|
766
|
+
' * @inner',
|
|
767
|
+
' */',
|
|
768
|
+
' function bump(arm, err) {',
|
|
769
|
+
' var a = STATS.arms[arm];',
|
|
770
|
+
' if (!err) { a.ok++; return; }',
|
|
771
|
+
' if (err.isTransient === true) { a.errTransient++; } else { a.errPermanent++; }',
|
|
772
|
+
' }',
|
|
773
|
+
'',
|
|
774
|
+
' /** @returns {string} rotating worker tag @inner */',
|
|
775
|
+
' function nextWorker() { return \'w\' + (STATS.rr++ % 8); }',
|
|
776
|
+
'',
|
|
777
|
+
' this.soakSetup = function(req, res) {',
|
|
778
|
+
' if (!db || !db.soak) {',
|
|
779
|
+
' return self.renderJSON({ ok: false, error: \'model wiring incomplete: getModel(...)\' +',
|
|
780
|
+
' (db ? \'.soak missing (entities did not load)\' : \' returned nothing\') });',
|
|
781
|
+
' }',
|
|
782
|
+
' var cluster = null;',
|
|
783
|
+
' try { cluster = db.soak.getCluster(); }',
|
|
784
|
+
' catch (e) { return self.renderJSON({ ok: false, error: \'getCluster(): \' + e.message }); }',
|
|
785
|
+
' cluster.query(\'CREATE PRIMARY INDEX IF NOT EXISTS ON ' + bt + db + bt + '\', { adhoc: true })',
|
|
786
|
+
' .then(function () {',
|
|
787
|
+
' return db.soak.kvPromiseCycle(\'soak:setup:probe\', { worker: \'setup\', seq: 1 }, 1);',
|
|
788
|
+
' })',
|
|
789
|
+
' .then(function () { self.renderJSON({ ok: true, indexed: true }); })',
|
|
790
|
+
' .catch(function (e) { self.renderJSON({ ok: false, error: String(e && e.message || e) }); });',
|
|
791
|
+
' };',
|
|
792
|
+
'',
|
|
793
|
+
' this.soakQuery = function(req, res) {',
|
|
794
|
+
' db.soak.findByWorker(nextWorker()).onComplete(function (err, rows) {',
|
|
795
|
+
' bump(\'query\', err);',
|
|
796
|
+
' self.renderJSON({ ok: !err, n: rows ? rows.length : 0 });',
|
|
797
|
+
' });',
|
|
798
|
+
' };',
|
|
799
|
+
'',
|
|
800
|
+
' this.soakQueryRp = function(req, res) {',
|
|
801
|
+
' db.soak.findByWorkerRp(nextWorker()).onComplete(function (err, rows) {',
|
|
802
|
+
' bump(\'query\', err);',
|
|
803
|
+
' self.renderJSON({ ok: !err, n: rows ? rows.length : 0 });',
|
|
804
|
+
' });',
|
|
805
|
+
' };',
|
|
806
|
+
'',
|
|
807
|
+
' this.soakKvCb = function(req, res) {',
|
|
808
|
+
' var seq = STATS.rr++;',
|
|
809
|
+
' var key = \'soak:cb:\' + process.pid + \':\' + seq;',
|
|
810
|
+
' db.soak.kvCbInsert(key, { worker: nextWorker(), seq: seq, via: \'cb\' }, function (err) {',
|
|
811
|
+
' bump(\'kv\', err);',
|
|
812
|
+
' if (err) { return self.renderJSON({ ok: false, error: String(err.message || err) }); }',
|
|
813
|
+
' // follow with a query-path mutation on the same key (UPDATE ... USE KEYS)',
|
|
814
|
+
' db.soak.bumpByKey(key, \'y\').onComplete(function (uErr) {',
|
|
815
|
+
' bump(\'query\', uErr);',
|
|
816
|
+
' self.renderJSON({ ok: !uErr });',
|
|
817
|
+
' });',
|
|
818
|
+
' });',
|
|
819
|
+
' };',
|
|
820
|
+
'',
|
|
821
|
+
' this.soakKvPromise = function(req, res) {',
|
|
822
|
+
' var seq = STATS.rr++;',
|
|
823
|
+
' var key = \'soak:p:\' + process.pid + \':\' + seq;',
|
|
824
|
+
' db.soak.kvPromiseCycle(key, { worker: nextWorker(), seq: seq, via: \'p\' }, seq)',
|
|
825
|
+
' .then(function () { bump(\'kv\', null); self.renderJSON({ ok: true }); })',
|
|
826
|
+
' .catch(function (err) { bump(\'kv\', err); self.renderJSON({ ok: false, error: String(err && err.message || err) }); });',
|
|
827
|
+
' };',
|
|
828
|
+
'',
|
|
829
|
+
' this.soakSession = function(req, res) {',
|
|
830
|
+
' if (!req.session) { return self.renderJSON({ ok: false, error: \'no req.session — store wiring failed\' }); }',
|
|
831
|
+
' req.session.n = (req.session.n || 0) + 1;',
|
|
832
|
+
' bump(\'session\', null);',
|
|
833
|
+
' self.renderJSON({ ok: true, n: req.session.n });',
|
|
834
|
+
' };',
|
|
835
|
+
'',
|
|
836
|
+
' this.soakSessionDestroy = function(req, res) {',
|
|
837
|
+
' if (!req.session) { return self.renderJSON({ ok: false, error: \'no req.session\' }); }',
|
|
838
|
+
' req.session.destroy(function (err) {',
|
|
839
|
+
' bump(\'session\', err || null);',
|
|
840
|
+
' self.renderJSON({ ok: !err });',
|
|
841
|
+
' });',
|
|
842
|
+
' };',
|
|
843
|
+
'',
|
|
844
|
+
' this.soakStats = function(req, res) {',
|
|
845
|
+
' self.renderJSON({ pid: process.pid, mem: process.memoryUsage(), stats: STATS });',
|
|
846
|
+
' };',
|
|
847
|
+
'}',
|
|
848
|
+
'',
|
|
849
|
+
'module.exports = SoakController;',
|
|
850
|
+
''
|
|
851
|
+
].join('\n'));
|
|
852
|
+
|
|
853
|
+
// -- bundle entry: the documented express-session wire-in (guide shape,
|
|
854
|
+
// #B167: the entry must be literally named `session`; the store takes
|
|
855
|
+
// the model layer's already-open bucket as options.db).
|
|
856
|
+
fs.writeFileSync(path.join(ctx.src, 'index.js'), [
|
|
857
|
+
'/**',
|
|
858
|
+
' * Soak bundle entry — generated by script/soak/couchbase-soak.js.',
|
|
859
|
+
' * Wires the couchbase express-session store per the documented shape.',
|
|
860
|
+
' */',
|
|
861
|
+
'var soakApp = require(\'gina\');',
|
|
862
|
+
'var expressSession = require(\'express-session\');',
|
|
863
|
+
'var SessionStore = soakApp.lib.SessionStore;',
|
|
864
|
+
'',
|
|
865
|
+
'soakApp.onInitialize(function (event, app) {',
|
|
866
|
+
' var CouchbaseStore = new SessionStore(expressSession);',
|
|
867
|
+
' app.use(expressSession({',
|
|
868
|
+
' secret : \'soak-\' + process.pid,',
|
|
869
|
+
' resave : false,',
|
|
870
|
+
' saveUninitialized : false,',
|
|
871
|
+
' store : new CouchbaseStore({',
|
|
872
|
+
' db : getModel(\'session\').getConnection(),',
|
|
873
|
+
' prefix : \'soak:sess:\'',
|
|
874
|
+
' }),',
|
|
875
|
+
' cookie : { maxAge: 300000 }',
|
|
876
|
+
' }));',
|
|
877
|
+
' event.emit(\'complete\', app);',
|
|
878
|
+
'});',
|
|
879
|
+
'',
|
|
880
|
+
'soakApp.onError(function (err, req, res, next) { next(err); });',
|
|
881
|
+
'',
|
|
882
|
+
'soakApp.start();',
|
|
883
|
+
''
|
|
884
|
+
].join('\n'));
|
|
885
|
+
|
|
886
|
+
log('fixture applied (connectors + entity + n1ql + routes + session wire-in)');
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Builds the prod release (fixture config is baked into the release tree).
|
|
891
|
+
* @param {object} ctx
|
|
892
|
+
* @returns {void} throws when no prod entry point was produced
|
|
893
|
+
*/
|
|
894
|
+
function build(ctx) {
|
|
895
|
+
runCli(ctx, ['bundle:build', ctx.bundle, '@' + ctx.proj, '--env=prod', '--scope=local']);
|
|
896
|
+
var relRoot = path.join(ctx.projDir, 'releases', ctx.bundle, 'local', 'prod');
|
|
897
|
+
var built = fs.existsSync(relRoot) && fs.readdirSync(relRoot).some(function (v) {
|
|
898
|
+
return fs.existsSync(path.join(relRoot, v, 'index.js'));
|
|
899
|
+
});
|
|
900
|
+
if (!built) { throw new Error('bundle:build produced no prod release under ' + relRoot); }
|
|
901
|
+
log('prod release built');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* @param {object} ctx
|
|
906
|
+
* @returns {number} the prod http/1.1 port from ports.reverse.json
|
|
907
|
+
*/
|
|
908
|
+
function resolvePort(ctx) {
|
|
909
|
+
var key = ctx.bundle + '@' + ctx.proj;
|
|
910
|
+
var port = null;
|
|
911
|
+
try { port = readJSON(path.join(ctx.ginaHome, 'ports.reverse.json'))[key].prod['http/1.1'].http; } catch (e) { /* fall through */ }
|
|
912
|
+
if (!port) { throw new Error('could not resolve the prod http/1.1 port for ' + key); }
|
|
913
|
+
return port;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* @param {object} ctx
|
|
918
|
+
* @param {number} port
|
|
919
|
+
* @param {number} timeoutMs
|
|
920
|
+
* @returns {Promise<boolean>}
|
|
921
|
+
*/
|
|
922
|
+
function waitForPort(ctx, port, timeoutMs) {
|
|
923
|
+
var deadline = Date.now() + timeoutMs;
|
|
924
|
+
return new Promise(function (resolve) {
|
|
925
|
+
(function poll() {
|
|
926
|
+
dial('127.0.0.1', port, 800).then(function (up) {
|
|
927
|
+
if (up) { return resolve(true); }
|
|
928
|
+
if (Date.now() >= deadline) { return resolve(false); }
|
|
929
|
+
setTimeout(poll, 300);
|
|
930
|
+
});
|
|
931
|
+
})();
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Boots the bundle (prod, daemonless) and records launcher exit + the bundle
|
|
937
|
+
* child's pid from the `[ FRAMEWORK ][ <pid> ]` mount line. Asserts via the
|
|
938
|
+
* launcher's own `App:` line that the boot really is the prod release.
|
|
939
|
+
*
|
|
940
|
+
* @param {object} ctx
|
|
941
|
+
* @param {number} port
|
|
942
|
+
* @returns {Promise<object>} boot handle {proc, logRef, exit, bundlePid, bootedAt}
|
|
943
|
+
*/
|
|
944
|
+
async function boot(ctx, port) {
|
|
945
|
+
var env = Object.assign({}, ctx.childEnv, { NODE_ENV: 'prod' });
|
|
946
|
+
var handle = { logRef: { txt: '' }, exit: { code: null, signal: null, done: false, atMs: null }, bundlePid: null };
|
|
947
|
+
handle.bootedAt = Date.now();
|
|
948
|
+
handle.proc = spawn(process.execPath, [ctx.container, ctx.bundle, '@' + ctx.proj], {
|
|
949
|
+
env: env, cwd: ctx.cwdDir, stdio: ['ignore', 'pipe', 'pipe']
|
|
950
|
+
});
|
|
951
|
+
handle.proc.stdout.on('data', function (d) { handle.logRef.txt += d; });
|
|
952
|
+
handle.proc.stderr.on('data', function (d) { handle.logRef.txt += d; });
|
|
953
|
+
handle.proc.on('exit', function (code, signal) {
|
|
954
|
+
handle.exit = { code: code, signal: signal, done: true, atMs: Date.now() - handle.bootedAt };
|
|
955
|
+
});
|
|
956
|
+
var opened = await waitForPort(ctx, port, 45000);
|
|
957
|
+
if (!opened) {
|
|
958
|
+
var note = handle.exit.done
|
|
959
|
+
? 'launcher EXITED (code ' + handle.exit.code + ', signal ' + handle.exit.signal + ') before binding'
|
|
960
|
+
: 'launcher still running at deadline — a stalled boot (cluster unreachable? credentials?)';
|
|
961
|
+
throw new Error('port ' + port + ' never opened. ' + note + '\n--- boot log tail:\n' + handle.logRef.txt.slice(-3000));
|
|
962
|
+
}
|
|
963
|
+
var pm = handle.logRef.txt.match(/\[ FRAMEWORK \]\[ (\d+) \]/);
|
|
964
|
+
handle.bundlePid = pm ? parseInt(pm[1], 10) : null;
|
|
965
|
+
var appLine = (handle.logRef.txt.match(/\[ gina-container \] App:.*$/m) || [''])[0];
|
|
966
|
+
if (appLine && appLine.indexOf('/releases/') === -1) {
|
|
967
|
+
throw new Error('boot is NOT the prod release (App: line = ' + appLine + ')');
|
|
968
|
+
}
|
|
969
|
+
log('booted (port ' + port + ', bundle pid ' + (handle.bundlePid || '?') + ')');
|
|
970
|
+
return handle;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* One HTTP GET/POST against the bundle, with a hard timeout.
|
|
975
|
+
*
|
|
976
|
+
* @param {object} agent - shared keep-alive agent
|
|
977
|
+
* @param {number} port
|
|
978
|
+
* @param {string} reqPath
|
|
979
|
+
* @param {object} [o] - {method, cookies: string[], timeoutMs}
|
|
980
|
+
* @returns {Promise<{status: number, body: string, setCookie: string[]}>} rejects on transport error/timeout
|
|
981
|
+
*/
|
|
982
|
+
function request(agent, port, reqPath, o) {
|
|
983
|
+
o = o || {};
|
|
984
|
+
return new Promise(function (resolve, reject) {
|
|
985
|
+
var req = http.request({
|
|
986
|
+
agent: agent, host: '127.0.0.1', port: port, path: reqPath,
|
|
987
|
+
method: o.method || 'GET',
|
|
988
|
+
headers: (o.cookies && o.cookies.length) ? { Cookie: o.cookies.join('; ') } : {}
|
|
989
|
+
}, function (res) {
|
|
990
|
+
var chunks = [];
|
|
991
|
+
res.on('data', function (c) { chunks.push(c); });
|
|
992
|
+
res.on('end', function () {
|
|
993
|
+
resolve({
|
|
994
|
+
status: res.statusCode,
|
|
995
|
+
body: Buffer.concat(chunks).toString(),
|
|
996
|
+
setCookie: res.headers['set-cookie'] || []
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
req.setTimeout(o.timeoutMs || 30000, function () { req.destroy(new Error('request timeout')); });
|
|
1001
|
+
req.on('error', reject);
|
|
1002
|
+
req.end();
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Runs the in-bundle setup route (primary index + KV probe) and fails loudly
|
|
1008
|
+
* on anything but `{ok: true}`.
|
|
1009
|
+
*
|
|
1010
|
+
* @param {object} ctx
|
|
1011
|
+
* @param {object} opts
|
|
1012
|
+
* @param {object} agent
|
|
1013
|
+
* @param {number} port
|
|
1014
|
+
* @returns {Promise<void>}
|
|
1015
|
+
*/
|
|
1016
|
+
async function setupTarget(ctx, opts, agent, port) {
|
|
1017
|
+
var webroot = '/' + ctx.bundle;
|
|
1018
|
+
var r = await request(agent, port, webroot + '/soak/setup', { method: 'POST', timeoutMs: opts.setupTimeoutMs });
|
|
1019
|
+
var payload = null;
|
|
1020
|
+
try { payload = JSON.parse(r.body); } catch (e) { /* handled below */ }
|
|
1021
|
+
if (!payload || payload.ok !== true) {
|
|
1022
|
+
throw new Error('in-bundle setup failed (HTTP ' + r.status + '): '
|
|
1023
|
+
+ (payload && payload.error ? payload.error : r.body.slice(0, 500))
|
|
1024
|
+
+ '\nCheck: bucket exists, credentials can write it, query service up, primary index creatable.');
|
|
1025
|
+
}
|
|
1026
|
+
log('setup ok (primary index ensured + KV probe round-tripped)');
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Drives the load for the full duration and samples /soak/stats, composing
|
|
1031
|
+
* server counters + driver transport counters into evaluator-shaped samples.
|
|
1032
|
+
*
|
|
1033
|
+
* @param {object} ctx
|
|
1034
|
+
* @param {object} opts
|
|
1035
|
+
* @param {object} handle - boot handle
|
|
1036
|
+
* @param {number} port
|
|
1037
|
+
* @returns {Promise<{samples: Array, endedEarly: ?object}>}
|
|
1038
|
+
*/
|
|
1039
|
+
async function drive(ctx, opts, handle, port) {
|
|
1040
|
+
var webroot = '/' + ctx.bundle;
|
|
1041
|
+
var agent = new http.Agent({ keepAlive: true, maxSockets: opts.concurrency + 4 });
|
|
1042
|
+
var stopFlag = { stop: false };
|
|
1043
|
+
var startMs = Date.now();
|
|
1044
|
+
var plannedEnd = startMs + opts.durationMs;
|
|
1045
|
+
var driver = { // transport-level outcomes per arm (server can't see these)
|
|
1046
|
+
query : { sent: 0, http200: 0, httpOther: 0, transport: 0 },
|
|
1047
|
+
kv : { sent: 0, http200: 0, httpOther: 0, transport: 0 },
|
|
1048
|
+
session : { sent: 0, http200: 0, httpOther: 0, transport: 0 }
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* @param {string} arm @param {number} idx @returns {Promise<void>} @inner
|
|
1053
|
+
*/
|
|
1054
|
+
async function worker(arm, idx) {
|
|
1055
|
+
var seq = 0;
|
|
1056
|
+
var jar = [];
|
|
1057
|
+
while (!stopFlag.stop) {
|
|
1058
|
+
var reqPath;
|
|
1059
|
+
var o = {};
|
|
1060
|
+
if (arm === 'query') {
|
|
1061
|
+
reqPath = (seq % 3 === 2) ? '/soak/query-rp' : '/soak/query';
|
|
1062
|
+
} else if (arm === 'kv') {
|
|
1063
|
+
reqPath = (seq % 2 === 0) ? '/soak/kv-cb' : '/soak/kv-p';
|
|
1064
|
+
} else {
|
|
1065
|
+
if (seq > 0 && seq % 25 === 0) { jar = []; } // fresh identity
|
|
1066
|
+
reqPath = (seq > 0 && seq % 10 === 0) ? '/soak/session-destroy' : '/soak/session';
|
|
1067
|
+
o.cookies = jar;
|
|
1068
|
+
}
|
|
1069
|
+
driver[arm].sent++;
|
|
1070
|
+
try {
|
|
1071
|
+
var r = await request(agent, port, webroot + reqPath, o);
|
|
1072
|
+
if (arm === 'session' && r.setCookie.length) {
|
|
1073
|
+
jar = r.setCookie.map(function (c) { return c.split(';')[0]; });
|
|
1074
|
+
}
|
|
1075
|
+
if (r.status === 200) { driver[arm].http200++; } else { driver[arm].httpOther++; }
|
|
1076
|
+
} catch (e) {
|
|
1077
|
+
driver[arm].transport++;
|
|
1078
|
+
await sleep(250); // do not spin on a dead socket
|
|
1079
|
+
}
|
|
1080
|
+
seq++;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
var samples = [];
|
|
1085
|
+
var ndjson = fs.createWriteStream(path.join(ctx.out, 'samples.ndjson'));
|
|
1086
|
+
|
|
1087
|
+
/** @returns {Promise<void>} one stats sample @inner */
|
|
1088
|
+
async function sampleOnce() {
|
|
1089
|
+
try {
|
|
1090
|
+
var r = await request(agent, port, webroot + '/soak/stats', { timeoutMs: 4000 });
|
|
1091
|
+
var p = JSON.parse(r.body);
|
|
1092
|
+
var s = { tMs: Date.now() - startMs, rssBytes: p.mem.rss, arms: {} };
|
|
1093
|
+
for (var a = 0; a < opts.arms.length; a++) {
|
|
1094
|
+
var arm = opts.arms[a];
|
|
1095
|
+
var srv = (p.stats.arms || {})[arm] || {};
|
|
1096
|
+
s.arms[arm] = {
|
|
1097
|
+
ok : srv.ok || 0,
|
|
1098
|
+
errTransient : srv.errTransient || 0,
|
|
1099
|
+
errPermanent : srv.errPermanent || 0,
|
|
1100
|
+
transport : driver[arm].transport + driver[arm].httpOther
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
// session `ok` is driver-observed (store failures surface as request
|
|
1104
|
+
// failures, not server counters — count successes from the wire).
|
|
1105
|
+
if (s.arms.session) { s.arms.session.ok = driver.session.http200; }
|
|
1106
|
+
samples.push(s);
|
|
1107
|
+
ndjson.write(JSON.stringify(s) + '\n');
|
|
1108
|
+
} catch (e) { /* liveness is judged below, not by one missed sample */ }
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
var workers = [];
|
|
1112
|
+
for (var w = 0; w < opts.concurrency; w++) {
|
|
1113
|
+
workers.push(worker(opts.arms[w % opts.arms.length], w));
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
var endedEarly = null;
|
|
1117
|
+
while (Date.now() < plannedEnd) {
|
|
1118
|
+
await sleep(Math.min(opts.sampleIntervalMs, plannedEnd - Date.now()));
|
|
1119
|
+
await sampleOnce();
|
|
1120
|
+
var dead = handle.exit.done
|
|
1121
|
+
|| (handle.bundlePid && !isAlive(handle.bundlePid));
|
|
1122
|
+
if (dead) {
|
|
1123
|
+
endedEarly = {
|
|
1124
|
+
atMs : handle.exit.done ? handle.exit.atMs : (Date.now() - handle.bootedAt),
|
|
1125
|
+
exit : handle.exit.done ? { code: handle.exit.code, signal: handle.exit.signal }
|
|
1126
|
+
: { code: null, signal: null }
|
|
1127
|
+
};
|
|
1128
|
+
log('bundle process ENDED EARLY — stopping the drive');
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
var elapsed = Math.round((Date.now() - startMs) / 1000);
|
|
1132
|
+
if (elapsed % 30 < opts.sampleIntervalMs / 1000) {
|
|
1133
|
+
log('t+' + elapsed + 's — ' + JSON.stringify(samples.length ? samples[samples.length - 1].arms : {}));
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
stopFlag.stop = true;
|
|
1137
|
+
await Promise.race([Promise.all(workers), sleep(8000)]);
|
|
1138
|
+
await sampleOnce(); // final counters
|
|
1139
|
+
ndjson.end();
|
|
1140
|
+
agent.destroy();
|
|
1141
|
+
return { samples: samples, endedEarly: endedEarly, driver: driver };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* @param {number} pid
|
|
1146
|
+
* @returns {boolean} whether the pid is signalable
|
|
1147
|
+
*/
|
|
1148
|
+
function isAlive(pid) {
|
|
1149
|
+
try { process.kill(pid, 0); return true; } catch (e) { return false; }
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* SIGTERM-drains the boot; escalates to SIGKILL after a grace period — and
|
|
1154
|
+
* then ALSO signals the bundle pid (gina-container forwards SIGTERM only; a
|
|
1155
|
+
* SIGKILLed launcher orphans the bundle child).
|
|
1156
|
+
*
|
|
1157
|
+
* @param {object} handle
|
|
1158
|
+
* @returns {Promise<void>}
|
|
1159
|
+
*/
|
|
1160
|
+
async function drain(handle) {
|
|
1161
|
+
try { handle.proc.kill('SIGTERM'); } catch (e) { /* already gone */ }
|
|
1162
|
+
var until = Date.now() + 12000;
|
|
1163
|
+
while (Date.now() < until && !handle.exit.done) { await sleep(200); }
|
|
1164
|
+
if (!handle.exit.done) {
|
|
1165
|
+
try { handle.proc.kill('SIGKILL'); } catch (e) { /* ignore */ }
|
|
1166
|
+
if (handle.bundlePid && isAlive(handle.bundlePid)) {
|
|
1167
|
+
try { process.kill(handle.bundlePid, 'SIGTERM'); } catch (e) { /* ignore */ }
|
|
1168
|
+
await sleep(1500);
|
|
1169
|
+
if (isAlive(handle.bundlePid)) {
|
|
1170
|
+
try { process.kill(handle.bundlePid, 'SIGKILL'); } catch (e) { /* ignore */ }
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Best-effort teardown: unregister + remove the throwaway home, hunt THIS
|
|
1178
|
+
* run's surviving processes, verify the real ~/.gina is untouched, and clear
|
|
1179
|
+
* stray CLI self-symlinks at the repo root (both the `gina` name and the
|
|
1180
|
+
* install-dir basename variant).
|
|
1181
|
+
*
|
|
1182
|
+
* @param {object} ctx
|
|
1183
|
+
* @param {object} opts
|
|
1184
|
+
* @returns {string[]} human-readable warnings (empty = clean)
|
|
1185
|
+
*/
|
|
1186
|
+
function teardown(ctx, opts) {
|
|
1187
|
+
var warnings = [];
|
|
1188
|
+
if (!opts.keep) {
|
|
1189
|
+
try { runCli(ctx, ['project:rm', '@' + ctx.proj, '--force']); } catch (e) { /* best effort */ }
|
|
1190
|
+
try { fs.rmSync(ctx.fakeHome, { recursive: true, force: true }); } catch (e) { warnings.push('could not remove ' + ctx.fakeHome + ': ' + e.message); }
|
|
1191
|
+
try { fs.rmSync(ctx.cwdDir, { recursive: true, force: true }); } catch (e) { /* ignore */ }
|
|
1192
|
+
} else {
|
|
1193
|
+
warnings.push('--keep: left ' + ctx.fakeHome + ' and the @' + ctx.proj + ' registration in place');
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
var ps = spawnSync('ps', ['-eo', 'pid,command'], { encoding: 'utf8' }).stdout || '';
|
|
1197
|
+
ps.split('\n').forEach(function (line) {
|
|
1198
|
+
if (line.indexOf('@' + ctx.proj) === -1) { return; }
|
|
1199
|
+
var pid = parseInt(line.trim().split(/\s+/)[0], 10);
|
|
1200
|
+
if (pid && pid !== process.pid) {
|
|
1201
|
+
try { process.kill(pid, 'SIGTERM'); warnings.push('killed surviving process ' + pid); } catch (e) { /* gone */ }
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
} catch (e) { /* ps unavailable */ }
|
|
1205
|
+
try {
|
|
1206
|
+
var realProjects = path.join(os.homedir(), '.gina', 'projects.json');
|
|
1207
|
+
if (fs.existsSync(realProjects) && readJSON(realProjects)[ctx.proj]) {
|
|
1208
|
+
warnings.push('LEAK: @' + ctx.proj + ' registered in the REAL ~/.gina/projects.json — remove it');
|
|
1209
|
+
}
|
|
1210
|
+
} catch (e) { /* ignore */ }
|
|
1211
|
+
try {
|
|
1212
|
+
var realDot = path.join(os.homedir(), '.' + ctx.proj);
|
|
1213
|
+
if (fs.existsSync(realDot)) {
|
|
1214
|
+
if (!fs.readdirSync(realDot).length) { fs.rmdirSync(realDot); }
|
|
1215
|
+
else { warnings.push('LEAK: non-empty ' + realDot + ' on the real home'); }
|
|
1216
|
+
}
|
|
1217
|
+
} catch (e) { /* ignore */ }
|
|
1218
|
+
[path.join(ctx.ginaRoot, 'gina'), path.join(ctx.ginaRoot, path.basename(ctx.ginaRoot))].forEach(function (stray) {
|
|
1219
|
+
try {
|
|
1220
|
+
if (fs.existsSync(stray) && fs.lstatSync(stray).isSymbolicLink()) {
|
|
1221
|
+
fs.unlinkSync(stray);
|
|
1222
|
+
warnings.push('removed a stray self-symlink at ' + stray);
|
|
1223
|
+
}
|
|
1224
|
+
} catch (e) { /* ignore */ }
|
|
1225
|
+
});
|
|
1226
|
+
return warnings;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Entry point: setup → soak → verdict → report.
|
|
1231
|
+
* @returns {Promise<void>} process.exit()s with 0/1/2
|
|
1232
|
+
*/
|
|
1233
|
+
async function main() {
|
|
1234
|
+
var parsed = parseArgs(process.argv.slice(2));
|
|
1235
|
+
if (!parsed.ok) {
|
|
1236
|
+
process.stderr.write('couchbase-soak: ' + parsed.error + '\n(see the header of script/soak/couchbase-soak.js for usage)\n');
|
|
1237
|
+
process.exit(2);
|
|
1238
|
+
}
|
|
1239
|
+
var opts = parsed.opts;
|
|
1240
|
+
var ctx = buildRunContext(opts);
|
|
1241
|
+
fs.mkdirSync(ctx.out, { recursive: true });
|
|
1242
|
+
log('output dir: ' + ctx.out);
|
|
1243
|
+
log('arms: ' + opts.arms.join(',') + ' · duration: ' + Math.round(opts.durationMs / 1000) + 's · concurrency: ' + opts.concurrency);
|
|
1244
|
+
|
|
1245
|
+
if (!opts.skipPreflight) {
|
|
1246
|
+
var unreachable = await preflight(opts);
|
|
1247
|
+
if (unreachable) {
|
|
1248
|
+
process.stderr.write('couchbase-soak: preflight failed — ' + unreachable + ' (use --skip-preflight to bypass)\n');
|
|
1249
|
+
process.exit(2);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
var handle = null;
|
|
1254
|
+
var result = null;
|
|
1255
|
+
var verdict = null;
|
|
1256
|
+
var report = { stamp: ctx.stamp, node: process.version, opts: Object.assign({}, opts, { password: '(redacted)' }) };
|
|
1257
|
+
|
|
1258
|
+
try {
|
|
1259
|
+
scaffold(ctx, opts);
|
|
1260
|
+
installDeps(ctx, opts);
|
|
1261
|
+
fixture(ctx, opts);
|
|
1262
|
+
build(ctx);
|
|
1263
|
+
var port = resolvePort(ctx);
|
|
1264
|
+
handle = await boot(ctx, port);
|
|
1265
|
+
var agent = new http.Agent({ keepAlive: true });
|
|
1266
|
+
await setupTarget(ctx, opts, agent, port);
|
|
1267
|
+
agent.destroy();
|
|
1268
|
+
|
|
1269
|
+
log('driving load for ' + Math.round(opts.durationMs / 1000) + 's ...');
|
|
1270
|
+
result = await drive(ctx, opts, handle, port);
|
|
1271
|
+
} catch (e) {
|
|
1272
|
+
if (handle) { await drain(handle); }
|
|
1273
|
+
if (handle && handle.logRef) {
|
|
1274
|
+
try { fs.writeFileSync(path.join(ctx.out, 'boot.log'), handle.logRef.txt); } catch (w) { /* ignore */ }
|
|
1275
|
+
}
|
|
1276
|
+
var warningsE = teardown(ctx, opts);
|
|
1277
|
+
warningsE.forEach(function (w) { log('teardown: ' + w); });
|
|
1278
|
+
process.stderr.write('couchbase-soak: SETUP/RUN error — ' + (e && e.stack || e) + '\n');
|
|
1279
|
+
process.exit(2);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
await drain(handle);
|
|
1283
|
+
try { fs.writeFileSync(path.join(ctx.out, 'boot.log'), handle.logRef.txt); } catch (w) { /* ignore */ }
|
|
1284
|
+
|
|
1285
|
+
verdict = evaluateRun({
|
|
1286
|
+
plannedMs : opts.durationMs,
|
|
1287
|
+
endedEarly : result.endedEarly,
|
|
1288
|
+
samples : result.samples,
|
|
1289
|
+
armsRequested : opts.arms,
|
|
1290
|
+
thresholds : opts.thresholds
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
report.sdkVersion = ctx.sdkVersion;
|
|
1294
|
+
report.driver = result.driver;
|
|
1295
|
+
report.verdict = verdict;
|
|
1296
|
+
fs.writeFileSync(path.join(ctx.out, 'report.json'), JSON.stringify(report, null, 2));
|
|
1297
|
+
|
|
1298
|
+
var lines = [
|
|
1299
|
+
'# couchbase-soak ' + ctx.stamp,
|
|
1300
|
+
'node ' + process.version + ' · couchbase@' + ctx.sdkVersion + ' · target ' + opts.protocol + opts.host + '/' + opts.database,
|
|
1301
|
+
'planned ' + Math.round(opts.durationMs / 1000) + 's · arms ' + opts.arms.join(',') + ' · concurrency ' + opts.concurrency,
|
|
1302
|
+
''
|
|
1303
|
+
];
|
|
1304
|
+
verdict.warnings.forEach(function (w) { lines.push('WARN: ' + w); });
|
|
1305
|
+
verdict.failures.forEach(function (f) { lines.push('FAIL [' + f.check + ']: ' + f.detail); });
|
|
1306
|
+
lines.push('', 'metrics: ' + JSON.stringify(verdict.metrics), '', verdict.pass ? 'VERDICT: PASS' : 'VERDICT: FAIL');
|
|
1307
|
+
fs.writeFileSync(path.join(ctx.out, 'report.txt'), lines.join('\n'));
|
|
1308
|
+
lines.forEach(function (l) { if (l) { log(l); } });
|
|
1309
|
+
|
|
1310
|
+
var warnings = teardown(ctx, opts);
|
|
1311
|
+
warnings.forEach(function (w) { log('teardown: ' + w); });
|
|
1312
|
+
log('report: ' + path.join(ctx.out, 'report.txt'));
|
|
1313
|
+
process.exit(verdict.pass ? 0 : 1);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (require.main === module) { main(); }
|