From 82326557d66fa84172263e96e57bfed289c50cab Mon Sep 17 00:00:00 2001 From: Arthurvroum Date: Mon, 30 Mar 2026 08:09:10 +0200 Subject: [PATCH 01/11] feat: implement automatic binary updater from github releases --- .gitignore | 2 +- package-lock.json | 374 ++++++++++++++++++++++++++++++++++++ package.json | 5 + src/ctrace/BinaryUpdater.ts | 172 +++++++++++++++++ src/extension.ts | 15 +- 5 files changed, 562 insertions(+), 6 deletions(-) create mode 100644 src/ctrace/BinaryUpdater.ts diff --git a/.gitignore b/.gitignore index 616fcd0..08012c4 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,4 @@ ccoretrace-sarif-tscancode.json .DS_Store Thumbs.db -# test \ No newline at end of file +# test.vscode-test/ diff --git a/package-lock.json b/package-lock.json index 6b988e8..f255c34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,14 @@ "": { "name": "ctrace-audit", "version": "0.0.1", + "dependencies": { + "axios": "^1.13.6", + "tar": "^6.2.1" + }, "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "^18.19.130", + "@types/tar": "^6.1.13", "@types/vscode": "^1.80.0", "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", @@ -149,6 +154,25 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "minipass": "^4.0.0" + } + }, + "node_modules/@types/tar/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@types/vscode": { "version": "1.109.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz", @@ -279,6 +303,21 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -357,6 +396,18 @@ } } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -421,6 +472,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -538,6 +597,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -599,6 +669,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/diff": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", @@ -608,6 +686,19 @@ "node": ">=0.3.1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -633,6 +724,47 @@ "node": ">=10.13.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -691,6 +823,25 @@ "flat": "cli.js" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -707,6 +858,43 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -721,6 +909,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -742,6 +938,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -775,6 +1006,17 @@ "node": ">= 6" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -790,6 +1032,42 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1108,6 +1386,33 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -1144,6 +1449,40 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mocha": { "version": "11.7.5", "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", @@ -1451,6 +1790,11 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1732,6 +2076,31 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/test-exclude": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", @@ -2019,6 +2388,11 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 40f9c15..dc79354 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "^18.19.130", + "@types/tar": "^6.1.13", "@types/vscode": "^1.80.0", "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", @@ -63,5 +64,9 @@ "mocha": "^11.7.5", "ts-node": "^10.9.2", "typescript": "^5.1.3" + }, + "dependencies": { + "axios": "^1.13.6", + "tar": "^6.2.1" } } diff --git a/src/ctrace/BinaryUpdater.ts b/src/ctrace/BinaryUpdater.ts new file mode 100644 index 0000000..5583eab --- /dev/null +++ b/src/ctrace/BinaryUpdater.ts @@ -0,0 +1,172 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import axios from 'axios'; +import * as tar from 'tar'; +import { locateBinary } from './BinaryLocator'; + +const REPO_LATEST_RELEASE_URL = 'https://api.github.com/repos/CoreTrace/coretrace/releases/latest'; + +export async function ensureBinary(context: vscode.ExtensionContext, output: vscode.OutputChannel): Promise { + const globalStorage = context.globalStorageUri.fsPath; + const binDir = path.join(globalStorage, 'bin'); + + if (!fs.existsSync(binDir)) { + fs.mkdirSync(binDir, { recursive: true }); + } + + const downloadedBinaryPath = await getExtractedBinaryPath(binDir); + const lastCheck = context.globalState.get('coretrace-last-update-check') || 0; + const now = Date.now(); + const TWELVE_HOURS = 12 * 60 * 60 * 1000; + + // Si on a déjà vérifié récemment, on ne spamme pas l'API GitHub. + // On retourne le binaire téléchargé s'il existe, sinon on tente de replier sur le binaire packagé. + if (now - lastCheck < TWELVE_HOURS) { + if (downloadedBinaryPath) return downloadedBinaryPath; + return await locateBinary(context.extensionUri.fsPath); + } + + try { + const response = await axios.get(REPO_LATEST_RELEASE_URL, { + headers: { 'User-Agent': 'vscode-coretrace' }, + timeout: 5000 // Don't block forever + }); + const release = response.data; + const latestVersion = release.tag_name; + + context.globalState.update('coretrace-last-update-check', now); + + const currentVersion = context.globalState.get('coretrace-version'); + const downloadedBinaryPath = await getExtractedBinaryPath(binDir); + + if (latestVersion !== currentVersion || !downloadedBinaryPath) { + const assetInfo = getAssetForPlatform(release.assets); + if (assetInfo) { + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: `Downloading CoreTrace ${latestVersion}...`, + cancellable: false + }, async (progress) => { + output.appendLine(`Downloading CoreTrace release ${latestVersion} from GitHub...`); + await downloadAndExtract(assetInfo.url, binDir, progress); + context.globalState.update('coretrace-version', latestVersion); + output.appendLine(`Updated CoreTrace to ${latestVersion} successfully.`); + }); + } else { + output.appendLine(`No GitHub release asset found for platform ${process.platform} arch ${process.arch}`); + } + } + + const bin = await getExtractedBinaryPath(binDir); + if (bin) return bin; + + } catch (err: any) { + output.appendLine(`Failed to check for CoreTrace updates: ${err.message}`); + } + + // Try to return the previously downloaded binary first, even if update check failed + const cachedBin = await getExtractedBinaryPath(binDir); + if (cachedBin) return cachedBin; + + // Fallback to bundled + return await locateBinary(context.extensionUri.fsPath); +} + +function getAssetForPlatform(assets: any[]): { name: string; url: string } | null { + const osMap: Record = { + 'win32': 'windows', + 'linux': 'linux', + 'darwin': 'darwin' + }; + const archMap: Record = { + 'x64': 'amd64', + 'arm64': 'arm64' + }; + + const os = osMap[process.platform]; + const arch = archMap[process.arch]; + if (!os || !arch) return null; + + // First try exact matches (os + arch + .tar.gz) + for (const asset of assets) { + const name = asset.name.toLowerCase(); + if ((name.includes(os) || (process.platform === 'darwin' && name.includes('macos'))) && + name.includes(arch) && + name.endsWith('.tar.gz')) { + return { name: asset.name, url: asset.browser_download_url }; + } + } + // Fallback for older formats (os + .tar.gz) + for (const asset of assets) { + const name = asset.name.toLowerCase(); + if ((name.includes(os) || (process.platform === 'darwin' && name.includes('macos'))) && + name.endsWith('.tar.gz')) { + return { name: asset.name, url: asset.browser_download_url }; + } + } + return null; +} + +async function downloadAndExtract(url: string, destDir: string, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise { + const tarballPath = path.join(destDir, 'download.tar.gz'); + + const token = process.env.GITHUB_TOKEN || ''; + const headers: any = { 'User-Agent': 'vscode-coretrace' }; + if (token) headers['Authorization'] = `token ${token}`; + + const response = await axios({ + url, + method: 'GET', + responseType: 'stream', + headers + }); + + const totalLength = parseInt(response.headers['content-length'], 10); + let downloadedLength = 0; + + const writer = fs.createWriteStream(tarballPath); + + response.data.on('data', (chunk: Buffer) => { + downloadedLength += chunk.length; + if (totalLength) { + const percent = Math.round((downloadedLength / totalLength) * 100); + progress.report({ message: `${percent}%`, increment: (chunk.length / totalLength) * 100 }); + } + }); + + response.data.pipe(writer); + + await new Promise((resolve, reject) => { + writer.on('finish', resolve); + writer.on('error', reject); + }); + + // Extract + await tar.x({ + file: tarballPath, + C: destDir, + strip: 1 // Sometimes they bundle it under a folder like coretrace-v0.73.1-linux-amd64/ctrace. We strip first dir. + }); + + fs.unlinkSync(tarballPath); + + // Make extracted files executable if on linux/mac + if (process.platform !== 'win32') { + const binPath = await getExtractedBinaryPath(destDir); + if (binPath) { + fs.chmodSync(binPath, 0o755); + } + } +} + +async function getExtractedBinaryPath(binDir: string): Promise { + const candidates = ['ctrace', 'coretrace', 'ctrace.exe', 'coretrace.exe']; + for (const name of candidates) { + const file = path.join(binDir, name); + if (fs.existsSync(file)) { + return file; + } + } + return null; +} diff --git a/src/extension.ts b/src/extension.ts index 497ae16..f6686d4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { SidebarProvider, type HostMessage } from './SidebarProvider'; -import { locateBinary } from './ctrace/BinaryLocator'; +import { ensureBinary } from './ctrace/BinaryUpdater'; import { buildCommand, parseAndValidateParams } from './ctrace/CommandBuilder'; import { runCommand } from './ctrace/AnalysisRunner'; import { parseSarifOutput, countResults } from './ctrace/SarifParser'; @@ -22,6 +22,11 @@ export function activate(context: vscode.ExtensionContext) { const output = vscode.window.createOutputChannel('Ctrace'); context.subscriptions.push(output); + // Initialise and pre-fetch the binary in the background on startup + ensureBinary(context, output).catch((err) => { + output.appendLine('Failed to pre-fetch binary on activation: ' + err); + }); + // ── Sidebar ────────────────────────────────────────────────────────────── const sidebarProvider = new SidebarProvider(context.extensionUri); // Register the provider itself as a Disposable so its view-scoped @@ -44,10 +49,10 @@ export function activate(context: vscode.ExtensionContext) { // ── Shared helpers ─────────────────────────────────────────────────────── async function locateOrError(): Promise { - const p = await locateBinary(context.extensionUri.fsPath); + const p = await ensureBinary(context, output); if (!p) { vscode.window.showErrorMessage( - `Ctrace binary not found in extension folder: ${context.extensionUri.fsPath}` + `Ctrace binary could not be found or downloaded.` ); } return p; @@ -316,10 +321,10 @@ export function activate(context: vscode.ExtensionContext) { } // Locate binary - const ctracePath = await locateBinary(context.extensionUri.fsPath); + const ctracePath = await ensureBinary(context, output); if (!ctracePath) { vscode.window.showErrorMessage( - `Ctrace binary not found in extension folder: ${context.extensionUri.fsPath}` + `Ctrace binary could not be found or downloaded.` ); sidebarProvider.postMessage({ type: 'analysis-error' }); isRunning = false; From ae6295011fcb885189a4a8948c2d86893b49e6fe Mon Sep 17 00:00:00 2001 From: Arthurvroum Date: Mon, 30 Mar 2026 08:17:27 +0200 Subject: [PATCH 02/11] fix: correctly locate binary inside extracted tarball 'bin/' folder --- src/ctrace/BinaryUpdater.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/ctrace/BinaryUpdater.ts b/src/ctrace/BinaryUpdater.ts index 5583eab..9d3a448 100644 --- a/src/ctrace/BinaryUpdater.ts +++ b/src/ctrace/BinaryUpdater.ts @@ -163,10 +163,18 @@ async function downloadAndExtract(url: string, destDir: string, progress: vscode async function getExtractedBinaryPath(binDir: string): Promise { const candidates = ['ctrace', 'coretrace', 'ctrace.exe', 'coretrace.exe']; for (const name of candidates) { - const file = path.join(binDir, name); - if (fs.existsSync(file)) { - return file; - } + // Tarball structure is often: coretrace-vX.Y.Z-arch/bin/ctrace + // With strip: 1, it becomes: binDir/bin/ctrace + const fileInBin = path.join(binDir, 'bin', name); + if (fs.existsSync(fileInBin)) { + return fileInBin; + } + + // Fallback for flat structure + const file = path.join(binDir, name); + if (fs.existsSync(file)) { + return file; + } } return null; } From 6f033721d8a2f7dc37efd71c4052517587db85a8 Mon Sep 17 00:00:00 2001 From: Arthurvroum Date: Mon, 30 Mar 2026 08:50:09 +0200 Subject: [PATCH 03/11] fix(parser): prevent TypeError by ensuring sarif.runs is initialized before pushing --- src/ctrace/SarifParser.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ctrace/SarifParser.ts b/src/ctrace/SarifParser.ts index 89ae0ab..9a081ea 100644 --- a/src/ctrace/SarifParser.ts +++ b/src/ctrace/SarifParser.ts @@ -31,9 +31,15 @@ export async function parseSarifOutput(stdout: string, reportFilePath?: string): } const mergeSarif = (source: SarifLog) => { + if (!source || !source.runs) { + return; + } if (!sarif) { sarif = source; } else { + if (!sarif.runs) { + sarif.runs = []; + } sarif.runs.push(...source.runs); } }; From 316382f373c9e74fc63553f9c9753f1df3eb2b62 Mon Sep 17 00:00:00 2001 From: Arthurvroum Date: Mon, 30 Mar 2026 09:09:13 +0200 Subject: [PATCH 04/11] chore: prepare release, bump version to 0.1.0 with updated README --- README.md | 2 +- package.json | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e5f940..eeacb66 100644 --- a/README.md +++ b/README.md @@ -43,5 +43,5 @@ No need to wait for a full re-scan. CoreTrace caches file hashes across the enti 4. Click **Run Analysis** to execute `ctrace`. All findings will appear instantly in the panel and directly in your code editor as error/warning highlights. ## ⚙️ Requirements -- The extension automatically uses the `ctrace` / `coretrace` CLI binaries bundled alongside the extension installation. You do not need to install them manually in your PATH. +- The extension automatically downloads and installs the latest `ctrace` / `coretrace` CLI binaries from GitHub Releases during its first activation. You do not need to install them manually in your PATH. It will also periodically check for and download binary updates automatically in the background. - *Recommended:* For accurate analysis in complex codebases, it is advised to generate a `compile_commands.json` (e.g. via `cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON`) in your workspace root or `build` directory. diff --git a/package.json b/package.json index dc79354..8fd4f78 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,12 @@ "name": "ctrace-audit", "displayName": "Ctrace Audit", "description": "Integration for Ctrace CLI Audit Tool", - "version": "0.0.1", + "version": "0.1.0", + "publisher": "CoreTrace", + "repository": { + "type": "git", + "url": "https://github.com/CoreTrace/coretrace-vscode.git" + }, "engines": { "vscode": "^1.80.0" }, @@ -60,6 +65,7 @@ "@types/vscode": "^1.80.0", "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", + "@vscode/vsce": "^2.32.0", "lucide": "^0.575.0", "mocha": "^11.7.5", "ts-node": "^10.9.2", From 1a2bbb841185e28a879a89d020b52b8dff943fb2 Mon Sep 17 00:00:00 2001 From: Arthurvroum Date: Mon, 30 Mar 2026 09:19:01 +0200 Subject: [PATCH 05/11] chore: remove vsce from devDependencies to fix CI on Node 18 --- package-lock.json | 4 ++-- package.json | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f255c34..2f5e50e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ctrace-audit", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ctrace-audit", - "version": "0.0.1", + "version": "0.1.0", "dependencies": { "axios": "^1.13.6", "tar": "^6.2.1" diff --git a/package.json b/package.json index 8fd4f78..6b86901 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "@types/vscode": "^1.80.0", "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", - "@vscode/vsce": "^2.32.0", "lucide": "^0.575.0", "mocha": "^11.7.5", "ts-node": "^10.9.2", From 82a7dc2d89b075293246e8b1a64064d22c557bbb Mon Sep 17 00:00:00 2001 From: Arthurvroum Date: Mon, 30 Mar 2026 09:33:59 +0200 Subject: [PATCH 06/11] chore: add extension icon, keywords, and gallery banner --- icon.png | Bin 0 -> 27535 bytes package.json | 13 +++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 icon.png diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3eb4802cb156710819e2db1d2a6b25bd1e93bf75 GIT binary patch literal 27535 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

