diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 5127ea19ef..d2a3fd628f 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -37,29 +37,29 @@ jobs: - 22 steps: - name: "Checkout" - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 - name: "Setup pnpm" - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 - name: 'Setup Node.js "${{ matrix.node }}.x" environment' - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node }} registry-url: https://npm.pkg.github.com/ cache: pnpm - name: "Setup Python environment, because of electron-builder install-app-deps requires Python setup tools" - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: "Get package info" id: package-info - uses: luizfelipelaviola/get-package-info@v1 + uses: luizfelipelaviola/get-package-info@v2 with: path: ./packages/gui @@ -88,7 +88,7 @@ jobs: python --version; - name: "Setup electron cache" - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ github.workspace }}/.cache/electron key: ${{ runner.os }}-electron-cache-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -96,7 +96,7 @@ jobs: ${{ runner.os }}-electron-cache- - name: "Setup electron-builder cache" - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ github.workspace }}/.cache/electron-builder key: ${{ runner.os }}-electron-builder-cache-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -137,7 +137,7 @@ jobs: - name: "Cache Flatpak runtime (Linux)" if: ${{ matrix.os == 'ubuntu' }} id: cache-flatpak-runtime - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.local/share/flatpak key: flatpak-runtime-freedesktop-21.08 @@ -161,7 +161,7 @@ jobs: - name: "Cache Flatpak runtime (Linux ARM64)" if: ${{ matrix.os == 'ubuntu-24.04-arm' }} id: cache-flatpak-runtime-linux-arm64 - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.local/share/flatpak key: flatpak-runtime-freedesktop-21.08 @@ -248,14 +248,12 @@ jobs: mv DevSidecar-${{ steps.package-info.outputs.version }}-x86_64.AppImage DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage; mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.tar.gz DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.tar.gz; mv DevSidecar-${{ steps.package-info.outputs.version }}-x86_64.rpm DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.rpm; - mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.pkg.tar.xz DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.pkg.tar.xz; # mv DevSidecar-${{ steps.package-info.outputs.version }}-x86_64.flatpak DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.flatpak; #------------------------------------------------------------------------------------------------------------------------- mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.deb DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb; mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.AppImage DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage; mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.tar.gz DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz; mv DevSidecar-${{ steps.package-info.outputs.version }}-aarch64.rpm DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.rpm; - mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.pkg.tar.xz DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.pkg.tar.xz; # arm64 flatpak is on Linux ARM64 runner #------------------------------------------------------------------------------------------------------------------------- mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.deb DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb; @@ -281,21 +279,21 @@ jobs: # region Upload artifacts - Windows - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-x86_64.exe" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-x86_64.exe name: "DevSidecar-${{ steps.package-info.outputs.version }}-windows-x86_64.exe" if-no-files-found: error - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe name: "DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe" if-no-files-found: error - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe @@ -305,42 +303,36 @@ jobs: # region Upload artifacts - Linux - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.deb" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.deb name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.deb" if-no-files-found: error - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage" if-no-files-found: error - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.tar.gz" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.tar.gz name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.tar.gz" if-no-files-found: error - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.rpm" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.rpm name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.rpm" if-no-files-found: error - - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.pkg.tar.xz" - uses: actions/upload-artifact@v4.4.0 - if: ${{ matrix.os == 'ubuntu' }} - with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.pkg.tar.xz - name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.pkg.tar.xz" - if-no-files-found: error + # - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.flatpak" - # uses: actions/upload-artifact@v4.4.0 + # uses: actions/upload-artifact@v6 # if: ${{ matrix.os == 'ubuntu' }} # with: # path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.flatpak @@ -349,65 +341,58 @@ jobs: # ------------------------------------------------------------------------------------------------------------------------- - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb" if-no-files-found: error - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage" if-no-files-found: error - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz" if-no-files-found: error - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.rpm" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.rpm name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.rpm" if-no-files-found: error - - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.pkg.tar.xz" - uses: actions/upload-artifact@v4.4.0 - if: ${{ matrix.os == 'ubuntu' }} - with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.pkg.tar.xz - name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.pkg.tar.xz" - if-no-files-found: error # ------------------------------------------------------------------------------------------------------------------------- - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb" if-no-files-found: error - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage" if-no-files-found: error - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz" if-no-files-found: error - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.rpm" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.rpm @@ -417,7 +402,7 @@ jobs: # region Upload artifacts - Linux ARM64 - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.flatpak" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu-24.04-arm' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.flatpak @@ -425,7 +410,7 @@ jobs: if-no-files-found: error # ------------------------------------------------------------------------------------------------------------------------- - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.flatpak" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu-24.04-arm' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.flatpak @@ -435,14 +420,14 @@ jobs: # Upload artifacts - macOS - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-x86_64.dmg" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'macos' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-x86_64.dmg name: "DevSidecar-${{ steps.package-info.outputs.version }}-macos-x86_64.dmg" if-no-files-found: error - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'macos' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg @@ -451,42 +436,42 @@ jobs: # region Upload update ZIPs (for auto-update, per-arch since native modules are arch-specific) - name: "Upload update ZIP (Win x64)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/update-win-x64-${{ steps.package-info.outputs.version }}.zip name: "update-win-x64-${{ steps.package-info.outputs.version }}.zip" if-no-files-found: error - name: "Upload update ZIP (Win ia32)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/update-win-ia32-${{ steps.package-info.outputs.version }}.zip name: "update-win-ia32-${{ steps.package-info.outputs.version }}.zip" if-no-files-found: warn - name: "Upload update ZIP (Win arm64)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/update-win-arm64-${{ steps.package-info.outputs.version }}.zip name: "update-win-arm64-${{ steps.package-info.outputs.version }}.zip" if-no-files-found: error - name: "Upload update ZIP (macOS x64)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'macos' }} with: path: packages/gui/dist_electron/update-mac-x64-${{ steps.package-info.outputs.version }}.zip name: "update-mac-x64-${{ steps.package-info.outputs.version }}.zip" if-no-files-found: error - name: "Upload update ZIP (macOS arm64)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'macos' }} with: path: packages/gui/dist_electron/update-mac-arm64-${{ steps.package-info.outputs.version }}.zip name: "update-mac-arm64-${{ steps.package-info.outputs.version }}.zip" if-no-files-found: error - name: "Upload update ZIP (Linux x64)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/update-linux-x64-${{ steps.package-info.outputs.version }}.zip @@ -496,70 +481,70 @@ jobs: # region Upload auto-update metadata files (latest*.yml + blockmaps + macOS zip target) - name: "Upload latest.yml (Windows)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/latest.yml name: "latest.yml" if-no-files-found: error - name: "Upload blockmap (Win x64)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-x64.exe.blockmap name: "DevSidecar-${{ steps.package-info.outputs.version }}-x64.exe.blockmap" if-no-files-found: error - name: "Upload blockmap (Win ia32)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-ia32.exe.blockmap name: "DevSidecar-${{ steps.package-info.outputs.version }}-ia32.exe.blockmap" if-no-files-found: warn - name: "Upload blockmap (Win arm64)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-arm64.exe.blockmap name: "DevSidecar-${{ steps.package-info.outputs.version }}-arm64.exe.blockmap" if-no-files-found: warn - name: "Upload latest-mac.yml" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'macos' }} with: path: packages/gui/dist_electron/latest-mac.yml name: "latest-mac.yml" if-no-files-found: error - name: "Upload blockmap (macOS x64)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'macos' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-x64.dmg.blockmap name: "DevSidecar-${{ steps.package-info.outputs.version }}-x64.dmg.blockmap" if-no-files-found: error - name: "Upload blockmap (macOS arm64)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'macos' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-arm64.dmg.blockmap name: "DevSidecar-${{ steps.package-info.outputs.version }}-arm64.dmg.blockmap" if-no-files-found: error - name: "Upload macOS zip target (x64)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'macos' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-x64.zip name: "DevSidecar-${{ steps.package-info.outputs.version }}-x64.zip" if-no-files-found: error - name: "Upload macOS zip target (arm64)" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'macos' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-arm64.zip name: "DevSidecar-${{ steps.package-info.outputs.version }}-arm64.zip" if-no-files-found: error - name: "Upload latest-linux.yml" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/latest-linux.yml @@ -574,11 +559,11 @@ jobs: - build-and-upload steps: - name: "Checkout" - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v5 - name: "Get package info" id: package-info - uses: luizfelipelaviola/get-package-info@v1 + uses: luizfelipelaviola/get-package-info@v2 with: path: ./packages/gui @@ -587,138 +572,128 @@ jobs: # Download artifacts - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-x86_64.exe" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-windows-x86_64.exe" path: release - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe" path: release - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe" path: release # ------------------------------------------------------------------------------------------------------------------------- - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.deb" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.deb" path: release - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage" path: release - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.tar.gz" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.tar.gz" path: release - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.rpm" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.rpm" path: release - - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.pkg.tar.xz" - uses: actions/download-artifact@v4.1.8 - with: - name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.pkg.tar.xz" - path: release #- name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.flatpak" - # uses: actions/download-artifact@v4.1.8 + # uses: actions/download-artifact@v7 # with: # name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.flatpak" # path: release # ------------------------------------------------------------------------------------------------------------------------- - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb" path: release - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage" path: release - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz" path: release - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.rpm" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.rpm" path: release - - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.pkg.tar.xz" - uses: actions/download-artifact@v4.1.8 - with: - name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.pkg.tar.xz" - path: release # ------------------------------------------------------------------------------------------------------------------------- - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb" path: release - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage" path: release - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz" path: release - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.rpm" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.rpm" path: release # ------------------------------------------------------------------------------------------------------------------------- - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-macos-x86_64.dmg" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-macos-x86_64.dmg" path: release - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg" path: release # region Download update ZIPs (for auto-update, per-arch) - name: "Download update ZIP (Win x64)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "update-win-x64-${{ steps.package-info.outputs.version }}.zip" path: release - name: "Download update ZIP (Win ia32)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "update-win-ia32-${{ steps.package-info.outputs.version }}.zip" path: release - name: "Download update ZIP (Win arm64)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "update-win-arm64-${{ steps.package-info.outputs.version }}.zip" path: release - name: "Download update ZIP (macOS x64)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "update-mac-x64-${{ steps.package-info.outputs.version }}.zip" path: release - name: "Download update ZIP (macOS arm64)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "update-mac-arm64-${{ steps.package-info.outputs.version }}.zip" path: release - name: "Download update ZIP (Linux x64)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "update-linux-x64-${{ steps.package-info.outputs.version }}.zip" path: release @@ -726,54 +701,54 @@ jobs: # region Download auto-update metadata files (latest*.yml + blockmaps + macOS zip target) - name: "Download latest.yml" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "latest.yml" path: release - name: "Download blockmap (Win x64)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-x64.exe.blockmap" path: release - name: "Download blockmap (Win ia32)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 continue-on-error: true with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-ia32.exe.blockmap" path: release - name: "Download blockmap (Win arm64)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 continue-on-error: true with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-arm64.exe.blockmap" path: release - name: "Download latest-mac.yml" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "latest-mac.yml" path: release - name: "Download blockmap (macOS x64)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-x64.dmg.blockmap" path: release - name: "Download blockmap (macOS arm64)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-arm64.dmg.blockmap" path: release - name: "Download macOS zip target (x64)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-x64.zip" path: release - name: "Download macOS zip target (arm64)" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "DevSidecar-${{ steps.package-info.outputs.version }}-arm64.zip" path: release - name: "Download latest-linux.yml" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: name: "latest-linux.yml" path: release diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 9c3ebf8dac..a0ab411980 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -27,13 +27,13 @@ jobs: ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/electron-builder steps: - name: "Checkout" - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v5 - name: "Setup pnpm" - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 - name: "Setup Node.js 22.x" - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 22 registry-url: https://npm.pkg.github.com/ @@ -46,7 +46,7 @@ jobs: run: pnpm --filter @docmirror/dev-sidecar-cli test - name: "Cache Node.js binaries (node-bin)" - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: packages/cli/dist/node-bin key: cli-node-bin-${{ runner.os }}-${{ hashFiles('packages/cli/scripts/build.js') }} @@ -66,7 +66,7 @@ jobs: file packages/cli/dist/ds-cli-* 2>/dev/null || true - name: "Upload artifacts" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 with: name: ds-cli-${{ github.sha }} path: | @@ -80,7 +80,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') steps: - name: "Download artifacts" - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v7 with: pattern: ds-cli-* merge-multiple: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3316db6c27..b6529ddc5f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -59,7 +59,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` diff --git a/.github/workflows/defender-for-devops.yml b/.github/workflows/defender-for-devops.yml index 12f76f47b2..ff780ae9d8 100644 --- a/.github/workflows/defender-for-devops.yml +++ b/.github/workflows/defender-for-devops.yml @@ -36,16 +36,16 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-dotnet@v5 with: dotnet-version: | 5.0.x 6.0.x - name: Run Microsoft Security DevOps - uses: microsoft/security-devops-action@v1.6.0 + uses: microsoft/security-devops-action@v1 id: msdo - name: Upload results to Security tab - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 with: sarif_file: ${{ steps.msdo.outputs.sarifFile }} diff --git a/.github/workflows/npm-run-electron.yml b/.github/workflows/npm-run-electron.yml index 5517d1c8c4..c9012ee089 100644 --- a/.github/workflows/npm-run-electron.yml +++ b/.github/workflows/npm-run-electron.yml @@ -35,23 +35,23 @@ jobs: - 22 steps: - name: "Checkout" - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 - name: "Setup pnpm" - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 - name: 'Setup Node.js "${{ matrix.node }}.x" environment' - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node }} registry-url: https://npm.pkg.github.com/ cache: pnpm - name: "Setup Python environment, because of electron-builder install-app-deps requires Python setup tools" - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" @@ -79,7 +79,7 @@ jobs: python --version; - name: "Setup electron cache" - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ github.workspace }}/.cache/electron key: ${{ runner.os }}-electron-cache-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -87,7 +87,7 @@ jobs: ${{ runner.os }}-electron-cache- - name: "Setup electron-builder cache" - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ github.workspace }}/.cache/electron-builder key: ${{ runner.os }}-electron-builder-cache-${{ hashFiles('**/pnpm-lock.yaml') }} diff --git a/.github/workflows/publish-to-aur.yml b/.github/workflows/publish-to-aur.yml index f1d846f75b..c7a14ddf3a 100644 --- a/.github/workflows/publish-to-aur.yml +++ b/.github/workflows/publish-to-aur.yml @@ -18,7 +18,7 @@ jobs: contents: read steps: - name: "Checkout" - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v5 # ── Resolve version ────────────────────────────────────────────────────── - name: "Resolve version" diff --git a/.github/workflows/test-and-upload.yml b/.github/workflows/test-and-upload.yml index e952d8ebc0..d8059ed4e2 100644 --- a/.github/workflows/test-and-upload.yml +++ b/.github/workflows/test-and-upload.yml @@ -49,29 +49,29 @@ jobs: - 22 steps: - name: "Checkout" - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 - name: "Setup pnpm" - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 - name: 'Setup Node.js "${{ matrix.node }}.x" environment' - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node }} registry-url: https://npm.pkg.github.com/ cache: pnpm - name: "Setup Python environment, because of electron-builder install-app-deps requires Python setup tools" - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: "Get package info" id: package-info - uses: luizfelipelaviola/get-package-info@v1 + uses: luizfelipelaviola/get-package-info@v2 with: path: ./packages/gui @@ -113,7 +113,7 @@ jobs: python --version; - name: "Setup electron cache" - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ github.workspace }}/.cache/electron key: ${{ runner.os }}-electron-cache-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -121,7 +121,7 @@ jobs: ${{ runner.os }}-electron-cache- - name: "Setup electron-builder cache" - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ github.workspace }}/.cache/electron-builder key: ${{ runner.os }}-electron-builder-cache-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -162,7 +162,7 @@ jobs: - name: "Cache Flatpak runtime (Linux)" if: ${{ matrix.os == 'ubuntu' }} id: cache-flatpak-runtime - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.local/share/flatpak key: flatpak-runtime-freedesktop-21.08 @@ -186,7 +186,7 @@ jobs: - name: "Cache Flatpak runtime (Linux ARM64)" if: ${{ matrix.os == 'ubuntu-24.04-arm' }} id: cache-flatpak-runtime-linux-arm64 - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.local/share/flatpak key: flatpak-runtime-freedesktop-21.08 @@ -301,21 +301,21 @@ jobs: # region Upload artifacts - Windows - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-windows-x86_64.exe" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-windows-x86_64.exe name: "DevSidecar-${{ env.BUILD_VERSION }}-windows-x86_64.exe" if-no-files-found: error - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-windows-ia32.exe" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-windows-ia32.exe name: "DevSidecar-${{ env.BUILD_VERSION }}-windows-ia32.exe" if-no-files-found: error - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-windows-arm64.exe" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-windows-arm64.exe @@ -325,35 +325,35 @@ jobs: # region Upload artifacts - Linux - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.deb" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.deb name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.deb" if-no-files-found: error - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.AppImage" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.AppImage name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.AppImage" if-no-files-found: error - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.tar.gz" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.tar.gz name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.tar.gz" if-no-files-found: error - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.rpm" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.rpm name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.rpm" if-no-files-found: error # - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.flatpak" - # uses: actions/upload-artifact@v4.4.0 + # uses: actions/upload-artifact@v6 # if: ${{ matrix.os == 'ubuntu' }} # with: # path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.flatpak @@ -361,28 +361,28 @@ jobs: # if-no-files-found: error # ------------------------------------------------------------------------------------------------------------------------- - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.deb" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.deb name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.deb" if-no-files-found: error - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.AppImage" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.AppImage name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.AppImage" if-no-files-found: error - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.tar.gz" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.tar.gz name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.tar.gz" if-no-files-found: error - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.rpm" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.rpm @@ -390,28 +390,28 @@ jobs: if-no-files-found: error # ------------------------------------------------------------------------------------------------------------------------- - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.deb" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.deb name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.deb" if-no-files-found: error - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.AppImage" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.AppImage name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.AppImage" if-no-files-found: error - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.tar.gz" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.tar.gz name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.tar.gz" if-no-files-found: error - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.rpm" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.rpm @@ -421,7 +421,7 @@ jobs: # region Upload artifacts - Linux ARM64 - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.flatpak" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu-24.04-arm' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.flatpak @@ -429,7 +429,7 @@ jobs: if-no-files-found: error # ------------------------------------------------------------------------------------------------------------------------- - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.flatpak" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'ubuntu-24.04-arm' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.flatpak @@ -439,14 +439,14 @@ jobs: # Upload artifacts - macOS - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-macos-x86_64.dmg" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'macos' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-macos-x86_64.dmg name: "DevSidecar-${{ env.BUILD_VERSION }}-macos-x86_64.dmg" if-no-files-found: error - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-macos-arm64.dmg" - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v6 if: ${{ matrix.os == 'macos' }} with: path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-macos-arm64.dmg diff --git a/README.md b/README.md index 7d7ef8135a..95a7efe281 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,7 @@ ### 6.1、dev-sidecar的前两个开关没有处于打开状态 +0. 请先检查你是否开启了其他代理软件。dev-sidecar不能与其他端口代理类软件同时使用,但host/TUN类软件应可正常工作。如您正在同时使用watt toolkit(原steam++)等软件,请将其改为host模式。 1. 尝试将开关按钮手动打开 2. 请尝试右键dev-sidecar图标,点退出。再重新打开 3. 如果还不行,请将日志发送给作者 diff --git a/_script/dev.js b/_script/dev.js new file mode 100644 index 0000000000..1e2424dfb1 --- /dev/null +++ b/_script/dev.js @@ -0,0 +1,241 @@ +#!/usr/bin/env node +/** + * dev-sidecar 开发环境统一启动/检查脚本 + * + * 用法: + * node _script\dev.js --action start + * node _script\dev.js --action check + * node _script\dev.js --action stop + * node _script\dev.js --action restart + * + * 可选参数: + * --port 8081 # 前端 dev server 端口,默认 8081 + * --foreground # start 时前台运行 + * --skip-kill # start/restart 时不先结束已占用端口的进程 + */ + +const { execSync, spawn, spawnSync } = require('child_process'); +const net = require('net'); +const path = require('path'); + +// ── 参数解析 ────────────────────────────────────────────────── +const args = process.argv.slice(2); + +function flag(name) { return args.includes(name); } +function option(name, fallback) { + const i = args.indexOf(name); + return i !== -1 && args[i + 1] ? args[i + 1] : fallback; +} + +const ACTION = option('--action', 'start'); +const DEV_PORT = parseInt(option('--port', '8081'), 10); +const FOREGROUND = flag('--foreground'); +const SKIP_KILL = flag('--skip-kill'); +const PROXY_HTTP = 31180; +const PROXY_HTTPS = 31181; +const PORTS = [DEV_PORT, PROXY_HTTP, PROXY_HTTPS]; +const TIMEOUT_SEC = 120; + +const REPO_ROOT = path.resolve(__dirname, '..'); +const GUI_DIR = path.join(REPO_ROOT, 'packages', 'gui'); +const ELECTRON_DEV = path.join(__dirname, 'electron-dev.mjs'); +const IS_WIN = process.platform === 'win32'; + +// ── 工具函数 ────────────────────────────────────────────────── +const log = (msg) => console.log(msg); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +function getNodePath() { + try { + return IS_WIN + ? execSync('where node', { encoding: 'utf8' }).trim().split(/\r?\n/)[0] + : execSync('which node', { encoding: 'utf8' }).trim(); + } catch { + console.error('node 未找到,请确认已安装 Node.js 并加入 PATH'); + process.exit(1); + } +} + +function isPortInUse(port) { + return new Promise((resolve) => { + const server = net.createServer(); + server.once('error', () => resolve(true)); + server.once('listening', () => { server.close(); resolve(false); }); + server.listen(port, '127.0.0.1'); + }); +} + +function getListeners(targetPorts) { + try { + const out = execSync( + IS_WIN + ? 'chcp 65001 >nul && netstat -ano -p tcp' + : "ss -Htlnp 2>/dev/null || netstat -tlnp 2>/dev/null", + { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } + ); + + return out.split(/\r?\n/).reduce((list, line) => { + const parts = line.trim().split(/\s+/); + let localAddr, state, pid; + + if (IS_WIN) { + [localAddr, , state, pid] = parts; + if (state !== 'LISTENING') return list; + } else { + if (parts.length >= 6) { + localAddr = parts[3]; state = parts[0]; pid = parts[5].split('/')[0]; + } else if (parts.length >= 7) { + localAddr = parts[3]; state = parts[5]; pid = parts[6].split('/')[0]; + } else return list; + if (state !== 'LISTEN' && state !== 'LISTENING') return list; + } + + const portStr = localAddr.replace(/.*:(\d+)$/, '$1'); + const port = parseInt(portStr, 10); + if (targetPorts.includes(port) && pid && pid !== '-') { + list.push({ address: localAddr, port, pid: parseInt(pid, 10) }); + } + return list; + }, []); + } catch { + return []; + } +} + +function getProcessName(pid) { + try { + return IS_WIN + ? execSync( + `wmic process where "ProcessId=${pid}" get Name /value`, + { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } + ).match(/Name=(.+)/)?.[1]?.trim() || '-' + : execSync(`ps -p ${pid} -o comm=`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + } catch { return '-'; } +} + +// ── 业务函数 ────────────────────────────────────────────────── +function showStatus(title) { + log(`== ${title} ==`); + const listeners = getListeners(PORTS) + .sort((a, b) => a.port - b.port); + if (listeners.length) { + listeners.forEach(({ address, port, pid }) => { + const name = getProcessName(pid); + log(` ${address}:${String(port).padEnd(6)} PID ${String(pid).padEnd(7)} ${name}`); + }); + } else { + log(' no listeners'); + } + log(''); +} + +async function stopProcesses() { + log('== stopping dev-sidecar =='); + const listeners = getListeners(PORTS); + const pids = [...new Set(listeners.map((l) => l.pid))]; + + if (!pids.length) { log(' no listeners\n'); return; } + + for (const pid of pids) { + try { + process.kill(pid, 'SIGTERM'); + log(` killed PID ${pid}`); + } catch { + if (IS_WIN) { + spawnSync('cmd', ['/c', 'taskkill', '/F', '/PID', String(pid), '/T'], + { stdio: 'ignore', windowsHide: true }); + log(` killed PID ${pid}`); + } + } + } + + // 等待端口释放(最多 5 秒) + for (let i = 0; i < 10; i++) { + if (!(await isPortInUse(PORTS[0]))) break; + await sleep(500); + } + log(''); +} + +async function startServer(devPort) { + const node = getNodePath(); + + if (FOREGROUND) { + log('== starting dev-sidecar in foreground (Ctrl+C to stop) =='); + const child = spawn(node, [ELECTRON_DEV, '--port', String(devPort)], + { cwd: GUI_DIR, stdio: 'inherit' }); + + if (!IS_WIN) { + process.on('SIGINT', () => { child.kill('SIGINT'); }); + process.on('SIGTERM', () => { child.kill('SIGTERM'); }); + } + child.on('exit', (code) => process.exit(code ?? 0)); + return; + } + + log('== starting dev-sidecar =='); + const child = spawn(node, [ELECTRON_DEV, '--port', String(devPort)], { + cwd: GUI_DIR, + detached: true, + stdio: 'ignore', + ...(IS_WIN && { windowsHide: true }), + }); + child.unref(); + log(` launcher PID: ${child.pid}`); + + await waitPorts(PORTS); + showStatus('port status after start'); +} + +async function waitPorts(ports, timeoutSec = TIMEOUT_SEC) { + const deadline = Date.now() + timeoutSec * 1000; + const ready = new Set(); + + while (Date.now() < deadline) { + for (const port of ports) { + if (ready.has(port)) continue; + if (!(await isPortInUse(port))) continue; + + // 双重确认:端口在用 且 netstat 能找到监听者 + const listeners = getListeners([port]); + if (listeners.length) ready.add(port); + } + if (ready.size >= ports.length) break; + await sleep(1000); + } + + if (ready.size < ports.length) { + const missing = ports.filter((p) => !ready.has(p)); + log(`等待端口超时,未就绪端口: ${missing.join(', ')}`); + } else { + log(`端口已全部就绪: ${ports.join(', ')}`); + } +} + +// ── 主流程 ──────────────────────────────────────────────────── +(async () => { + switch (ACTION) { + case 'check': + showStatus(`port status (dev: ${DEV_PORT}, http proxy: ${PROXY_HTTP}, https proxy: ${PROXY_HTTPS})`); + break; + + case 'stop': + await stopProcesses(); + showStatus('port status after stop'); + break; + + case 'start': + if (!SKIP_KILL) await stopProcesses(); + await startServer(DEV_PORT); + break; + + case 'restart': + await stopProcesses(); + await startServer(DEV_PORT); + break; + + default: + console.error(`未知 action: ${ACTION}`); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/_script/electron-dev.mjs b/_script/electron-dev.mjs index 1c2d67ff15..149891f7a8 100644 --- a/_script/electron-dev.mjs +++ b/_script/electron-dev.mjs @@ -6,7 +6,17 @@ import { setTimeout as delay } from 'node:timers/promises' const guiDir = process.cwd() const require = createRequire(import.meta.url) -const devServerUrl = 'http://localhost:8080' +function resolveDevServer () { + const argv = process.argv.slice(2) + const portIndex = argv.indexOf('--port') + const port = portIndex >= 0 ? Number.parseInt(argv[portIndex + 1], 10) : 8080 + if (!Number.isInteger(port) || port <= 0 || port > 65535) { + throw new Error(`无效的端口号: ${argv[portIndex + 1]}`) + } + return { port, url: `http://localhost:${port}` } +} + +const { port: devServerPort, url: devServerUrl } = resolveDevServer() const state = { closing: false, devServer: null, @@ -86,7 +96,7 @@ async function main () { const vueCliServiceBin = resolveVueCliServiceBin() const electronBin = resolveElectronBin() - state.devServer = spawnCommand(process.execPath, [vueCliServiceBin, 'serve', '--port', '8080']) + state.devServer = spawnCommand(process.execPath, [vueCliServiceBin, 'serve', '--port', String(devServerPort)]) state.devServer.on('exit', (code, signal) => { if (!state.closing) { void shutdown(code ?? (signal ? 1 : 0)) diff --git "a/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" "b/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" index c46b72f04b..0d05c57bab 100644 --- "a/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" +++ "b/doc/wiki/\345\212\240\351\200\237\346\234\215\345\212\241\344\275\277\347\224\250\350\257\264\346\230\216.md" @@ -176,4 +176,4 @@ TODO:内容待完善 "your-DNS-name-used-in-test2" ] } -``` +``` \ No newline at end of file diff --git a/packages/core/src/config-api.js b/packages/core/src/config-api.js index c0aed46604..07610048b4 100644 --- a/packages/core/src/config-api.js +++ b/packages/core/src/config-api.js @@ -60,7 +60,11 @@ const configApi = { if (remoteConfigUrl.startsWith('https://raw.githubusercontent.com/')) { headers['Server-Name'] = 'baidu.com' } - request(remoteConfigUrl, { headers }, (error, response, body) => { + // 禁用环境变量代理(HTTPS_PROXY/HTTP_PROXY): + // 当用户开启 proxy.setEnv 后,环境变量会指向 dev-sidecar 自己的代理端口, + // 启动时本地代理尚未监听,走代理会导致下载失败(ECONNREFUSED 127.0.0.1:31181), + // 新装用户会因此无法下载远程配置,只能使用内置规则。 + request(remoteConfigUrl, { headers, proxy: null }, (error, response, body) => { if (error) { log.error(`下载远程配置失败: ${remoteConfigUrl}, error:`, error, ', response:', response, ', body:', body) reject(error) diff --git a/packages/core/src/config/index.js b/packages/core/src/config/index.js index 4a430822c1..fa330b5477 100644 --- a/packages/core/src/config/index.js +++ b/packages/core/src/config/index.js @@ -41,6 +41,7 @@ const defaultConfig = { showShutdownTip: true, // 日志相关配置 + logDisabled: false, // 完全禁用日志:控制台不输出,日志文件也不写入 logFileSavePath: path.join(configLoader.getUserBasePath(), '/logs'), // 日志文件保存路径 keepLogFileCount: 15, // 保留日志文件数 maxLogFileSize: 1, // 最大日志文件大小 @@ -99,6 +100,11 @@ const defaultConfig = { // } }, }, + // Cloudflare 路由重定向:命中 Cloudflare IP 段时改写为优选地址 + cloudflareRoute: { + enabled: false, + preferredEndpoint: '', // 优选地址,可填写 IP 或 CNAME 域名 + }, intercept: { enabled: true, }, diff --git a/packages/core/src/expose.js b/packages/core/src/expose.js index a617cd257d..7d53906bb8 100644 --- a/packages/core/src/expose.js +++ b/packages/core/src/expose.js @@ -41,7 +41,9 @@ for (const key in modules.plugin) { start: async () => log.warn(`插件【${key}】不可用,无法启动`), stop: async () => {}, close: async () => {}, - run: async () => { throw new Error(`插件【${key}】不可用`) }, + run: async () => { + throw new Error(`插件【${key}】不可用`) + }, }), } const stubApi = setupPlugin(`plugin.${key}`, stub, context, config) @@ -55,80 +57,97 @@ config.resetDefault() const server = modules.server const serverStart = server.start -function newServerStart ({ mitmproxyPath }) { - return serverStart({ mitmproxyPath, plugins: plugin }) +function newServerStart ({ mitmproxyPath, setting }) { + return serverStart({ mitmproxyPath, plugins: plugin, setting }) } server.start = newServerStart -async function startup ({ mitmproxyPath }) { +async function startup ({ mitmproxyPath, setting }) { const conf = config.get() + const tasks = [] + + // 并行启动:服务未监听瞬间的请求失败属计划内行为(代理未开请求同样会失败) if (conf.server.enabled && !status.server.enabled) { - try { - await server.start({ mitmproxyPath }) - } catch (err) { - log.error('代理服务启动失败:', err) - } + tasks.push((async () => { + try { + await server.start({ mitmproxyPath, setting }) + } catch (err) { + log.error('代理服务启动失败:', err) + } + })()) } if (conf.proxy.enabled && !status.proxy.enabled) { - try { - await proxy.start() - } catch (err) { - log.error('开启系统代理失败:', err) - } + tasks.push((async () => { + try { + await proxy.start() + } catch (err) { + log.error('开启系统代理失败:', err) + } + })()) } + try { - const plugins = [] for (const key in plugin) { if (conf.plugin[key].enabled && !status.plugin[key]?.enabled) { - const start = async () => { + if (key === 'overwall' && setting && setting.overwall !== true) { + log.info(`插件【${key}】未启动:setting.json 未开启 overwall`) + continue + } + tasks.push((async () => { try { await plugin[key].start() log.info(`插件【${key}】已启动`) } catch (err) { log.error(`插件【${key}】启动失败:`, err) } - } - plugins.push(start()) + })()) } } - if (plugins && plugins.length > 0) { - await Promise.all(plugins) - } } catch (err) { log.error('开启插件失败:', err) } + + if (tasks.length > 0) { + // 系统代理与各插件之间没有相互依赖,并行启动以缩短整体等待时间 + await Promise.all(tasks) + } } async function shutdown () { + const tasks = [] + try { - const plugins = [] for (const key in plugin) { if (status.plugin[key] && status.plugin[key].enabled && plugin[key].close) { - const close = async () => { + tasks.push((async () => { try { await plugin[key].close() log.info(`插件【${key}】已关闭`) } catch (err) { log.error(`插件【${key}】关闭失败:`, err) } - } - plugins.push(close()) + })()) } } - if (plugins.length > 0) { - await Promise.all(plugins) - } } catch (error) { log.error('插件关闭失败:', error) } if (status.proxy.enabled) { - try { - await proxy.close() - log.info('系统代理已关闭') - } catch (err) { - log.error('系统代理关闭失败:', err) - } + tasks.push((async () => { + try { + await proxy.close() + log.info('系统代理已关闭') + } catch (err) { + log.error('系统代理关闭失败:', err) + } + })()) } + + if (tasks.length > 0) { + // 插件关闭(清理 git/npm 配置)与关闭系统代理互不依赖,并行执行 + await Promise.all(tasks) + } + if (status.server.enabled) { try { await server.close() diff --git a/packages/core/src/modules/plugin/node/config.js b/packages/core/src/modules/plugin/node/config.js index 9a02451a51..ab6544a68d 100644 --- a/packages/core/src/modules/plugin/node/config.js +++ b/packages/core/src/modules/plugin/node/config.js @@ -30,15 +30,16 @@ module.exports = { }, }, }, - variables: { - phantomjs_cdnurl: 'https://npmmirror.com/mirrors/phantomjs', - chromedriver_cdnurl: 'https://npmmirror.com/mirrors/chromedriver', - sass_binary_site: 'https://npmmirror.com/mirrors/node-sass', + // npm 11 开始不再接受未知的 npm config,以下镜像变量全部改为直接设置系统环境变量 + variables: {}, + envVariables: { ELECTRON_MIRROR: 'https://npmmirror.com/mirrors/electron/', - NVM_NODEJS_ORG_MIRROR: 'https://npmmirror.com/mirrors/node', + ELECTRON_BUILDER_BINARIES_MIRROR: 'https://npmmirror.com/mirrors/electron-builder-binaries/', + PHANTOMJS_CDNURL: 'https://npmmirror.com/mirrors/phantomjs', CHROMEDRIVER_CDNURL: 'https://npmmirror.com/mirrors/chromedriver', + SASS_BINARY_SITE: 'https://npmmirror.com/mirrors/node-sass', + NVM_NODEJS_ORG_MIRROR: 'https://npmmirror.com/mirrors/node', OPERADRIVER: 'https://npmmirror.com/mirrors/operadriver', - ELECTRON_BUILDER_BINARIES_MIRROR: 'https://npmmirror.com/mirrors/electron-builder-binaries/', PYTHON_MIRROR: 'https://npmmirror.com/mirrors/python', }, } diff --git a/packages/core/src/modules/plugin/node/index.js b/packages/core/src/modules/plugin/node/index.js index 1f060cdaab..1c4cc4f274 100644 --- a/packages/core/src/modules/plugin/node/index.js +++ b/packages/core/src/modules/plugin/node/index.js @@ -1,6 +1,31 @@ +const fs = require('node:fs') +const path = require('node:path') const jsonApi = require('@docmirror/mitmproxy/src/json') const nodeConfig = require('./config') +function getUserNpmrcPath () { + return path.join(process.env.USERPROFILE || process.env.HOME || '', '.npmrc') +} + +function isKeyInUserNpmrc (key) { + try { + if (!fs.existsSync(getUserNpmrcPath())) { + return false + } + const content = fs.readFileSync(getUserNpmrcPath(), 'utf8') + const target = key.toLowerCase() + return content.split(/\r?\n/).some((line) => { + const index = line.indexOf('=') + if (index <= 0) { + return false + } + return line.slice(0, index).trim().toLowerCase() === target + }) + } catch { + return false + } +} + const NodePlugin = function (context) { const { config, shell, event, log } = context const nodeApi = { @@ -43,25 +68,35 @@ const NodePlugin = function (context) { async setNpmEnv (list) { const command = config.get().plugin.node.setting.command || 'npm' - const cmds = [] + // npm config set 支持一次设置多个 key=value,合并成一条命令,避免启动多个 npm 进程拖慢速度 + const setArgs = [] + const deleteKeys = [] for (const item of list) { if (item.value != null && item.value.length > 0 && item.value !== 'default' && item.value !== 'null') { - cmds.push(`${command} config set ${item.key} ${item.value}`) + setArgs.push(`${item.key}=${item.value}`) } else { - cmds.push(`${command} config delete ${item.key}`) + deleteKeys.push(item.key) } } + + const cmds = [] + if (setArgs.length > 0) { + cmds.push(`${command} config set ${setArgs.join(' ')}`) + } + if (deleteKeys.length > 0) { + cmds.push(`${command} config delete ${deleteKeys.join(' ')}`) + } return await shell.exec(cmds, { type: 'cmd' }) }, async unsetNpmEnv (list) { const command = config.get().plugin.node.setting.command || 'npm' - const cmds = [] - for (const item of list) { - cmds.push(`${command} config delete ${item} `) - } - return await shell.exec(cmds, { type: 'cmd' }) + // npm config delete 支持一次删除多个 key,合并成一条命令 + const keys = list.map((item) => { + return typeof item === 'string' ? item : item.key + }).filter(Boolean) + return await shell.exec([`${command} config delete ${keys.join(' ')}`], { type: 'cmd' }) }, async setYarnEnv (list) { @@ -99,16 +134,57 @@ const NodePlugin = function (context) { hadSet: currentMap[key] === map[key], }) } + + // 环境变量形式的镜像配置,直接检查 process.env + const envMap = config.get().plugin.node.envVariables || {} + const defaultEnvMap = nodeConfig.envVariables || {} + for (const key in envMap) { + const oldValue = process.env[key] + list.push({ + key, + value: envMap[key], + oldValue, + exists: oldValue != null, + hadSet: oldValue === envMap[key], + defaultValue: defaultEnvMap[key], + }) + } return list }, async setVariables () { - const list = await nodeApi.getVariables() - const noSetList = list.filter((item) => { - return !item.exists + const nodeConfig = config.get().plugin.node + const envMap = nodeConfig.envVariables || {} + const variables = nodeConfig.variables || {} + + // 清理旧版通过 npm config 写入的镜像变量,避免 npm 未知配置告警。 + // 只有用户级 .npmrc 中确实存在这些 key 时才启动 npm 执行删除,避免每次启动/应用都白跑一遍 npm。 + const staleKeys = Object.keys(envMap).filter((key) => isKeyInUserNpmrc(key)) + if (staleKeys.length > 0) { + try { + await nodeApi.unsetNpmEnv(staleKeys) + } catch (e) { + log.warn('删除旧的 npm config 镜像变量失败:', e) + } + } + + // 每次启动/应用时都强制重写所有镜像环境变量,覆盖手动修改的值 + const envList = Object.keys(envMap).map((key) => { + return { key, value: envMap[key] } }) - if (noSetList.length > 0) { - return nodeApi.setNpmEnv(noSetList) + if (envList.length > 0) { + await shell.setSystemEnv({ list: envList }) + } + + // 其余非环境变量形式的 npm config 变量(当前默认为空),仅在确实配置了 variables 时才查询 npm + if (Object.keys(variables).length > 0) { + const list = await nodeApi.getVariables() + const noSetList = list.filter((item) => { + return !item.hadSet && envMap[item.key] == null + }) + if (noSetList.length > 0) { + await nodeApi.setNpmEnv(noSetList) + } } }, @@ -124,9 +200,10 @@ const NodePlugin = function (context) { async setProxy (ip, port) { const command = config.get().plugin.node.setting.command || 'npm' - const cmds = [ - `${command} config set proxy=http://${ip}:${port - 1}`, - `${command} config set https-proxy=http://${ip}:${port}`, + // npm config set 支持一次设置多个 key=value,合并成一条命令 + const setArgs = [ + `proxy=http://${ip}:${port - 1}`, + `https-proxy=http://${ip}:${port}`, ] const env = [] @@ -140,23 +217,23 @@ const NodePlugin = function (context) { const nodeConfig = config.get().plugin.node const rootCaCertFile = config.get().server.setting.rootCaFile.certPath if (nodeConfig.setting['strict-ssl']) { - cmds.push(`${command} config set strict-ssl false`) + setArgs.push('strict-ssl=false') } if (nodeConfig.setting.cafile) { - cmds.push(`${command} config set cafile "${rootCaCertFile}"`) + setArgs.push(`cafile=${rootCaCertFile}`) } if (nodeConfig.setting.NODE_EXTRA_CA_CERTS) { - cmds.push(`${command} config set NODE_EXTRA_CA_CERTS "${rootCaCertFile}"`) + setArgs.push(`NODE_EXTRA_CA_CERTS=${rootCaCertFile}`) env.push({ key: 'NODE_EXTRA_CA_CERTS', value: rootCaCertFile }) } if (nodeConfig.setting.NODE_TLS_REJECT_UNAUTHORIZED) { - cmds.push(`${command} config set NODE_TLS_REJECT_UNAUTHORIZED 0`) + setArgs.push('NODE_TLS_REJECT_UNAUTHORIZED=0') env.push({ key: 'NODE_TLS_REJECT_UNAUTHORIZED', value: '0' }) } - const ret = await shell.exec(cmds, { type: 'cmd' }) + const ret = await shell.exec([`${command} config set ${setArgs.join(' ')}`], { type: 'cmd' }) if (env.length > 0) { await shell.setSystemEnv({ list: env }) } @@ -169,15 +246,8 @@ const NodePlugin = function (context) { async unsetProxy () { const command = config.get().plugin.node.setting.command || 'npm' - const cmds = [ - `${command} config delete proxy`, - `${command} config delete https-proxy`, - `${command} config delete strict-ssl`, - `${command} config delete cafile`, - `${command} config delete NODE_EXTRA_CA_CERTS`, - `${command} config delete NODE_TLS_REJECT_UNAUTHORIZED`, - ] - const ret = await shell.exec(cmds, { type: 'cmd' }) + // npm config delete 支持一次删除多个 key,合并成一条命令 + const ret = await shell.exec([`${command} config delete proxy https-proxy strict-ssl cafile NODE_EXTRA_CA_CERTS NODE_TLS_REJECT_UNAUTHORIZED`], { type: 'cmd' }) event.fire('status', { key: 'plugin.node.enabled', value: false }) log.info('关闭【NPM】代理成功') return ret diff --git a/packages/core/src/modules/plugin/overwall/config.js b/packages/core/src/modules/plugin/overwall/config.js index 870cae95d5..80209f8f03 100644 --- a/packages/core/src/modules/plugin/overwall/config.js +++ b/packages/core/src/modules/plugin/overwall/config.js @@ -1,9 +1,11 @@ module.exports = { - name: '梯子', - enabled: false, // 默认关闭梯子 + name: '增强功能', + enabled: false, // 默认关闭 + restartServer: true, // 首页开关切换后需要重启代理服务才能生效 server: {}, serverDefault: { 'ow-prod.docmirror.top': { + id: 0, // 自带默认服务器 ID 固定为 0,用户新增服务器依次为 1、2、3... port: 443, path: 'X2dvX292ZXJfd2FsbF8', password: 'dev_sidecar_is_666', diff --git a/packages/core/src/modules/server/index.js b/packages/core/src/modules/server/index.js index c27a56ad47..f43d9c58b3 100644 --- a/packages/core/src/modules/server/index.js +++ b/packages/core/src/modules/server/index.js @@ -19,6 +19,16 @@ function sleep (time) { }, time) }) } + +function onceExit (child) { + return new Promise((resolve) => { + if (child.exitCode != null || child.signalCode != null) { + resolve(true) + return + } + child.once('exit', () => resolve(true)) + }) +} const serverApi = { async startup () { if (config.get().server.startup) { @@ -30,7 +40,7 @@ const serverApi = { return this.close() } }, - async start ({ mitmproxyPath, plugins }) { + async start ({ mitmproxyPath, plugins, setting }) { // 防止重复启动:如果已有子进程存活,直接返回 if (server && server.process && !server.process.killed && server.process.exitCode == null) { log.warn('server is already running, skip start (pid:', server.id, ')') @@ -73,7 +83,11 @@ const serverApi = { plugin.overrideRunningConfig(serverConfig) } } - serverConfig.plugin = allConfig.plugin + serverConfig.plugin = lodash.cloneDeep(allConfig.plugin || {}) + if (setting && setting.overwall !== true && serverConfig.plugin.overwall) { + // setting.json 未开启 overwall 时,增强功能插件不生效 + serverConfig.plugin.overwall.enabled = false + } if (allConfig.proxy && allConfig.proxy.enabled) { serverConfig.proxy = allConfig.proxy @@ -137,23 +151,34 @@ const serverApi = { event.fire('error', { key: 'server', value: code, error: msg.event, message: msg.message }) } else if (msg.type === 'speed') { event.fire('speed', msg.event) + } else if (msg.type === 'traffic') { + event.fire('traffic', msg.event) } }) return { port: serverConfig.port } }, async kill () { if (server) { - server.process.kill('SIGINT') - await sleep(1000) + const child = server.process + if (child.exitCode == null && child.signalCode == null) { + const exited = onceExit(child) + child.kill('SIGINT') + const exitedBySigint = await Promise.race([exited, sleep(1000).then(() => false)]) + if (!exitedBySigint) { + log.warn('server process 未在 1 秒内响应 SIGINT,尝试强制结束') + child.kill('SIGKILL') + await Promise.race([exited, sleep(1000).then(() => false)]) + } + } } fireStatus(false) }, async close () { return await serverApi.kill() }, - async restart ({ mitmproxyPath }) { + async restart ({ mitmproxyPath, setting }) { await serverApi.kill() - await serverApi.start({ mitmproxyPath }) + await serverApi.start({ mitmproxyPath, setting }) }, getServer () { return server diff --git a/packages/core/src/shell/scripts/set-system-env.js b/packages/core/src/shell/scripts/set-system-env.js index e9721c72a7..8a28a62096 100644 --- a/packages/core/src/shell/scripts/set-system-env.js +++ b/packages/core/src/shell/scripts/set-system-env.js @@ -1,88 +1,352 @@ /** * 设置环境变量 + * - Windows: HKCU\Environment + WM_SETTINGCHANGE 广播 + * - macOS: launchctl setenv(会话立即生效)+ LaunchAgent(重启后恢复) + * - Linux: systemd user environment.d(有 systemd 时)+ shell profile 兜底 */ +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') +const { execFile } = require('node:child_process') +const Registry = require('winreg') const Shell = require('../shell') +const log = require('../../utils/util.log.core') const execute = Shell.execute +const MAC_LAUNCH_AGENT_LABEL = 'com.docmirror.dev-sidecar.env' +const POSIX_MARKER_START = '# >>> dev-sidecar env >>>' +const POSIX_MARKER_END = '# <<< dev-sidecar env <<<' + +function injectProcessEnv (list) { + for (const item of list) { + if (item.value == null) { + delete process.env[item.key] + } else { + process.env[item.key] = String(item.value) + } + } +} + +function execFileAsync (file, args) { + return new Promise((resolve, reject) => { + execFile(file, args, { windowsHide: true }, (error, stdout, stderr) => { + if (error) { + error.stderr = stderr + reject(error) + } else { + resolve(stdout || '') + } + }) + }) +} + +function escapeXml (value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') +} + +// --------------------------------------------------------------------------- +// macOS: launchctl + LaunchAgent +// --------------------------------------------------------------------------- + +function macLaunchAgentPlistPath () { + return path.join(os.homedir(), 'Library/LaunchAgents', `${MAC_LAUNCH_AGENT_LABEL}.plist`) +} + +function buildMacLaunchAgentPlist (list) { + // 登录时由该 Agent 执行 launchctl setenv,把变量写入用户 launchd 域 + // (EnvironmentVariables 只作用于 agent 自身,不能给全会话用) + const commands = list + .filter(item => item.value != null) + .map((item) => { + const value = String(item.value).replace(/'/g, `'\\''`) + return `/bin/launchctl setenv ${item.key} '${value}'` + }) + const script = commands.length > 0 ? commands.join('; ') : '/bin/true' + + return ` + + + + Label + ${MAC_LAUNCH_AGENT_LABEL} + ProgramArguments + + /bin/sh + -c + ${escapeXml(script)} + + RunAtLoad + + + +` +} + +async function applyMacEnv (list) { + const results = { launchctl: [], launchAgent: null } + + // 1) 立即写入当前用户 launchd 域:之后新启动的 GUI/多数进程可继承 + for (const item of list) { + if (item.value == null) { + await execFileAsync('launchctl', ['unsetenv', item.key]) + results.launchctl.push({ key: item.key, action: 'unsetenv' }) + } else { + await execFileAsync('launchctl', ['setenv', item.key, String(item.value)]) + results.launchctl.push({ key: item.key, action: 'setenv' }) + } + } + + // 2) LaunchAgent 持久化:重启/重新登录后由 launchd 自动加载 + const plistPath = macLaunchAgentPlistPath() + fs.mkdirSync(path.dirname(plistPath), { recursive: true }) + fs.writeFileSync(plistPath, buildMacLaunchAgentPlist(list), 'utf-8') + results.launchAgent = plistPath + + try { + await execFileAsync('launchctl', ['unload', '-w', plistPath]) + } catch { + // 尚未加载时 unload 可能失败,忽略 + } + await execFileAsync('launchctl', ['load', '-w', plistPath]) + + injectProcessEnv(list) + return results +} + +async function removeMacEnv (keys) { + for (const key of keys) { + try { + await execFileAsync('launchctl', ['unsetenv', key]) + } catch { + // ignore + } + } + const plistPath = macLaunchAgentPlistPath() + if (fs.existsSync(plistPath)) { + try { + await execFileAsync('launchctl', ['unload', '-w', plistPath]) + } catch { + // ignore + } + try { + fs.unlinkSync(plistPath) + } catch { + // ignore + } + } +} + +// --------------------------------------------------------------------------- +// Linux: systemd environment.d 优先,否则 shell profile +// --------------------------------------------------------------------------- + +function detectShellProfilePath () { + const home = os.homedir() + const shellName = path.basename(process.env.SHELL || '').toLowerCase() + + if (shellName === 'zsh') { + return path.join(home, '.zshrc') + } + if (shellName === 'bash') { + const bashrc = path.join(home, '.bashrc') + return fs.existsSync(bashrc) ? bashrc : path.join(home, '.profile') + } + if (shellName === 'fish') { + return path.join(home, '.config/fish/conf.d/dev-sidecar.fish') + } + if (shellName === 'ksh') { + return path.join(home, '.kshrc') + } + const profile = path.join(home, '.profile') + return fs.existsSync(profile) ? profile : path.join(home, '.bashrc') +} + +function quoteShellValue (value) { + return `'${String(value).replace(/'/g, `'\\''`)}'` +} + +function stripManagedBlock (content) { + const start = content.indexOf(POSIX_MARKER_START) + if (start < 0) { + return content + } + const end = content.indexOf(POSIX_MARKER_END, start) + if (end < 0) { + return content.slice(0, start).replace(/\n+$/, '\n') + } + const after = content.slice(end + POSIX_MARKER_END.length).replace(/^\n/, '') + return content.slice(0, start).replace(/\n+$/, '\n') + after +} + +function writeLinuxShellProfile (list) { + const profilePath = detectShellProfilePath() + const isFish = profilePath.endsWith('.fish') + fs.mkdirSync(path.dirname(profilePath), { recursive: true }) + + let original = '' + if (fs.existsSync(profilePath)) { + original = fs.readFileSync(profilePath, 'utf-8') + } + const cleaned = stripManagedBlock(original) + + const lines = isFish + ? list.map((item) => { + if (item.value == null) { + return `set -e ${item.key}` + } + return `set -gx ${item.key} ${quoteShellValue(item.value)}` + }) + : list.map((item) => { + if (item.value == null) { + return `unset ${item.key}` + } + return `export ${item.key}=${quoteShellValue(item.value)}` + }) + + const block = [ + '', + POSIX_MARKER_START, + '# managed by dev-sidecar, do not edit this block manually', + ...lines, + POSIX_MARKER_END, + '', + ].join('\n') + + const next = cleaned.endsWith('\n') || cleaned === '' ? `${cleaned}${block}` : `${cleaned}\n${block}` + fs.writeFileSync(profilePath, next, 'utf-8') + return profilePath +} + +function writeLinuxEnvironmentD (list) { + const confDir = path.join(os.homedir(), '.config/environment.d') + const confPath = path.join(confDir, '90-dev-sidecar.conf') + fs.mkdirSync(confDir, { recursive: true }) + + // systemd environment.d:KEY=value,无 export;重启/新 user session 生效 + const lines = list.map((item) => { + if (item.value == null) { + // 空值在 environment.d 无法 unset,写空字符串近似 + return `${item.key}=` + } + // 简单转义:值中空格/引号用双引号 + const value = String(item.value) + if (/[\s"'\\$]/.test(value)) { + return `${item.key}="${value.replace(/(["\\$])/g, '\\$1')}"` + } + return `${item.key}=${value}` + }) + + const content = [ + '# Managed by dev-sidecar. systemd user environment.d', + ...lines, + '', + ].join('\n') + fs.writeFileSync(confPath, content, 'utf-8') + return confPath +} + +function hasSystemdUserSession () { + return process.platform === 'linux' && !!process.env.XDG_RUNTIME_DIR && process.env.XDG_RUNTIME_DIR.startsWith('/run/user/') +} + +// --------------------------------------------------------------------------- + const executor = { async windows (exec, { list }) { - const psCmdsMachine = [] - const psCmdsUser = [] - for (const item of list) { - const v = item.value == null ? '' : String(item.value) - // escape single quotes for PowerShell single-quoted string - const escaped = v.replace(/'/g, "''") - psCmdsMachine.push(`[Environment]::SetEnvironmentVariable('${item.key}', '${escaped}', 'Machine')`) - psCmdsUser.push(`[Environment]::SetEnvironmentVariable('${item.key}', '${escaped}', 'User')`) + const regKey = new Registry({ + hive: Registry.HKCU, + key: '\\Environment', + }) + + const setItem = (item) => { + const value = item.value == null ? '' : String(item.value) + return new Promise((resolve, reject) => { + regKey.set(item.key, Registry.REG_SZ, value, (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) } - // Wrap whole flow and return structured result so caller can show detailed errors try { - let ret - let appliedScope = null + for (const item of list) { + await setItem(item) + } + + // 广播环境变量变更(setx 一个临时值触发 WM_SETTINGCHANGE) try { - ret = await exec(psCmdsMachine, { type: 'ps' }) - appliedScope = 'Machine:PowerShell' - } catch (eMachine) { - // try User scope via PowerShell - try { - ret = await exec(psCmdsUser, { type: 'ps' }) - appliedScope = 'User:PowerShell' - } catch (eUser) { - // PowerShell attempts failed; fallback to setx via cmd - const cmdsSetxMachine = [] - const cmdsSetxUser = [] - for (const item of list) { - const v = item.value == null ? '' : String(item.value) - // escape backslashes first, then double quotes (cmd.exe quoting order) - const escaped = v.replace(/\\/g, '\\\\').replace(/"/g, '\\"') - cmdsSetxMachine.push(`setx ${item.key} "${escaped}" /M`) - cmdsSetxUser.push(`setx ${item.key} "${escaped}"`) - } - try { - ret = await exec(cmdsSetxMachine, { type: 'cmd' }) - appliedScope = 'Machine:setx' - } catch (eSetxMachine) { - try { - ret = await exec(cmdsSetxUser, { type: 'cmd' }) - appliedScope = 'User:setx' - } catch (eSetxUser) { - // all attempts failed — include all error messages - const combined = [eMachine, eUser, eSetxMachine, eSetxUser].map(e => (e && e.message) || String(e)).join(' | ') - return { success: false, error: 'Failed to set environment variables', details: combined } - } - } - } + await exec('setx DS_REFRESH "1"', { type: 'cmd' }) + } catch { + // 广播失败不影响主流程 } - // inject into current process so subsequent exec/child processes can inherit immediately let envUpdateError = null try { - for (const item of list) { - if (item.value == null) { - delete process.env[item.key] - } else { - process.env[item.key] = String(item.value) - } - } + injectProcessEnv(list) } catch (e) { envUpdateError = e.message || String(e) } - return { success: true, scope: appliedScope, output: ret, envUpdateError } - } catch (finalErr) { - return { success: false, error: finalErr.message || String(finalErr), details: finalErr.stack } + return { success: true, scope: 'User:winreg', envUpdateError } + } catch (e) { + return { success: false, error: 'Failed to set environment variables', details: e.message || String(e) } } }, - async linux (exec, { port }) { - throw new Error('暂未实现此功能') + + async linux (exec, { list }) { + try { + const applied = [] + let primaryPath = null + + if (hasSystemdUserSession()) { + primaryPath = writeLinuxEnvironmentD(list) + applied.push(`systemd-environment.d:${primaryPath}`) + try { + // 尽量让当前 systemd user 立刻重载;失败不影响写入 + await execFileAsync('systemctl', ['--user', 'daemon-reload']) + } catch { + // ignore + } + } + + // shell profile 兜底:无 systemd 会话 / 终端内 npm 等场景仍需要 + const profilePath = writeLinuxShellProfile(list) + applied.push(`shell-profile:${profilePath}`) + + injectProcessEnv(list) + log.info(`Linux 环境变量已写入: ${applied.join(', ')}(当前进程已生效)`) + return { success: true, scope: applied.join('|'), primaryPath } + } catch (e) { + return { success: false, error: 'Failed to set environment variables', details: e.message || String(e) } + } }, - async mac (exec, { port }) { - throw new Error('暂未实现此功能') + + async mac (exec, { list }) { + try { + const results = await applyMacEnv(list) + log.info(`macOS 环境变量已通过 launchctl setenv + LaunchAgent 写入: ${results.launchAgent}`) + return { + success: true, + scope: 'User:launchctl+LaunchAgent', + launchAgent: results.launchAgent, + } + } catch (e) { + return { success: false, error: 'Failed to set environment variables', details: e.message || String(e) } + } }, } module.exports = async function (args) { return execute(executor, args) } + +// 供关闭代理时清理 macOS 残留(可选调用) +module.exports.removeMacEnv = removeMacEnv diff --git a/packages/core/src/shell/scripts/set-system-proxy/index.js b/packages/core/src/shell/scripts/set-system-proxy/index.js index 65ba6e536a..11be00233b 100644 --- a/packages/core/src/shell/scripts/set-system-proxy/index.js +++ b/packages/core/src/shell/scripts/set-system-proxy/index.js @@ -25,12 +25,112 @@ function getDomesticDomainAllowListTmpFilePath () { return path.join(config.get().server.setting.userBasePath, '/domestic-domain-allowlist.txt') } +// 通过 HKCU\Environment 注册表直接写入/删除环境变量,避免每次 setx 都启动 PowerShell 造成数秒卡顿 +function createEnvRegKey () { + return new Registry({ + hive: Registry.HKCU, + key: '\\Environment', + }) +} + +function setWindowsEnvVariable (regKey, key, value) { + return new Promise((resolve, reject) => { + regKey.set(key, Registry.REG_SZ, value == null ? '' : String(value), (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} + +function removeWindowsEnvVariable (regKey, key) { + return new Promise((resolve) => { + regKey.remove(key, (removeErr) => { + resolve(!removeErr) + }) + }) +} + +function getWindowsEnvVariable (regKey, key) { + return new Promise((resolve) => { + regKey.get(key, (err, item) => { + if (err || !item) { + resolve(null) + } else { + resolve(item.value) + } + }) + }) +} + +// 记录开启代理前的环境变量值,关闭时恢复,避免覆盖用户/其他工具已有配置 +function getProxyEnvBackupPath () { + loadConfig() + return path.join(config.get().server.setting.userBasePath, '/proxy-env-backup.json') +} + +async function saveProxyEnvBackup (regKey, keys) { + try { + const backup = {} + for (const key of keys) { + backup[key] = await getWindowsEnvVariable(regKey, key) + } + fs.writeFileSync(getProxyEnvBackupPath(), JSON.stringify(backup), 'utf-8') + } catch (e) { + log.warn('备份代理环境变量原值失败:', e) + } +} + +function restoreProxyEnvBackup () { + const backupPath = getProxyEnvBackupPath() + if (!fs.existsSync(backupPath)) { + // 无备份:退回旧行为(直接删除),保证兼容 + return null + } + try { + const backup = JSON.parse(fs.readFileSync(backupPath, 'utf-8')) + fs.unlinkSync(backupPath) + return backup + } catch (e) { + log.warn('读取代理环境变量备份失败:', e) + return null + } +} + +async function broadcastWindowsEnvChange (exec) { + try { + await exec('setx DS_REFRESH "1"', { type: 'cmd' }) + } catch { + // 广播失败不影响主流程 + } +} + +async function setWindowsEnvVariables (exec, envList) { + if (!envList || envList.length === 0) { + return + } + + const regKey = createEnvRegKey() + // 仅在尚无备份时保存原值:重复 enable 时不能把 DS 注入值再当成“用户原值” + if (!fs.existsSync(getProxyEnvBackupPath())) { + await saveProxyEnvBackup(regKey, envList.map(item => item.key)) + } + for (const item of envList) { + await setWindowsEnvVariable(regKey, item.key, item.value) + process.env[item.key] = String(item.value) + } + await broadcastWindowsEnvChange(exec) +} + async function downloadDomesticDomainAllowListAsync () { loadConfig() const remoteFileUrl = config.get().proxy.remoteDomesticDomainAllowListFileUrl log.info('开始下载远程 domestic-domain-allowlist.txt 文件:', remoteFileUrl) - request(remoteFileUrl, (error, response, body) => { + // 禁用环境变量代理:防止走 dev-sidecar 自己的代理(127.0.0.1:31181)导致启动时下载失败 + request(remoteFileUrl, { proxy: null }, (error, response, body) => { if (error) { log.error(`下载远程 domestic-domain-allowlist.txt 文件失败: ${remoteFileUrl}, error:`, error, ', response:', response, ', body:', body) return @@ -458,18 +558,27 @@ const executor = { log.info('开始设置windows系统代理:', ip, port, setEnv) - // https - let proxyAddr = `https=http://${ip}:${port}` - // http - if (config.get().proxy.proxyHttp) { - proxyAddr = `http=http://${ip}:${port - 1};${proxyAddr}` + const sysproxy = require('@starknt/sysproxy') + const proxyHttp = config.get().proxy.proxyHttp + + // 同时代理 HTTP+HTTPS 时需要两个端口,使用 WinINET 协议格式(协议=地址:端口); + // 只代理 HTTPS 时,地址和端口分开传参,Windows 会按规范分别填入“地址”和“端口”两个框。 + let proxyAddr + if (proxyHttp) { + proxyAddr = `http=${ip}:${port - 1};https=${ip}:${port}` + } else { + proxyAddr = `${ip}:${port}` } // 读取排除域名 const excludeIpStr = getProxyExcludeIpStr(';') // 设置代理,同时设置排除域名 try { - require('@starknt/sysproxy').triggerManualProxyByUrl(true, proxyAddr, excludeIpStr, true) + if (proxyHttp) { + sysproxy.triggerManualProxyByUrl(true, proxyAddr, excludeIpStr, true) + } else { + sysproxy.triggerManualProxy(true, ip, port, excludeIpStr) + } log.info(`设置windows系统代理成功: ${proxyAddr} ......(省略排除IP列表)`) } catch (e1) { log.warn('设置windows系统代理失败:执行 `@starknt/sysproxy` 失败,现尝试通过执行 `sysproxy.exe global ...` 来设置系统代理!\r\n捕获的异常:', e1) @@ -488,23 +597,24 @@ const executor = { if (setEnv) { // 设置全局代理所需的环境变量 try { - await exec(`echo '设置环境变量 HTTPS_PROXY${config.get().proxy.proxyHttp ? '、HTTP_PROXY' : ''}${setCaBundle ? '、REQUEST_CA_BUNDLE' : ''}'`) - - log.info(`开启系统代理的同时设置环境变量:HTTPS_PROXY = "http://${ip}:${port}/"`) - await exec(`setx HTTPS_PROXY "http://${ip}:${port}/"`) + const envList = [] + const httpsProxy = `http://${ip}:${port}/` + log.info(`开启系统代理的同时设置环境变量:HTTPS_PROXY = "${httpsProxy}"`) + envList.push({ key: 'HTTPS_PROXY', value: httpsProxy }) if (config.get().proxy.proxyHttp) { - log.info(`开启系统代理的同时设置环境变量:HTTP_PROXY = "http://${ip}:${port - 1}/"`) - await exec(`setx HTTP_PROXY "http://${ip}:${port - 1}/"`) + const httpProxy = `http://${ip}:${port - 1}/` + log.info(`开启系统代理的同时设置环境变量:HTTP_PROXY = "${httpProxy}"`) + envList.push({ key: 'HTTP_PROXY', value: httpProxy }) } if (setCaBundle) { const caCertPath = config.get().server.setting.rootCaFile.certPath log.info(`开启系统代理的同时设置环境变量:REQUEST_CA_BUNDLE = "${caCertPath}"`) - await exec(`setx REQUEST_CA_BUNDLE "${caCertPath}"`) + envList.push({ key: 'REQUEST_CA_BUNDLE', value: caCertPath }) } - // await addClearScriptIni() + await setWindowsEnvVariables(exec, envList) } catch (e) { log.error('设置环境变量 HTTPS_PROXY、HTTP_PROXY、REQUEST_CA_BUNDLE 失败:', e) } @@ -530,42 +640,31 @@ const executor = { } try { - await exec('echo \'删除环境变量 HTTPS_PROXY、HTTP_PROXY、REQUEST_CA_BUNDLE\'') - const regKey = new Registry({ // new operator is optional - hive: Registry.HKCU, // open registry hive HKEY_CURRENT_USER - key: '\\Environment', // key containing autostart programs - }) - regKey.get('HTTPS_PROXY', (err) => { - if (!err) { - regKey.remove('HTTPS_PROXY', async (err) => { - if (err) { - log.warn('删除环境变量 HTTPS_PROXY 失败:', err) - } else { - await exec('setx DS_REFRESH "1"') - } - }) - } - }) - regKey.get('HTTP_PROXY', (err) => { - if (!err) { - regKey.remove('HTTP_PROXY', async (err) => { - if (err) { - log.warn('删除环境变量 HTTP_PROXY 失败:', err) - } - }) - } - }) - regKey.get('REQUEST_CA_BUNDLE', (err) => { - if (!err) { - regKey.remove('REQUEST_CA_BUNDLE', async (err) => { - if (err) { - log.warn('删除环境变量 REQUEST_CA_BUNDLE 失败:', err) - } - }) + const defaultKeys = ['HTTPS_PROXY', 'HTTP_PROXY', 'REQUEST_CA_BUNDLE'] + const backup = restoreProxyEnvBackup() + // 有备份时只处理备份中的 key,避免误删用户本来就有、但 DS 未写入的变量 + const keys = backup ? Object.keys(backup) : defaultKeys + const regKey = createEnvRegKey() + let changed = false + for (const key of keys) { + const previous = backup ? backup[key] : undefined + if (backup && previous != null) { + await setWindowsEnvVariable(regKey, key, previous) + process.env[key] = String(previous) + changed = true + } else { + const existed = await removeWindowsEnvVariable(regKey, key) + if (existed) { + delete process.env[key] + changed = true + } } - }) + } + if (changed) { + await broadcastWindowsEnvChange(exec) + } } catch (e) { - log.error('删除环境变量 HTTPS_PROXY、HTTP_PROXY 失败:', e) + log.error('恢复/删除环境变量 HTTPS_PROXY、HTTP_PROXY、REQUEST_CA_BUNDLE 失败:', e) } return true diff --git a/packages/core/src/utils/util.log-or-console.js b/packages/core/src/utils/util.log-or-console.js index f547f41991..3fb7afcc51 100644 --- a/packages/core/src/utils/util.log-or-console.js +++ b/packages/core/src/utils/util.log-or-console.js @@ -1,15 +1,18 @@ const dateUtil = require('./util.date') +// DEV_SIDECAR_LOG_DISABLED=true 时完全静默:不输出控制台,也不备份历史日志 +const disabled = process.env.DEV_SIDECAR_LOG_DISABLED === 'true' + // CLI 命令(如 status)会 require core 但不会启动,若默认输出到 console 会污染命令结果。 // 当 DEV_SIDECAR_LOG_TO_CONSOLE=false 时保持静默,但仍备份,待 setLogger 后回放进日志文件 -const silent = process.env.DEV_SIDECAR_LOG_TO_CONSOLE === 'false' +const silent = disabled || process.env.DEV_SIDECAR_LOG_TO_CONSOLE === 'false' let log = silent ? { debug () {}, info () {}, warn () {}, error () {} } : console // 将console中的日志缓存起来,当setLogger时,将控制台的日志写入日志文件 -let backupLogs = [] +let backupLogs = disabled ? null : [] function backup (fun, args) { if (backupLogs === null) { @@ -61,6 +64,10 @@ function _doLog (fun, args) { module.exports = { setLogger (logger) { + if (disabled) { + return + } + if (logger == null) { log.error('logger 不能为空') return diff --git a/packages/core/src/utils/util.logger.js b/packages/core/src/utils/util.logger.js index 6175f18676..9f33fa3526 100644 --- a/packages/core/src/utils/util.logger.js +++ b/packages/core/src/utils/util.logger.js @@ -7,8 +7,18 @@ const configFromFiles = defaultConfig.configFromFiles // 日志级别 const level = process.env.NODE_ENV === 'development' ? 'debug' : 'info' +// 是否完全禁用日志:不输出 stdout,也不写日志文件 +const logDisabled = process.env.DEV_SIDECAR_LOG_DISABLED === 'true' + // 是否同时输出到 stdout。默认开启(GUI/开发调试需要),CLI 守护进程通过环境变量关闭 -const logToConsole = process.env.DEV_SIDECAR_LOG_TO_CONSOLE !== 'false' +const logToConsole = !logDisabled && process.env.DEV_SIDECAR_LOG_TO_CONSOLE !== 'false' + +function createNoopLogger () { + const noop = () => {} + return { debug: noop, info: noop, warn: noop, error: noop, level: 'off', category: 'noop' } +} + +const noopLogger = logDisabled ? createNoopLogger() : null function getDefaultConfigBasePath () { if (configFromFiles.app.logFileSavePath) { @@ -40,6 +50,10 @@ let log = null // 设置一组日志配置 function log4jsConfigure (categories) { + if (logDisabled) { + return + } + if (log != null) { log.error('当前进程已经设置过日志配置,无法再设置更多日志配置:', categories) return @@ -69,6 +83,10 @@ function log4jsConfigure (categories) { module.exports = { getLogger (category) { + if (logDisabled) { + return noopLogger + } + if (!category) { if (log) { log.error('未指定日志类型,无法配置并获取日志对象!!!') diff --git a/packages/core/test/instanceTest.js b/packages/core/test/instanceTest.js index 978e1bd0f0..cfd82d9544 100644 --- a/packages/core/test/instanceTest.js +++ b/packages/core/test/instanceTest.js @@ -6,24 +6,39 @@ const os = require('node:os') const instance = require('../src/modules/instance') const event = require('../src/event') -const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) -describe('instance', function () { +describe('instance', () => { let tmpDir + let oldHome + let oldUserProfile - beforeEach(function () { + beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-core-instance-')) + // getUserBasePath 优先读 USERPROFILE(Windows),其次 HOME(Linux/macOS) + oldHome = process.env.HOME + oldUserProfile = process.env.USERPROFILE process.env.HOME = tmpDir + process.env.USERPROFILE = tmpDir fs.mkdirSync(path.join(tmpDir, '.dev-sidecar'), { recursive: true }) }) - afterEach(function () { - delete process.env.HOME + afterEach(() => { + if (oldHome === undefined) { + delete process.env.HOME + } else { + process.env.HOME = oldHome + } + if (oldUserProfile === undefined) { + delete process.env.USERPROFILE + } else { + process.env.USERPROFILE = oldUserProfile + } fs.rmSync(tmpDir, { recursive: true, force: true }) }) - describe('lock', function () { - it('should acquire and release the lock', async function () { + describe('lock', () => { + it('should acquire and release the lock', async () => { assert.isFalse(await instance.isLocked()) const release = await instance.acquireLock() assert.isTrue(await instance.isLocked()) @@ -31,7 +46,7 @@ describe('instance', function () { assert.isFalse(await instance.isLocked()) }) - it('should reject second acquire while lock is held', async function () { + it('should reject second acquire while lock is held', async () => { const release = await instance.acquireLock() try { await instance.acquireLock() @@ -42,7 +57,7 @@ describe('instance', function () { await release() }) - it('should take over a stale lock left by a crashed process', async function () { + it('should take over a stale lock left by a crashed process', async () => { const lockPath = instance.getLockPath() fs.mkdirSync(lockPath, { recursive: true }) const past = new Date(Date.now() - 30000) @@ -53,7 +68,7 @@ describe('instance', function () { await release() }) - it('should not take over a fresh lock', async function () { + it('should not take over a fresh lock', async () => { const lockPath = instance.getLockPath() fs.mkdirSync(lockPath, { recursive: true }) assert.isTrue(await instance.isLocked()) @@ -66,18 +81,18 @@ describe('instance', function () { }) }) - describe('instance info', function () { - it('should write and read instance info', function () { + describe('instance info', () => { + it('should write and read instance info', () => { const payload = { type: 'cli', pid: 123, command: 'node --daemon', startTime: '2026-01-01T00:00:00.000Z' } instance.writeInstance(payload) assert.deepEqual(instance.readInstance(), payload) }) - it('should return null when running.json is missing', function () { + it('should return null when running.json is missing', () => { assert.isNull(instance.readInstance()) }) - it('should preserve app.instance when updateStatus writes', async function () { + it('should preserve app.instance when updateStatus writes', async () => { instance.writeInstance({ type: 'cli', pid: 123 }) instance.updateStatus('server.enabled', true) await sleep(400) @@ -87,8 +102,8 @@ describe('instance', function () { }) }) - describe('updateStatus', function () { - it('should debounce multiple updates into one write', async function () { + describe('updateStatus', () => { + it('should debounce multiple updates into one write', async () => { instance.updateStatus('server.enabled', true) instance.updateStatus('proxy.enabled', true) instance.updateStatus('plugin.git.enabled', true) @@ -101,14 +116,14 @@ describe('instance', function () { assert.isTrue(data.app.status.plugin.git.enabled) }) - it('should set nested keys via dot path', async function () { + it('should set nested keys via dot path', async () => { instance.updateStatus('plugin.node.enabled', true) await sleep(400) const data = JSON.parse(fs.readFileSync(instance.getRunningJsonPath(), 'utf-8')) assert.isTrue(data.app.status.plugin.node.enabled) }) - it('should ignore invalid keys', async function () { + it('should ignore invalid keys', async () => { instance.updateStatus('', true) instance.updateStatus(null, true) await sleep(400) @@ -120,8 +135,8 @@ describe('instance', function () { }) }) - describe('watchStatusEvents', function () { - it('should sync *.enabled events to running.json', async function () { + describe('watchStatusEvents', () => { + it('should sync *.enabled events to running.json', async () => { const release = await instance.acquireLock() try { event.fire('status', { key: 'server.enabled', value: true }) @@ -135,7 +150,7 @@ describe('instance', function () { } }) - it('should filter non-enabled events (e.g. free_eye.result)', async function () { + it('should filter non-enabled events (e.g. free_eye.result)', async () => { const release = await instance.acquireLock() try { event.fire('status', { key: 'server.enabled', value: true }) diff --git a/packages/gui/electron-builder.config.cjs b/packages/gui/electron-builder.config.cjs index e45f4af3dd..f68200d58d 100644 --- a/packages/gui/electron-builder.config.cjs +++ b/packages/gui/electron-builder.config.cjs @@ -63,8 +63,10 @@ module.exports = { allowToChangeInstallationDirectory: true, }, win: { - icon: 'build/icons/', - signAndEditExecutable: isCI, // 本地开发跳过签名 + icon: 'build/icons/icon.ico', + // 必须为 true 才会用 rcedit 写入 exe 图标和版本信息; + // 未配置证书时 electron-builder 会自动跳过签名,不会失败。 + signAndEditExecutable: true, target: isCI ? [ { target: 'nsis', arch: ['x64'] }, @@ -84,12 +86,6 @@ module.exports = { { target: 'tar.gz', arch: ['x64', 'arm64', 'armv7l'] }, { target: 'rpm', arch: ['x64', 'arm64', 'armv7l'] }, { target: 'flatpak', arch: ['x64'] }, - // Arch Linux (fpm/pacman), 产物为 .pkg.tar.xz, 可直接 pacman -U 安装 - { - target: 'pacman', - arch: ['x64', 'arm64'], - artifactName: 'DevSidecar-${version}-${arch}.pkg.tar.xz', - }, ] : [ { target: 'deb', arch: [localArch] }, diff --git a/packages/gui/package.json b/packages/gui/package.json index 637846724f..e414060e7a 100644 --- a/packages/gui/package.json +++ b/packages/gui/package.json @@ -24,7 +24,9 @@ "@docmirror/dev-sidecar": "workspace:*", "@docmirror/mitmproxy": "workspace:*", "@starknt/shutdown-handler-napi": "^0.0.3", + "@starknt/shutdown-handler-napi-win32-x64-msvc": "^0.0.3", "@starknt/sysproxy": "^0.0.3", + "@starknt/sysproxy-win32-x64-msvc": "^0.0.3", "@vscode/sudo-prompt": "^9.3.2", "adm-zip": "^0.5.17", "ant-design-vue": "^4.2.6", diff --git a/packages/gui/src/background.js b/packages/gui/src/background.js index 0c5b3ede38..0f51e4c0b1 100644 --- a/packages/gui/src/background.js +++ b/packages/gui/src/background.js @@ -1,4 +1,5 @@ 'use strict' +import './utils/util.log-env.js' import path from 'node:path' import { fileURLToPath } from 'node:url' import DevSidecar from '@docmirror/dev-sidecar' @@ -15,6 +16,12 @@ const isWindows = process.platform === 'win32' const isLinux = process.platform === 'linux' const isMac = process.platform === 'darwin' +if (isWindows) { + // 让 Windows 托盘气泡/通知/任务栏显示正常软件名,而不是开发模式默认的 electron.app + app.setName('dev-sidecar') + app.setAppUserModelId('dev-sidecar') +} + // 禁用不需要的 Chromium 组件以减少内存和 CPU 占用 // 这些开关必须在 app.whenReady() 之前设置 @@ -240,18 +247,30 @@ function changeAppConfig (config) { } function loadAppIcon () { - // 优先:从 asar 内读取(fs.readFileSync 被 Electron 补丁支持 asar) - try { - const p = path.join(app.getAppPath(), 'dist', 'icon.png') - if (fs.existsSync(p)) return nativeImage.createFromBuffer(fs.readFileSync(p)) - } catch { /* ignore */ } + const candidates = [] + + // Windows 下优先使用 exe 同款 ico,任务栏图标更清晰 + if (isWindows) { + candidates.push(path.join(process.resourcesPath, 'extra', 'icons', 'icon.ico')) + } + // 优先:从 asar 内读取 + candidates.push(path.join(app.getAppPath(), 'dist', 'icon.png')) // 回退:asar.unpacked 真实路径 - try { - const p = path.join(app.getAppPath(), '..', 'app.asar.unpacked', 'dist', 'icon.png') - if (fs.existsSync(p)) return nativeImage.createFromPath(p) - } catch { /* ignore */ } + candidates.push(path.join(app.getAppPath(), '..', 'app.asar.unpacked', 'dist', 'icon.png')) + // 再回退:extra 资源目录 + candidates.push(path.join(process.resourcesPath, 'extra', 'icons', '512x512.png')) // 开发模式回退 - return nativeImage.createFromPath(path.resolve('public/icon.png')) + candidates.push(path.resolve('public/icon.png')) + + for (const p of candidates) { + try { + if (p && fs.existsSync(p)) { + return nativeImage.createFromPath(p) + } + } catch { /* ignore */ } + } + + return nativeImage.createEmpty() } function createWindow (startHideWindow, autoQuitIfError = true) { @@ -295,9 +314,7 @@ function createWindow (startHideWindow, autoQuitIfError = true) { if (process.env.WEBPACK_DEV_SERVER_URL) { // Load the url of the dev server if in development mode win.loadURL(process.env.WEBPACK_DEV_SERVER_URL) - if (!process.env.IS_TEST) { - setTimeout(openDevTools, 2000) - } + // 默认不自动打开 DevTools,需要调试时可手动按 F12 或通过托盘菜单打开 } else { // Load the index.html when not in development win.loadFile(path.join(app.getAppPath(), 'dist', 'index.html')) diff --git a/packages/gui/src/bridge/api/backend.js b/packages/gui/src/bridge/api/backend.js index f89be23a05..f41b2e6247 100644 --- a/packages/gui/src/bridge/api/backend.js +++ b/packages/gui/src/bridge/api/backend.js @@ -175,7 +175,7 @@ const localApi = { * @returns {Promise} */ startup () { - return DevSidecar.api.startup({ mitmproxyPath }) + return DevSidecar.api.startup({ mitmproxyPath, setting: localApi.setting.load() }) }, server: { /** @@ -183,14 +183,14 @@ const localApi = { * @returns {Promise<{port: *}>} */ start () { - return DevSidecar.api.server.start({ mitmproxyPath }) + return DevSidecar.api.server.start({ mitmproxyPath, setting: localApi.setting.load() }) }, /** * 重启代理服务 * @returns {Promise} */ restart () { - return DevSidecar.api.server.restart({ mitmproxyPath }) + return DevSidecar.api.server.restart({ mitmproxyPath, setting: localApi.setting.load() }) }, }, shell: { @@ -281,6 +281,11 @@ export default { win.webContents.send('speed', event) } }) + DevSidecar.api.event.register('traffic', (event) => { + if (win) { + win.webContents.send('traffic', event) + } + }) // 合并用户配置 DevSidecar.api.config.reload() diff --git a/packages/gui/src/bridge/update/backend.js b/packages/gui/src/bridge/update/backend.js index 0d5a3ec27a..1e27be6350 100644 --- a/packages/gui/src/bridge/update/backend.js +++ b/packages/gui/src/bridge/update/backend.js @@ -1,5 +1,6 @@ import fs from 'node:fs' import path from 'node:path' +import tls from 'node:tls' import { fileURLToPath } from 'node:url' import DevSidecar from '@docmirror/dev-sidecar' import AdmZip from 'adm-zip' @@ -22,6 +23,68 @@ const isLinux = process.platform === 'linux' const curVersion = pkg.version const isCurrentPreRelease = curVersion.includes('-') +let cachedRootCaCert = null +/** + * 更新检查/下载会走本地 MITM 代理,需要信任 dev-sidecar 自己的根证书; + * 同时也保留 Node 内置根证书,保证直连 GitHub 时也能正常校验。 + */ +function getTrustedCaList () { + const caList = [...tls.rootCertificates] + if (cachedRootCaCert != null) { + if (cachedRootCaCert !== false) { + caList.push(cachedRootCaCert) + } + return caList + } + + try { + const certPath = DevSidecar.api?.config?.get()?.server?.setting?.rootCaFile?.certPath + if (certPath && fs.existsSync(certPath)) { + cachedRootCaCert = fs.readFileSync(certPath, 'utf8') + caList.push(cachedRootCaCert) + } else { + cachedRootCaCert = false + } + } catch (e) { + log.warn('读取 dev-sidecar 根证书失败:', e) + cachedRootCaCert = false + } + return caList +} + +/** + * 获取当前平台/架构对应的资源命名。 + * - partArch: 增量包命名用(win-x64 / win-arm64 / win-ia32) + * - fullArch: 完整安装包命名用(windows-x86_64 / windows-arm64 / linux-armv7l 等) + */ +function getPlatformAssetInfo () { + const arch = process.arch + let partArch + let fullArch + if (arch === 'ia32') { + partArch = 'ia32' + fullArch = 'ia32' + } else if (arch === 'arm64') { + partArch = 'arm64' + fullArch = 'arm64' + } else if (arch === 'arm') { + // 无 arm 增量包:不要回退到 x64 增量包,否则可能把不兼容补丁装到 ARM 安装上 + partArch = null + fullArch = 'armv7l' + } else { + partArch = 'x64' + fullArch = 'x86_64' + } + + if (isMac) { + return { platform: 'macos', partPlatform: 'mac', partArch, fullArch } + } + if (isLinux) { + return { platform: 'linux', partPlatform: 'linux', partArch, fullArch } + } + return { platform: 'windows', partPlatform: 'win', partArch, fullArch } +} + function extractVersion (versionData) { const candidates = [versionData.tag_name, versionData.name] for (const candidate of candidates) { @@ -36,9 +99,28 @@ function extractVersion (versionData) { return null } +function getUpdateDownloadDir (app) { + // 安装目录(Program Files / .app 内)可能只读,下载到 userData/update + const userData = app.getPath('userData') + const fileDir = path.join(userData, 'update') + fs.mkdirSync(fileDir, { recursive: true }) + return fileDir +} + function downloadFile (uri, filePath, onProgress, onSuccess, onError) { log.info('download url', uri) - progress(request(uri), { + const writeStream = fs.createWriteStream(filePath) + let settled = false + const fail = (err) => { + if (settled) { + return + } + settled = true + writeStream.destroy() + log.error('下载升级包失败:', err) + onError(err) + } + progress(request(uri, { ca: getTrustedCaList() }), { // throttle: 2000, // Throttle the progress event to 2000ms, defaults to 1000ms // delay: 1000, // Only start to emit after 1000ms delay, defaults to 0ms // lengthHeader: 'x-transfer-length' // Length header to use, defaults to content-length @@ -47,16 +129,17 @@ function downloadFile (uri, filePath, onProgress, onSuccess, onError) { onProgress(state.percent * 100) log.log('progress', state.percent) }) - .on('error', (err) => { - // Do something with err - log.error('下载升级包失败:', err) - onError(err) - }) - .on('end', () => { - // Do something after request finishes - onSuccess() + .on('error', fail) + .pipe(writeStream) + .on('error', fail) + .on('finish', () => { + // 必须等写流 flush 完成,避免文件未落盘就通知安装 + if (settled) { + return + } + settled = true + onSuccess(filePath) }) - .pipe(fs.createWriteStream(filePath)) } /** @@ -98,7 +181,7 @@ function updateHandle (app, api, win, beforeQuit, quit, log) { // 检查更新 const releasesApiUrl = 'https://api.github.com/repos/docmirror/dev-sidecar/releases' async function checkForUpdatesFromGitHub () { - request(releasesApiUrl, { headers: { 'User-Agent': `DS/${curVersion}`, 'Server-Name': 'baidu.com' } }, (error, response, body) => { + request(releasesApiUrl, { headers: { 'User-Agent': `DS/${curVersion}`, 'Server-Name': 'baidu.com' }, ca: getTrustedCaList() }, (error, response, body) => { try { if (error) { log.error('检查更新失败:', error) @@ -162,10 +245,22 @@ function updateHandle (app, api, win, beforeQuit, quit, log) { log.info(`检查更新:发现新版本 '${onlineVersion}',当前版本号为 '${curVersion}'`) // 查找当前平台+架构对应的增量更新包 - const arch = process.arch === 'ia32' ? 'ia32' : process.arch === 'arm64' ? 'arm64' : 'x64' - const platformPrefix = isMac ? `update-mac-${arch}-` : isLinux ? `update-linux-${arch}-` : `update-win-${arch}-` - const partAsset = versionData.assets.find(a => a.name && a.name.startsWith(platformPrefix) && a.name.endsWith('.zip')) - const partPackage = partAsset ? partAsset.browser_download_url : null + const assetInfo = getPlatformAssetInfo() + let partPackage = null + if (assetInfo.partArch) { + const partPrefix = `update-${assetInfo.partPlatform}-${assetInfo.partArch}-` + const partAsset = versionData.assets.find(a => a.name && a.name.startsWith(partPrefix) && a.name.endsWith('.zip')) + partPackage = partAsset ? partAsset.browser_download_url : null + } + + // 查找当前平台+架构对应的完整安装包 + const fullPrefix = `DevSidecar-${onlineVersion}-${assetInfo.platform}-${assetInfo.fullArch}.` + const fullSuffix = assetInfo.platform === 'windows' ? '.exe' : assetInfo.platform === 'macos' ? '.dmg' : '.AppImage' + const fullAsset = versionData.assets.find(a => a.name && a.name.startsWith(fullPrefix) && a.name.endsWith(fullSuffix)) + const fullPackage = fullAsset ? fullAsset.browser_download_url : null + const fullPackageName = fullAsset ? fullAsset.name : null + + log.info(`平台资源匹配: ${JSON.stringify({ assetInfo, partPackage, fullPackageName })}`) win.webContents.send('update', { key: 'available', @@ -175,6 +270,8 @@ function updateHandle (app, api, win, beforeQuit, quit, log) { ? (versionData.body.replace(/\r\n/g, '\n').replace(/https:\/\/github.com\/docmirror\/dev-sidecar/g, '').replace(/(?<=(^|\n))[ \t]*(?:#[ #]*)?#\s*/g, '') || '无') : '无', partPackage, + fullPackage, + fullPackageName, }, }) } else { @@ -226,26 +323,55 @@ function updateHandle (app, api, win, beforeQuit, quit, log) { // 下载升级包 function downloadPart (app, value) { - const appPath = appPathUtil.getAppRootPath(app) - const fileDir = path.join(appPath, 'update') + const fileDir = getUpdateDownloadDir(app) log.info('download dir:', fileDir) - try { - fs.accessSync(fileDir, fs.constants.F_OK) - } catch { - fs.mkdirSync(fileDir) - } const filePath = path.join(fileDir, `${value.version}.zip`) downloadFile(value.partPackage, filePath, (data) => { win.webContents.send('update', { key: 'progress', value: Number.parseInt(data) }) - }, () => { + }, (downloadedPath) => { // 文件下载完成 win.webContents.send('update', { key: 'progress', value: 100 }) - log.info('升级包下载成功:', filePath) - partPackagePath = filePath + log.info('升级包下载成功:', downloadedPath) + partPackagePath = downloadedPath + win.webContents.send('update', { + key: 'downloaded', + value, + filePath: downloadedPath, + updateType: 'part', + }) + }, (error) => { + sendUpdateMessage({ key: 'error', value: error, error }) + }) + } + + // 下载完整安装包(按平台+架构自动匹配后的安装文件) + function downloadFull (app, value) { + if (!value.fullPackage) { + sendUpdateMessage({ key: 'error', value: new Error('未找到对应的完整安装包'), error: '未找到对应的完整安装包' }) + return + } + const fileDir = getUpdateDownloadDir(app) + log.info('download full dir:', fileDir) + // 文件名来自 renderer/更新元数据,必须限制在 update 目录内,防止 path traversal + const rawName = value.fullPackageName || `${value.version}-${getPlatformAssetInfo().platform}-${getPlatformAssetInfo().fullArch}` + const fileName = path.basename(String(rawName)) + const filePath = path.join(fileDir, fileName) + if (!filePath.startsWith(fileDir + path.sep)) { + sendUpdateMessage({ key: 'error', value: new Error('非法的安装包文件名'), error: '非法的安装包文件名' }) + return + } + + downloadFile(value.fullPackage, filePath, (data) => { + win.webContents.send('update', { key: 'progress', value: Number.parseInt(data) }) + }, (downloadedPath) => { + win.webContents.send('update', { key: 'progress', value: 100 }) + log.info('完整安装包下载成功:', downloadedPath) win.webContents.send('update', { key: 'downloaded', value, + filePath: downloadedPath, + updateType: 'full', }) }, (error) => { sendUpdateMessage({ key: 'error', value: error, error }) @@ -337,6 +463,10 @@ function updateHandle (app, api, win, beforeQuit, quit, log) { // 下载增量更新版本 log.info('autoUpdater downloadPart') downloadPart(app, arg.value) + } else if (arg.key === 'downloadFull') { + // 下载完整安装包 + log.info('autoUpdater downloadFull') + downloadFull(app, arg.value) } }) // 通过main进程发送事件给renderer进程,提示更新信息 diff --git a/packages/gui/src/bridge/update/front.js b/packages/gui/src/bridge/update/front.js index 05e6ac28a2..42af184e74 100644 --- a/packages/gui/src/bridge/update/front.js +++ b/packages/gui/src/bridge/update/front.js @@ -20,6 +20,10 @@ function install (app, api) { // 增量更新 api.ipc.send('update', { key: 'downloadPart', value }) }, + downloadFull (value) { + // 完整安装包更新 + api.ipc.send('update', { key: 'downloadFull', value }) + }, doUpdateNow () { api.ipc.send('update', { key: 'doUpdateNow' }) }, @@ -35,8 +39,16 @@ function install (app, api) { updateParams.checking = false noNewVersion() } else if (type === 'downloaded') { - // 更新包已下载完成,让用户确认是否更新 + // 更新包已下载完成 updateParams.downloading = false + if (message.updateType === 'full') { + // 完整安装包:自动打开,由用户完成安装 + if (message.filePath) { + api.ipc.openPath(message.filePath) + } + app.config.globalProperties.$message.success(message.filePath ? '完整安装包已下载并自动打开,请按提示完成安装' : '完整安装包已下载完成') + return + } console.log('updateParams', updateParams) newUpdateIsReady(message.value) } else if (type === 'progress') { @@ -95,7 +107,7 @@ function install (app, api) { async function downloadNewVersion (value) { const platform = await api.info.getSystemPlatform() console.log(`download new version: ${JSON.stringify(value)}, platform: ${platform}`) - if (platform === 'linux') { + if (platform === 'linux' && !value.fullPackage) { goManualUpdate(value) return } @@ -103,6 +115,10 @@ function install (app, api) { // 有增量更新 ZIP,走增量更新流程 updateParams.downloading = true api.update.downloadPart(value) + } else if (value.fullPackage) { + // 没有增量包时,自动下载当前平台/架构对应的完整安装包 + updateParams.downloading = true + api.update.downloadFull(value) } else { goManualUpdate(value) } @@ -117,13 +133,15 @@ function install (app, api) { } const hasPartPackage = !!value.partPackage - const platform = api.info ? null : null // placeholder, will resolve below + const hasFullPackage = !!value.fullPackage + const okText = hasPartPackage ? '自动更新(实验性)' : hasFullPackage ? '自动下载安装包' : '手动下载' + const okType = hasPartPackage ? 'danger' : hasFullPackage ? 'primary' : 'default' app.config.globalProperties.$confirm({ title: `发现新版本:v${value.version}`, cancelText: '暂不升级', - okText: hasPartPackage ? '自动更新(实验性)' : '手动下载', - okType: hasPartPackage ? 'danger' : 'default', + okText, + okType, width: 700, content: (h) => { const children = [] @@ -146,20 +164,25 @@ function install (app, api) { h('span', { style: { fontWeight: 'bold' } }, '更新方式说明:'), h('ul', { style: { marginTop: '6px', paddingLeft: '20px' } }, [ h('li', {}, [h('b', {}, '自动更新(实验性)'), ' — 下载增量包自动覆盖,重启即完成。仅 Windows/macOS 支持']), - h('li', {}, [h('b', {}, '手动下载'), ' — 跳转 GitHub Releases 下载完整安装包,覆盖安装即可']), + h('li', {}, [h('b', {}, '自动下载安装包'), ' — 下载当前平台/架构对应的完整安装包']), ]), ]), ) } - if (!hasPartPackage) { + if (!hasPartPackage && hasFullPackage) { children.push( - h('div', { style: { marginTop: '12px' } }, '当前平台不支持增量更新,点击"手动下载"前往 GitHub Releases。'), + h('div', { style: { marginTop: '12px', color: '#888' } }, `已自动识别当前平台/架构,将下载安装包:${value.fullPackageName || '对应完整安装包'},下载完成后自动打开供你安装。`), + ) + } + if (!hasPartPackage && !hasFullPackage) { + children.push( + h('div', { style: { marginTop: '12px' } }, '当前平台/架构暂未匹配到安装包,点击"手动下载"前往 GitHub Releases。'), ) } return h('div', {}, children) }, onOk () { - if (hasPartPackage) { + if (hasPartPackage || hasFullPackage) { downloadNewVersion(value) } else { openGithubUrl() @@ -177,7 +200,7 @@ function install (app, api) { cancelText: '暂不升级', okText: '立即升级', width: 700, - content: (h) => { + content: (_h) => { if (value.releaseNotes) { const notes = [] if (typeof value.releaseNotes === 'string') { diff --git a/packages/gui/src/utils/util.log-env.js b/packages/gui/src/utils/util.log-env.js new file mode 100644 index 0000000000..b40f3ba93d --- /dev/null +++ b/packages/gui/src/utils/util.log-env.js @@ -0,0 +1,41 @@ +// 必须在加载 core/mitmproxy 日志模块之前 import,确保 DEV_SIDECAR_LOG_DISABLED 尽早生效 +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { createRequire } from 'node:module' + +const require = createRequire(import.meta.url) + +function readUserConfig () { + const userBase = path.join(process.env.USERPROFILE || process.env.HOME || '/', '.dev-sidecar') + const newFilePath = path.join(userBase, 'config.json') + const oldFilePath = path.join(userBase, 'config.json5') + // 与 core 的 local-config-loader 一致:优先新路径,不存在时回退 json5 + const configPath = fs.existsSync(newFilePath) ? newFilePath : oldFilePath + if (!fs.existsSync(configPath)) { + return null + } + const raw = fs.readFileSync(configPath, 'utf-8') + try { + return JSON.parse(raw) + } catch { + // json5 配置:使用 mitmproxy 同款解析器 + try { + const jsonApi = require('@docmirror/mitmproxy/src/json') + return jsonApi.parse(raw) + } catch { + return null + } + } +} + +if (process.env.DEV_SIDECAR_LOG_DISABLED !== 'true') { + try { + const userConfig = readUserConfig() + if (userConfig && userConfig.app && userConfig.app.logDisabled === true) { + process.env.DEV_SIDECAR_LOG_DISABLED = 'true' + } + } catch { + // 配置文件不存在或格式异常时,按默认(允许日志)处理 + } +} diff --git a/packages/gui/src/view/components/container.vue b/packages/gui/src/view/components/container.vue index 9964a456b6..3700109795 100644 --- a/packages/gui/src/view/components/container.vue +++ b/packages/gui/src/view/components/container.vue @@ -1,9 +1,9 @@