R!$S2*04mqDQ$9P2 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 Date: Wed, 1 Apr 2026 12:14:24 +0200 Subject: [PATCH 07/11] refactor: optimize binary download with streams and error handling --- src/ctrace/BinaryUpdater.ts | 136 +++++++++++++++++++++++++----------- src/extension.ts | 8 ++- 2 files changed, 100 insertions(+), 44 deletions(-) diff --git a/src/ctrace/BinaryUpdater.ts b/src/ctrace/BinaryUpdater.ts index 9d3a448..e18ccc6 100644 --- a/src/ctrace/BinaryUpdater.ts +++ b/src/ctrace/BinaryUpdater.ts @@ -3,25 +3,36 @@ import * as fs from 'fs'; import * as path from 'path'; import axios from 'axios'; import * as tar from 'tar'; +import { pipeline } from 'stream/promises'; import { locateBinary } from './BinaryLocator'; const REPO_LATEST_RELEASE_URL = 'https://api.github.com/repos/CoreTrace/coretrace/releases/latest'; +let updatePromise: Promise | null = null; + export async function ensureBinary(context: vscode.ExtensionContext, output: vscode.OutputChannel): Promise { + if (updatePromise) { + return updatePromise; + } + updatePromise = doEnsureBinary(context, output).finally(() => { + updatePromise = null; + }); + return updatePromise; +} + +async function doEnsureBinary(context: vscode.ExtensionContext, output: vscode.OutputChannel): Promise { const globalStorage = context.globalStorageUri.fsPath; const binDir = path.join(globalStorage, 'bin'); - if (!fs.existsSync(binDir)) { - fs.mkdirSync(binDir, { recursive: true }); - } + await fs.promises.mkdir(binDir, { recursive: true }); const downloadedBinaryPath = await getExtractedBinaryPath(binDir); const lastCheck = context.globalState.get('coretrace-last-update-check') || 0; const now = Date.now(); const TWELVE_HOURS = 12 * 60 * 60 * 1000; - // Si on a déjà vérifié récemment, on ne spamme pas l'API GitHub. - // On retourne le binaire téléchargé s'il existe, sinon on tente de replier sur le binaire packagé. + // If we have checked recently, avoid spamming the GitHub API. + // Return the downloaded binary if it exists, otherwise fallback to the packaged binary. if (now - lastCheck < TWELVE_HOURS) { if (downloadedBinaryPath) return downloadedBinaryPath; return await locateBinary(context.extensionUri.fsPath); @@ -35,12 +46,10 @@ export async function ensureBinary(context: vscode.ExtensionContext, output: vsc const release = response.data; const latestVersion = release.tag_name; - context.globalState.update('coretrace-last-update-check', now); - const currentVersion = context.globalState.get('coretrace-version'); - const downloadedBinaryPath = await getExtractedBinaryPath(binDir); + const cachedBinaryPath = await getExtractedBinaryPath(binDir); - if (latestVersion !== currentVersion || !downloadedBinaryPath) { + if (latestVersion !== currentVersion || !cachedBinaryPath) { const assetInfo = getAssetForPlatform(release.assets); if (assetInfo) { await vscode.window.withProgress({ @@ -58,6 +67,9 @@ export async function ensureBinary(context: vscode.ExtensionContext, output: vsc } } + // Update the timestamp only after a successful check (and potential download) + context.globalState.update('coretrace-last-update-check', now); + const bin = await getExtractedBinaryPath(binDir); if (bin) return bin; @@ -109,7 +121,8 @@ function getAssetForPlatform(assets: any[]): { name: string; url: string } | nul } async function downloadAndExtract(url: string, destDir: string, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise { - const tarballPath = path.join(destDir, 'download.tar.gz'); + const timestamp = Date.now(); + const tarballPath = path.join(destDir, `download-${timestamp}.tar.gz`); const token = process.env.GITHUB_TOKEN || ''; const headers: any = { 'User-Agent': 'vscode-coretrace' }; @@ -122,40 +135,79 @@ async function downloadAndExtract(url: string, destDir: string, progress: vscode headers }); + if (response.status !== 200) { + throw new Error(`Failed to download asset: HTTP ${response.status}`); + } + const totalLength = parseInt(response.headers['content-length'], 10); let downloadedLength = 0; const writer = fs.createWriteStream(tarballPath); - response.data.on('data', (chunk: Buffer) => { - downloadedLength += chunk.length; - if (totalLength) { - const percent = Math.round((downloadedLength / totalLength) * 100); - progress.report({ message: `${percent}%`, increment: (chunk.length / totalLength) * 100 }); - } - }); - - response.data.pipe(writer); + try { + response.data.on('data', (chunk: Buffer) => { + downloadedLength += chunk.length; + if (totalLength) { + const percent = Math.round((downloadedLength / totalLength) * 100); + progress.report({ message: `${percent}%`, increment: (chunk.length / totalLength) * 100 }); + } + }); - await new Promise((resolve, reject) => { - writer.on('finish', resolve); - writer.on('error', reject); - }); + await pipeline(response.data, writer); + + // Extract to a unique temp directory + const tmpDir = path.join(destDir, `tmp-${timestamp}`); + await fs.promises.mkdir(tmpDir, { recursive: true }); + + try { + await tar.x({ + file: tarballPath, + C: tmpDir, + strip: 0 // Do not strip to avoid dropping root-level binaries + }); + + // Find the extracted binary recursively + const candidates = ['ctrace', 'coretrace', 'ctrace.exe', 'coretrace.exe']; + async function findBinary(dir: string): Promise { + const entries = await fs.promises.readdir(dir, { withFileTypes: true }); + for (const name of candidates) { + const match = entries.find(e => e.isFile() && e.name === name); + if (match) return path.join(dir, match.name); + } + for (const entry of entries) { + if (entry.isDirectory()) { + const res = await findBinary(path.join(dir, entry.name)); + if (res) return res; + } + } + return null; + } - // Extract - await tar.x({ - file: tarballPath, - C: destDir, - strip: 1 // Sometimes they bundle it under a folder like coretrace-v0.73.1-linux-amd64/ctrace. We strip first dir. - }); + const binaryInTmp = await findBinary(tmpDir); + if (!binaryInTmp) { + throw new Error("Could not find ctrace/coretrace binary inside the downloaded archive."); + } - fs.unlinkSync(tarballPath); + const finalBinPath = path.join(destDir, path.basename(binaryInTmp)); + // Move binary to the root of destDir + await fs.promises.rename(binaryInTmp, finalBinPath); - // Make extracted files executable if on linux/mac - if (process.platform !== 'win32') { - const binPath = await getExtractedBinaryPath(destDir); - if (binPath) { - fs.chmodSync(binPath, 0o755); + // Make it executable if on linux/mac + if (process.platform !== 'win32') { + await fs.promises.chmod(finalBinPath, 0o755); + } + } finally { + try { + await fs.promises.rm(tmpDir, { recursive: true, force: true }); + } catch (e) { + // Ignore removal errors + } + } + } finally { + try { + await fs.promises.unlink(tarballPath); + } catch (e) { + // Ignore if file doesn't exist or can't be removed } } } @@ -163,18 +215,18 @@ async function downloadAndExtract(url: string, destDir: string, progress: vscode async function getExtractedBinaryPath(binDir: string): Promise { const candidates = ['ctrace', 'coretrace', 'ctrace.exe', 'coretrace.exe']; for (const name of candidates) { + // Fallback for flat structure or the newly moved binary + const file = path.join(binDir, name); + if (fs.existsSync(file)) { + return file; + } + // Tarball structure is often: coretrace-vX.Y.Z-arch/bin/ctrace - // With strip: 1, it becomes: binDir/bin/ctrace + // Keeping this for backwards compatibility const fileInBin = path.join(binDir, 'bin', name); if (fs.existsSync(fileInBin)) { return fileInBin; } - - // Fallback for flat structure - const file = path.join(binDir, name); - if (fs.existsSync(file)) { - return file; - } } return null; } diff --git a/src/extension.ts b/src/extension.ts index f6686d4..e8b846e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -51,8 +51,10 @@ export function activate(context: vscode.ExtensionContext) { async function locateOrError(): Promise { const p = await ensureBinary(context, output); if (!p) { + const extPath = context.extensionUri.fsPath; + const globalStorage = context.globalStorageUri.fsPath; vscode.window.showErrorMessage( - `Ctrace binary could not be found or downloaded.` + `Ctrace binary could not be found or downloaded. Checked: \n- ${globalStorage}/bin\n- ${extPath}\nSee the "Ctrace" Output channel for details.` ); } return p; @@ -323,8 +325,10 @@ export function activate(context: vscode.ExtensionContext) { // Locate binary const ctracePath = await ensureBinary(context, output); if (!ctracePath) { + const extPath = context.extensionUri.fsPath; + const globalStorage = context.globalStorageUri.fsPath; vscode.window.showErrorMessage( - `Ctrace binary could not be found or downloaded.` + `Ctrace binary could not be found or downloaded. Checked: \n- ${globalStorage}/bin\n- ${extPath}\nSee the "Ctrace" Output channel for details.` ); sidebarProvider.postMessage({ type: 'analysis-error' }); isRunning = false; From d9af9f6b0f1baf775390e21432c5a214870986f6 Mon Sep 17 00:00:00 2001 From: Arthurvroum Date: Wed, 1 Apr 2026 12:18:14 +0200 Subject: [PATCH 08/11] fix: prevent analysis execution while downloading binary update --- src/ctrace/BinaryUpdater.ts | 4 ++++ src/extension.ts | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/ctrace/BinaryUpdater.ts b/src/ctrace/BinaryUpdater.ts index e18ccc6..1c71dbb 100644 --- a/src/ctrace/BinaryUpdater.ts +++ b/src/ctrace/BinaryUpdater.ts @@ -10,6 +10,10 @@ const REPO_LATEST_RELEASE_URL = 'https://api.github.com/repos/CoreTrace/coretrac let updatePromise: Promise | null = null; +export function isUpdatingBinary(): boolean { + return updatePromise !== null; +} + export async function ensureBinary(context: vscode.ExtensionContext, output: vscode.OutputChannel): Promise { if (updatePromise) { return updatePromise; diff --git a/src/extension.ts b/src/extension.ts index e8b846e..a013b83 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { SidebarProvider, type HostMessage } from './SidebarProvider'; -import { ensureBinary } from './ctrace/BinaryUpdater'; +import { ensureBinary, isUpdatingBinary } from './ctrace/BinaryUpdater'; import { buildCommand, parseAndValidateParams } from './ctrace/CommandBuilder'; import { runCommand } from './ctrace/AnalysisRunner'; import { parseSarifOutput, countResults } from './ctrace/SarifParser'; @@ -72,6 +72,11 @@ export function activate(context: vscode.ExtensionContext) { return; } + if (isUpdatingBinary()) { + vscode.window.showWarningMessage('Ctrace is currently updating. Please wait for the download to finish before running an analysis.'); + return; + } + if (!vscode.workspace.workspaceFolders?.length) { vscode.window.showErrorMessage('Please open a workspace folder.'); sidebarProvider.postMessage({ type: 'analysis-error' }); @@ -302,6 +307,12 @@ export function activate(context: vscode.ExtensionContext) { vscode.window.showWarningMessage('An analysis is already in progress.'); return; } + + if (isUpdatingBinary()) { + vscode.window.showWarningMessage('Ctrace is currently updating. Please wait for the download to finish before running an analysis.'); + return; + } + isRunning = true; const params = resolveParams(arg); From e548b0cfc526845a7f4fa4f6eefa7fa5ce05d4b7 Mon Sep 17 00:00:00 2001 From: Arthurvroum Date: Wed, 1 Apr 2026 12:46:28 +0200 Subject: [PATCH 09/11] fix(deps): bump tar to ^7.x to resolve security issues + add stream abort timeouts --- package-lock.json | 114 ++++++++++++------------------------ package.json | 2 +- src/ctrace/BinaryUpdater.ts | 24 +++++++- tsconfig.json | 3 +- 4 files changed, 62 insertions(+), 81 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f5e50e..658c276 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "axios": "^1.13.6", - "tar": "^6.2.1" + "tar": "^7.5.13" }, "devDependencies": { "@types/mocha": "^10.0.10", @@ -65,6 +65,17 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -473,11 +484,11 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/cli-cursor": { @@ -873,28 +884,6 @@ "node": ">= 6" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1444,43 +1433,19 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">=8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" + "node": ">= 18" } }, "node_modules/mocha": { @@ -2077,28 +2042,18 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/test-exclude": { @@ -2389,9 +2344,12 @@ } }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "engines": { + "node": ">=18" + } }, "node_modules/yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index 05b51ad..87bef4c 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,6 @@ }, "dependencies": { "axios": "^1.13.6", - "tar": "^6.2.1" + "tar": "^7.5.13" } } diff --git a/src/ctrace/BinaryUpdater.ts b/src/ctrace/BinaryUpdater.ts index 1c71dbb..3089c01 100644 --- a/src/ctrace/BinaryUpdater.ts +++ b/src/ctrace/BinaryUpdater.ts @@ -79,6 +79,8 @@ async function doEnsureBinary(context: vscode.ExtensionContext, output: vscode.O } catch (err: any) { output.appendLine(`Failed to check for CoreTrace updates: ${err.message}`); + // Even on failure, update the check timestamp to avoid spamming the API continuously during an outage + context.globalState.update('coretrace-last-update-check', now); } // Try to return the previously downloaded binary first, even if update check failed @@ -132,14 +134,19 @@ async function downloadAndExtract(url: string, destDir: string, progress: vscode const headers: any = { 'User-Agent': 'vscode-coretrace' }; if (token) headers['Authorization'] = `token ${token}`; + const controller = new AbortController(); + let timeoutId = setTimeout(() => controller.abort(new Error("Download stalled")), 30000); + const response = await axios({ url, method: 'GET', responseType: 'stream', - headers + headers, + signal: controller.signal }); if (response.status !== 200) { + clearTimeout(timeoutId); throw new Error(`Failed to download asset: HTTP ${response.status}`); } @@ -150,6 +157,9 @@ async function downloadAndExtract(url: string, destDir: string, progress: vscode try { response.data.on('data', (chunk: Buffer) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => controller.abort(new Error("Download stalled")), 30000); + downloadedLength += chunk.length; if (totalLength) { const percent = Math.round((downloadedLength / totalLength) * 100); @@ -158,6 +168,7 @@ async function downloadAndExtract(url: string, destDir: string, progress: vscode }); await pipeline(response.data, writer); + clearTimeout(timeoutId); // Extract to a unique temp directory const tmpDir = path.join(destDir, `tmp-${timestamp}`); @@ -193,6 +204,16 @@ async function downloadAndExtract(url: string, destDir: string, progress: vscode } const finalBinPath = path.join(destDir, path.basename(binaryInTmp)); + + // Remove existing binary to avoid errors (e.g., EPERM/EEXIST on Windows) during rename + try { + await fs.promises.unlink(finalBinPath); + } catch (err: any) { + if (err.code !== 'ENOENT') { + throw err; + } + } + // Move binary to the root of destDir await fs.promises.rename(binaryInTmp, finalBinPath); @@ -208,6 +229,7 @@ async function downloadAndExtract(url: string, destDir: string, progress: vscode } } } finally { + clearTimeout(timeoutId); try { await fs.promises.unlink(tarballPath); } catch (e) { diff --git a/tsconfig.json b/tsconfig.json index aac2dee..79c8edc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ ], "sourceMap": true, "rootDir": "src", - "strict": true + "strict": true, + "skipLibCheck": true }, "exclude": [ "node_modules", From d892a49364979a6c83712bc79d751a5b06bbbcd9 Mon Sep 17 00:00:00 2001 From: Arthurvroum Date: Wed, 1 Apr 2026 13:48:47 +0200 Subject: [PATCH 10/11] fix: stabilize binary download UI state and update retry flow --- media/main.js | 37 +++++++++++++++++++++++++++++++-- src/SidebarProvider.ts | 3 +++ src/ctrace/BinaryUpdater.ts | 41 ++++++++++++++++++++++++++++++------- src/extension.ts | 28 +++++++++++++++++++------ 4 files changed, 94 insertions(+), 15 deletions(-) diff --git a/media/main.js b/media/main.js index 5ce32bb..d3bdc54 100644 --- a/media/main.js +++ b/media/main.js @@ -30,16 +30,17 @@ // ── Scope selector ───────────────────────────────────────────────────────── let workspaceMode = false; let isRunning = false; + let isDownloading = false; function applyScope(ws) { - if (isRunning) { return; } + if (isRunning || isDownloading) { return; } workspaceMode = !!ws; if (scopeFile) { scopeFile.classList.toggle('active', !workspaceMode); } if (scopeWs) { scopeWs.classList.toggle('active', workspaceMode); } if (filePill) { filePill.classList.toggle('hidden', workspaceMode); } if (wsPill) { wsPill.classList.toggle('hidden', !workspaceMode); } if (wsProgress) { wsProgress.classList.add('hidden'); } - if (runLabel) { runLabel.textContent = 'Run Analysis'; } + if (runLabel && !isDownloading && !isRunning) { runLabel.textContent = 'Run Analysis'; } } if (scopeFile) { scopeFile.addEventListener('click', () => applyScope(false)); } @@ -86,6 +87,15 @@ window.addEventListener('message', event => { const msg = event.data; switch (msg.type) { + case 'analysis-downloading': + setDownloading(msg.progress); + break; + case 'analysis-download-complete': + setDownloadComplete(); + break; + case 'analysis-start': + setRunning(true); + break; case 'analysis-result': handleAnalysisResult(msg.data); break; @@ -107,8 +117,31 @@ }); // ── Handlers ─────────────────────────────────────────────────────────────── + function setDownloading(progressMsg) { + isDownloading = true; + if (!runBtn) { return; } + runBtn.disabled = true; + runBtn.classList.add('running'); // Force spinner instead of play icon + if (runLabel) { runLabel.textContent = `Downloading (${progressMsg})`; } + + if (scopeFile) scopeFile.style.opacity = '0.5'; + if (scopeFile) scopeFile.style.cursor = 'not-allowed'; + if (scopeWs) scopeWs.style.opacity = '0.5'; + if (scopeWs) scopeWs.style.cursor = 'not-allowed'; + } + + function setDownloadComplete() { + isDownloading = false; + if (!isRunning) { + setRunning(false); + } + } + function setRunning(running) { isRunning = running; + if (running) { + isDownloading = false; + } if (!runBtn) { return; } runBtn.disabled = running; runBtn.classList.toggle('running', running); diff --git a/src/SidebarProvider.ts b/src/SidebarProvider.ts index f838c1f..b677883 100644 --- a/src/SidebarProvider.ts +++ b/src/SidebarProvider.ts @@ -24,6 +24,9 @@ const ALLOWED_COMMANDS = new Set(['ctrace.runAnalysis', 'ctrace.runWorkspaceAnal export type HostMessage = | { type: 'analysis-result'; data: unknown } | { type: 'analysis-error' } + | { type: 'analysis-start' } + | { type: 'analysis-download-complete' } + | { type: 'analysis-downloading'; progress: string } | { type: 'active-file'; name: string | null } | { type: 'workspace-progress'; total: number; changed: number; cached: number; done: number }; diff --git a/src/ctrace/BinaryUpdater.ts b/src/ctrace/BinaryUpdater.ts index 3089c01..4e19ce1 100644 --- a/src/ctrace/BinaryUpdater.ts +++ b/src/ctrace/BinaryUpdater.ts @@ -14,12 +14,21 @@ export function isUpdatingBinary(): boolean { return updatePromise !== null; } +let progressListener: ((msg: string) => void) | undefined; + +export function setBinaryUpdateListener(listener: (msg: string) => void) { + progressListener = listener; +} + export async function ensureBinary(context: vscode.ExtensionContext, output: vscode.OutputChannel): Promise { if (updatePromise) { return updatePromise; } updatePromise = doEnsureBinary(context, output).finally(() => { updatePromise = null; + if (progressListener) { + progressListener('__done__'); + } }); return updatePromise; } @@ -32,13 +41,20 @@ async function doEnsureBinary(context: vscode.ExtensionContext, output: vscode.O const downloadedBinaryPath = await getExtractedBinaryPath(binDir); const lastCheck = context.globalState.get('coretrace-last-update-check') || 0; + const lastFailedCheck = context.globalState.get('coretrace-last-failed-update-check') || 0; const now = Date.now(); const TWELVE_HOURS = 12 * 60 * 60 * 1000; + const FAILURE_BACKOFF = 60 * 1000; + + // If we have a cached binary and checked recently, avoid spamming the GitHub API. + if (downloadedBinaryPath && now - lastCheck < TWELVE_HOURS) { + return downloadedBinaryPath; + } - // If we have checked recently, avoid spamming the GitHub API. - // Return the downloaded binary if it exists, otherwise fallback to the packaged binary. - if (now - lastCheck < TWELVE_HOURS) { - if (downloadedBinaryPath) return downloadedBinaryPath; + // When there is no cached binary, keep retrying with a short backoff. + // This avoids a 12-hour lockout if the very first download fails. + if (!downloadedBinaryPath && now - lastFailedCheck < FAILURE_BACKOFF) { + output.appendLine('Skipping update check due to recent failure backoff.'); return await locateBinary(context.extensionUri.fsPath); } @@ -56,6 +72,8 @@ async function doEnsureBinary(context: vscode.ExtensionContext, output: vscode.O if (latestVersion !== currentVersion || !cachedBinaryPath) { const assetInfo = getAssetForPlatform(release.assets); if (assetInfo) { + output.appendLine(`Selected release asset: ${assetInfo.name}`); + if (progressListener) { progressListener("0%"); } await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: `Downloading CoreTrace ${latestVersion}...`, @@ -64,6 +82,7 @@ async function doEnsureBinary(context: vscode.ExtensionContext, output: vscode.O output.appendLine(`Downloading CoreTrace release ${latestVersion} from GitHub...`); await downloadAndExtract(assetInfo.url, binDir, progress); context.globalState.update('coretrace-version', latestVersion); + context.globalState.update('coretrace-last-failed-update-check', undefined); output.appendLine(`Updated CoreTrace to ${latestVersion} successfully.`); }); } else { @@ -79,8 +98,12 @@ async function doEnsureBinary(context: vscode.ExtensionContext, output: vscode.O } catch (err: any) { output.appendLine(`Failed to check for CoreTrace updates: ${err.message}`); - // Even on failure, update the check timestamp to avoid spamming the API continuously during an outage - context.globalState.update('coretrace-last-update-check', now); + context.globalState.update('coretrace-last-failed-update-check', now); + + // Keep old behaviour only when we already have a working cached binary. + if (downloadedBinaryPath) { + context.globalState.update('coretrace-last-update-check', now); + } } // Try to return the previously downloaded binary first, even if update check failed @@ -163,13 +186,17 @@ async function downloadAndExtract(url: string, destDir: string, progress: vscode downloadedLength += chunk.length; if (totalLength) { const percent = Math.round((downloadedLength / totalLength) * 100); - progress.report({ message: `${percent}%`, increment: (chunk.length / totalLength) * 100 }); + const msg = `${percent}%`; + progress.report({ message: msg, increment: (chunk.length / totalLength) * 100 }); + if (progressListener) { progressListener(msg); } } }); await pipeline(response.data, writer); clearTimeout(timeoutId); + if (progressListener) { progressListener("Extracting..."); } + // Extract to a unique temp directory const tmpDir = path.join(destDir, `tmp-${timestamp}`); await fs.promises.mkdir(tmpDir, { recursive: true }); diff --git a/src/extension.ts b/src/extension.ts index a013b83..b836cf0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { SidebarProvider, type HostMessage } from './SidebarProvider'; -import { ensureBinary, isUpdatingBinary } from './ctrace/BinaryUpdater'; +import { ensureBinary, isUpdatingBinary, setBinaryUpdateListener } from './ctrace/BinaryUpdater'; import { buildCommand, parseAndValidateParams } from './ctrace/CommandBuilder'; import { runCommand } from './ctrace/AnalysisRunner'; import { parseSarifOutput, countResults } from './ctrace/SarifParser'; @@ -22,11 +22,6 @@ export function activate(context: vscode.ExtensionContext) { const output = vscode.window.createOutputChannel('Ctrace'); context.subscriptions.push(output); - // Initialise and pre-fetch the binary in the background on startup - ensureBinary(context, output).catch((err) => { - output.appendLine('Failed to pre-fetch binary on activation: ' + err); - }); - // ── Sidebar ────────────────────────────────────────────────────────────── const sidebarProvider = new SidebarProvider(context.extensionUri); // Register the provider itself as a Disposable so its view-scoped @@ -37,6 +32,19 @@ export function activate(context: vscode.ExtensionContext) { vscode.window.registerWebviewViewProvider('ctrace-audit-view', sidebarProvider) ); + setBinaryUpdateListener((msg) => { + if (msg === '__done__') { + sidebarProvider.postMessage({ type: 'analysis-download-complete' }); + return; + } + sidebarProvider.postMessage({ type: 'analysis-downloading', progress: msg }); + }); + + // Initialise and pre-fetch the binary in the background on startup + ensureBinary(context, output).catch((err) => { + output.appendLine('Failed to pre-fetch binary on activation: ' + err); + }); + // ── Diagnostics collection ─────────────────────────────────────────────── const diagnosticCollection = vscode.languages.createDiagnosticCollection('ctrace'); context.subscriptions.push(diagnosticCollection); @@ -69,11 +77,13 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('ctrace.runWorkspaceAnalysis', async (arg?: AnalysisParams | string) => { if (isRunning) { vscode.window.showWarningMessage('An analysis is already in progress.'); + sidebarProvider.postMessage({ type: 'analysis-error' }); return; } if (isUpdatingBinary()) { vscode.window.showWarningMessage('Ctrace is currently updating. Please wait for the download to finish before running an analysis.'); + sidebarProvider.postMessage({ type: 'analysis-error' }); return; } @@ -88,6 +98,8 @@ export function activate(context: vscode.ExtensionContext) { sidebarProvider.postMessage({ type: 'analysis-error' }); return; } + + sidebarProvider.postMessage({ type: 'analysis-start' }); const params = resolveParams(arg); @@ -305,11 +317,13 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('ctrace.runAnalysis', async (arg?: AnalysisParams | string) => { if (isRunning) { vscode.window.showWarningMessage('An analysis is already in progress.'); + sidebarProvider.postMessage({ type: 'analysis-error' }); return; } if (isUpdatingBinary()) { vscode.window.showWarningMessage('Ctrace is currently updating. Please wait for the download to finish before running an analysis.'); + sidebarProvider.postMessage({ type: 'analysis-error' }); return; } @@ -346,6 +360,8 @@ export function activate(context: vscode.ExtensionContext) { return; } + sidebarProvider.postMessage({ type: 'analysis-start' }); + // Build command (also validates params — throws on unsafe input). // Async on Windows: the fallback path copies the binary to %TEMP% // using non-blocking I/O to avoid stalling the extension host. From 2107c59fa5bf2d30d175cc981c579d41002eb02f Mon Sep 17 00:00:00 2001 From: Arthurvroum Date: Wed, 1 Apr 2026 16:32:16 +0200 Subject: [PATCH 11/11] fix(binary-updater): await globalState updates and catch ensureBinary errors --- package-lock.json | 8 ++++---- package.json | 4 ++-- src/ctrace/BinaryUpdater.ts | 12 +++++++----- src/extension.ts | 25 ++++++++++++------------- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 658c276..7b407bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@types/mocha": "^10.0.10", "@types/node": "^18.19.130", "@types/tar": "^6.1.13", - "@types/vscode": "^1.80.0", + "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "lucide": "^0.575.0", @@ -185,9 +185,9 @@ } }, "node_modules/@types/vscode": { - "version": "1.109.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz", - "integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==", + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", "dev": true }, "node_modules/@vscode/test-cli": { diff --git a/package.json b/package.json index 87bef4c..407e81d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "url": "https://github.com/CoreTrace/coretrace-vscode.git" }, "engines": { - "vscode": "^1.80.0" + "vscode": "^1.82.0" }, "categories": [ "Other" @@ -75,7 +75,7 @@ "@types/mocha": "^10.0.10", "@types/node": "^18.19.130", "@types/tar": "^6.1.13", - "@types/vscode": "^1.80.0", + "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "lucide": "^0.575.0", diff --git a/src/ctrace/BinaryUpdater.ts b/src/ctrace/BinaryUpdater.ts index 4e19ce1..217a6b0 100644 --- a/src/ctrace/BinaryUpdater.ts +++ b/src/ctrace/BinaryUpdater.ts @@ -81,8 +81,10 @@ async function doEnsureBinary(context: vscode.ExtensionContext, output: vscode.O }, async (progress) => { output.appendLine(`Downloading CoreTrace release ${latestVersion} from GitHub...`); await downloadAndExtract(assetInfo.url, binDir, progress); - context.globalState.update('coretrace-version', latestVersion); - context.globalState.update('coretrace-last-failed-update-check', undefined); + await Promise.all([ + context.globalState.update('coretrace-version', latestVersion), + context.globalState.update('coretrace-last-failed-update-check', undefined) + ]); output.appendLine(`Updated CoreTrace to ${latestVersion} successfully.`); }); } else { @@ -91,18 +93,18 @@ async function doEnsureBinary(context: vscode.ExtensionContext, output: vscode.O } // Update the timestamp only after a successful check (and potential download) - context.globalState.update('coretrace-last-update-check', now); + await context.globalState.update('coretrace-last-update-check', now); const bin = await getExtractedBinaryPath(binDir); if (bin) return bin; } catch (err: any) { output.appendLine(`Failed to check for CoreTrace updates: ${err.message}`); - context.globalState.update('coretrace-last-failed-update-check', now); + await context.globalState.update('coretrace-last-failed-update-check', now); // Keep old behaviour only when we already have a working cached binary. if (downloadedBinaryPath) { - context.globalState.update('coretrace-last-update-check', now); + await context.globalState.update('coretrace-last-update-check', now); } } diff --git a/src/extension.ts b/src/extension.ts index b836cf0..33b32eb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -57,7 +57,13 @@ export function activate(context: vscode.ExtensionContext) { // ── Shared helpers ─────────────────────────────────────────────────────── async function locateOrError(): Promise { - const p = await ensureBinary(context, output); + let p: string | null = null; + try { + p = await ensureBinary(context, output); + } catch (e: any) { + output.appendLine(`ensureBinary threw an error: ${e.message}`); + } + if (!p) { const extPath = context.extensionUri.fsPath; const globalStorage = context.globalStorageUri.fsPath; @@ -327,14 +333,10 @@ export function activate(context: vscode.ExtensionContext) { return; } - isRunning = true; - const params = resolveParams(arg); - const editor = vscode.window.activeTextEditor; if (!editor) { vscode.window.showErrorMessage('No active file to analyse.'); sidebarProvider.postMessage({ type: 'analysis-error' }); - isRunning = false; return; } @@ -343,25 +345,22 @@ export function activate(context: vscode.ExtensionContext) { if (!vscode.workspace.workspaceFolders?.length) { vscode.window.showErrorMessage('Please open a workspace folder.'); sidebarProvider.postMessage({ type: 'analysis-error' }); - isRunning = false; return; } // Locate binary - const ctracePath = await ensureBinary(context, output); + const ctracePath = await locateOrError(); + if (!ctracePath) { - const extPath = context.extensionUri.fsPath; - const globalStorage = context.globalStorageUri.fsPath; - vscode.window.showErrorMessage( - `Ctrace binary could not be found or downloaded. Checked: \n- ${globalStorage}/bin\n- ${extPath}\nSee the "Ctrace" Output channel for details.` - ); sidebarProvider.postMessage({ type: 'analysis-error' }); - isRunning = false; return; } + isRunning = true; sidebarProvider.postMessage({ type: 'analysis-start' }); + const params = resolveParams(arg); + // Build command (also validates params — throws on unsafe input). // Async on Windows: the fallback path copies the binary to %TEMP% // using non-blocking I/O to avoid stalling the extension host.