diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..2c3bc66 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,42 @@ +name: Build and Push Electros docker image + +on: + push: + branches: [ develop ] + workflow_dispatch: + +permissions: + contents: write + packages: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + + # build and push the docker image to ghcr.io + + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.CI_TOKEN }} + submodules: recursive + ref: ${{ github.ref }} + + - name: Populate daemons folder + run: | + export CI_TOKEN=${{ secrets.CI_TOKEN }} + ./populate_daemons.sh --platform linux --arch x64 --develop + + - name: Login to GHCR + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.REPO_KEY }} + + - name: Build and push Docker image + run: | + OWNER_NAME=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + docker build -f docker/Dockerfile -t ghcr.io/${OWNER_NAME}/elemento-electros-instance:latest . + docker push ghcr.io/${OWNER_NAME}/elemento-electros-instance:latest diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000..c2b392e --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,408 @@ +name: nightly.yml + +on: + schedule: + - cron: "0 17 * * *" + workflow_dispatch: + +jobs: + remove-old-releases: + name: Remove old releases + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + steps: + - name: Delete old releases + uses: s00d/delete-older-releases@0.2.1 + with: + repo: Elemento-Modular-Cloud/Electros + keep_latest: 3 + delete_type: 'release' + delete_branch: ${{ github.ref }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Delete old prereleases + uses: s00d/delete-older-releases@0.2.1 + with: + repo: Elemento-Modular-Cloud/Electros + keep_latest: 1 + delete_type: 'prerelease' + delete_branch: ${{ github.ref }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + sync-submodule: + name: Sync GUI Submoudle + runs-on: ubuntu-latest + + create-release: + name: Create Release + needs: remove-old-releases + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + outputs: + tag_name: ${{ steps.get_tag_name.outputs.tag_name }} + tag_name_nodate: ${{ steps.get_tag_name.outputs.tag_name_nodate }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.CI_TOKEN }} + submodules: recursive + ref: ${{ github.ref }} + + - name: Get current date + id: date + run: | + echo "date=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT + echo "time=$(date +'%H%M%S')" >> $GITHUB_OUTPUT + + - name: Get tag name + id: get_tag_name + run: | + VERSION=$(node -p "require('./electros-electron/package.json').version") + echo "tag_name=v${VERSION}-${{ steps.date.outputs.date }}-${{ steps.date.outputs.time }}-nightly-${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + echo "tag_name_nodate=${VERSION}-nightly" >> $GITHUB_OUTPUT + + - name: Create release + id: create_release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create ${{ steps.get_tag_name.outputs.tag_name }} \ + --prerelease \ + --title "ElectrosRelease ${{ steps.get_tag_name.outputs.tag_name }}" \ + --generate-notes + + build: + name: Build (${{ matrix.os }}-${{ matrix.arch }}) + needs: create-release + runs-on: ${{ matrix.platform }} + strategy: + fail-fast: false + matrix: + include: + - platform: macos-latest + os: mac + arch: x64 + - platform: macos-latest + os: mac + arch: arm64 + - platform: macos-latest + os: win + arch: x64 + - platform: ubuntu-latest + os: linux + arch: x64 + - platform: ubuntu-latest + os: linux + arch: arm64 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.CI_TOKEN }} + submodules: recursive + ref: ${{ github.ref }} + + - name: Get tag name + id: get_tag_name + run: | + VERSION=$(node -p "require('./electros-electron/package.json').version") + echo "tag_name=v${VERSION}-${{ steps.date.outputs.date }}-${{ steps.date.outputs.time }}-${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + echo "tag_name_nodate=${VERSION}" >> $GITHUB_OUTPUT + + + - name: Setup step + if: runner.os != 'Windows' + run: | + export CI_TOKEN=${{ secrets.CI_TOKEN }} + cd ./electros-electron + chmod +x ./setup.sh + ./setup.sh + shell: bash + + - name: Populate Daemons step + if: runner.os != 'Windows' + run: | + export CI_TOKEN=${{ secrets.CI_TOKEN }} + chmod +x ./populate_daemons.sh + ./populate_daemons.sh --platform ${{ matrix.os }} --arch ${{ matrix.arch }} --develop + shell: bash + + - name: Build step (${{ matrix.os }}-${{ matrix.arch }}) + if: runner.os != 'Windows' + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + if [ ${{ matrix.os }} == "mac" ]; then + # Create keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security set-keychain-settings -t 3600 -u build.keychain + + # Import certificate + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output certificate.p12 + security import certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain + rm -fr *.p12 + fi + + # Clean up + cd ./electros-electron + chmod +x ./autobuild.sh + ./autobuild.sh --platform ${{ matrix.os }} --arch ${{ matrix.arch }} --version ${{ steps.get_tag_name.outputs.tag_name_nodate }} + shell: bash + + - name: Upload un-notarized & un-stapled DMG artifact + uses: actions/upload-artifact@v4 + if: matrix.os == 'mac' + with: + name: Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}-unsigned.dmg + path: electros-electron/build/${{ matrix.os }}/${{ matrix.arch }}/Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.dmg + + - name: Apple notarize step + if: runner.os != 'Windows' && matrix.os == 'mac' + env: + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APP_ID: app.elemento.cloud + run: | + # Load keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + + cd "electros-electron/build/${{ matrix.os }}/${{ matrix.arch }}" + + # First, extract the .app from the unsigned DMG + echo "Extracting .app bundle from DMG..." + VOLUME_NAME=$(hdiutil attach "./Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.dmg" | tail -1 | awk '{$1=$2=""; print substr($0,3)}') + cp -R "$VOLUME_NAME/Electros.app" ./ + hdiutil detach "$VOLUME_NAME" + + # First get the signing identity from the keychain + SIGNING_IDENTITY=$(security find-identity -v -p codesigning build.keychain | grep -o '".*"' | head -1 | tr -d '"') + codesign -s "$SIGNING_IDENTITY" -vvv --deep --timestamp --options=runtime --entitlements ../../../entitlements.plist Electros.app/Contents/Resources/app.asar.unpacked/node_modules/ssh2/lib/protocol/crypto/build/node_gyp_bins/python3 --force + codesign -s "$SIGNING_IDENTITY" -vvv --deep --timestamp --options=runtime --entitlements ../../../entitlements.plist ./Electros.app --force + + # Now notarize the extracted .app bundle + echo "Notarizing .app bundle..." + ditto -c -k --keepParent "Electros.app" "Electros.zip" + xcrun notarytool submit --apple-id $APPLE_ID --password $APPLE_APP_SPECIFIC_PASSWORD --team-id $APPLE_TEAM_ID ./Electros.zip | tee app-${{ matrix.os }}-${{ matrix.arch }}-notarize.log + + # Wait for app notarization to complete and staple the app + while read -r line; do + if [[ $line =~ "id: "([^\ ]+) ]]; then + submission_id="${BASH_REMATCH[1]}" + echo "Waiting for app notarization to complete..." + xcrun notarytool wait "$submission_id" --apple-id $APPLE_ID --password $APPLE_APP_SPECIFIC_PASSWORD --team-id $APPLE_TEAM_ID + xcrun stapler staple "Electros.app" + break + fi + done < app-${{ matrix.os }}-${{ matrix.arch }}-notarize.log + + rm Electros.zip + + # Create new DMG with the notarized app while preserving electron-builder layout + echo "Creating new DMG with notarized app..." + mv "./Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.dmg" "./original.dmg" + + # Mount the original DMG + VOLUME_NAME=$(hdiutil attach -nobrowse "./original.dmg" | tail -1 | awk '{$1=$2=""; print substr($0,3)}') + + # Create a temporary read-write DMG + hdiutil convert "./original.dmg" -format UDRW -o "./temp.dmg" + hdiutil detach "$VOLUME_NAME" + + # Mount the temporary DMG + VOLUME_NAME=$(hdiutil attach -nobrowse "./temp.dmg" | tail -1 | awk '{$1=$2=""; print substr($0,3)}') + + # Replace the .app with the notarized version + rm -rf "$VOLUME_NAME/Electros.app" + cp -R "./Electros.app" "$VOLUME_NAME/" + + # Unmount the temporary DMG + hdiutil detach "$VOLUME_NAME" + + # Convert back to compressed read-only DMG + hdiutil convert "./temp.dmg" -format UDZO -o "./Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.dmg" + + # Clean up temporary files + rm "./temp.dmg" "./original.dmg" + + # Sign the DMG with the same identity used for the app + echo "Signing DMG..." + codesign -s "$SIGNING_IDENTITY" -vvv --deep --timestamp --options=runtime \ + "./Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.dmg" + + # Now notarize the new DMG + echo "Notarizing DMG..." + xcrun notarytool submit --apple-id $APPLE_ID --password $APPLE_APP_SPECIFIC_PASSWORD --team-id $APPLE_TEAM_ID ./Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.dmg | tee ${{ matrix.os }}-${{ matrix.arch }}-notarize.log + + # Wait for DMG notarization to complete and staple + while read -r line; do + if [[ $line =~ "id: "([^\ ]+) ]]; then + submission_id="${BASH_REMATCH[1]}" + echo "Waiting for DMG notarization to complete..." + xcrun notarytool wait "$submission_id" --apple-id $APPLE_ID --password $APPLE_APP_SPECIFIC_PASSWORD --team-id $APPLE_TEAM_ID + xcrun stapler staple "./Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.dmg" + break + fi + done < ${{ matrix.os }}-${{ matrix.arch }}-notarize.log + + - name: Upload Apple notarize logs artifact + uses: actions/upload-artifact@v4 + if: matrix.os == 'mac' + with: + name: ${{ matrix.os }}-${{ matrix.arch }}-notarize.log + path: electros-electron/build/${{ matrix.os }}/${{ matrix.arch }}/${{ matrix.os }}-${{ matrix.arch }}-notarize.log electros-electron/build/${{ matrix.os }}/${{ matrix.arch }}/app-${{ matrix.os }}-${{ matrix.arch }}-notarize.log + + - name: Upload Apple notarize + uses: actions/upload-artifact@v4 + if: matrix.os == 'mac' + with: + name: Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.dmg + path: electros-electron/build/${{ matrix.os }}/${{ matrix.arch }}/Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.dmg + + - name: Upload win exe artifact + uses: actions/upload-artifact@v4 + if: matrix.os == 'win' + with: + name: Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.exe + path: electros-electron/build/${{ matrix.os }}/${{ matrix.arch }}/Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.exe + + - name: Upload linux artifacts + uses: actions/upload-artifact@v4 + if: matrix.os == 'linux' + with: + name: Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}-artifacts + path: | + electros-electron/build/${{ matrix.os }}/${{ matrix.arch }}/Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.AppImage + electros-electron/build/${{ matrix.os }}/${{ matrix.arch }}/Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.deb + electros-electron/build/${{ matrix.os }}/${{ matrix.arch }}/Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.rpm + + + - name: Generate checksums + run: | + cd electros-electron/build + + # Create the checksum file with absolute path + checksum_file="$(pwd)/checksums-${{ matrix.os }}-${{ matrix.arch }}.txt" + + # Create a single checksums file + echo "# Electros ${{ needs.create-release.outputs.tag_name }} Checksums" > "$checksum_file" + echo "# Generated on $(date)" >> "$checksum_file" + echo "" >> "$checksum_file" + + # Function to generate checksums for a file + generate_checksums() { + local file=$1 + if [ -f "$file" ]; then + echo "Generating checksums for $file" + echo "## $file" >> "$checksum_file" + if [[ "$RUNNER_OS" == "macOS" ]]; then + echo "MD5: $(md5 -r "$file" | cut -d' ' -f1)" >> "$checksum_file" + echo "SHA256: $(shasum -a 256 "$file" | cut -d' ' -f1)" >> "$checksum_file" + else + echo "MD5: $(md5sum "$file" | cut -d' ' -f1)" >> "$checksum_file" + echo "SHA256: $(sha256sum "$file" | cut -d' ' -f1)" >> "$checksum_file" + fi + echo "" >> "$checksum_file" + fi + } + + cd "${{ matrix.os }}/${{ matrix.arch }}" + + if [ ${{ matrix.os }} == "mac" ]; then + generate_checksums "Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.dmg" + + gh release upload ${{ needs.create-release.outputs.tag_name }} \ + "./Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.dmg" + elif [ ${{ matrix.os }} == "win" ]; then + generate_checksums "Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.exe" + + gh release upload ${{ needs.create-release.outputs.tag_name }} \ + "./Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.exe" + elif [ ${{ matrix.os }} == "linux" ]; then + generate_checksums "Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.AppImage" + generate_checksums "Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.deb" + + gh release upload ${{ needs.create-release.outputs.tag_name }} \ + "./Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.AppImage" \ + "./Electros-${{ steps.get_tag_name.outputs.tag_name_nodate }}-${{ matrix.os }}-${{ matrix.arch }}.deb" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload checksums artifact + uses: actions/upload-artifact@v4 + with: + name: checksums-${{ matrix.os }}-${{ matrix.arch }} + path: electros-electron/build/checksums-${{ matrix.os }}-${{ matrix.arch }}.txt + + combine-checksums: + name: Combine Checksums + needs: [create-release, build] + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.CI_TOKEN }} + ref: ${{ github.ref }} + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Combine checksums and update release notes + run: | + # Create header for combined checksums + { + echo "# Electros ${{ needs.create-release.outputs.tag_name }} Checksums" + echo "# Generated on $(date)" + echo "# This file contains checksums for all platform builds" + echo "" + } > combined_checksums.txt + + # Find and combine all checksum files + find artifacts -name "checksums-*.txt" -type f | while read -r file; do + echo "Processing $file" + platform=$(basename "$file" | sed 's/checksums-\(.*\)\.txt/\1/') + echo "## Platform: $platform" >> combined_checksums.txt + # Skip the header lines and add the content + tail -n +4 "$file" >> combined_checksums.txt + echo "" >> combined_checksums.txt + done + + # Get existing release notes + gh release view ${{ needs.create-release.outputs.tag_name }} --json body -q .body > existing_notes.txt + + # Combine existing notes with checksums + { + cat existing_notes.txt + echo "" + echo "
" + echo "Build Checksums" + echo "" + cat combined_checksums.txt + echo "
" + } > updated_notes.txt + + # Update release notes + gh release edit ${{ needs.create-release.outputs.tag_name }} --notes-file updated_notes.txt + + # Still upload the checksums file separately for direct access + gh release upload ${{ needs.create-release.outputs.tag_name }} combined_checksums.txt --clobber + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 208bf4d..9b3d441 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ electros-daemons electros-nwjs/nwjs-sdk-* .DS_Store electros-electron/venv +.env +logs/ +env/ diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..9d8dd4f --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,57 @@ +# #******************************************************************************# +# # Copyright(c) 2019-2023, Elemento srl, All rights reserved # +# # Author: Elemento srl # +# # Contributors are mentioned in the code where appropriate. # +# # Permission to use and modify this software and its documentation strictly # +# # for personal purposes is hereby granted without fee, # +# # provided that the above copyright notice appears in all copies # +# # and that both the copyright notice and this permission notice appear in the # +# # supporting documentation. # +# # Modifications to this work are allowed for personal use. # +# # Such modifications have to be licensed under a # +# # Creative Commons BY-NC-ND 4.0 International License available at # +# # http://creativecommons.org/licenses/by-nc-nd/4.0/ and have to be made # +# # available to the Elemento user community # +# # through the original distribution channels. # +# # The authors make no claims about the suitability # +# # of this software for any purpose. # +# # It is provided "as is" without express or implied warranty. # +# #******************************************************************************# +# +# #------------------------------------------------------------------------------# +# #Electros # +# #Authors: # +# #- Filippo Ferrando Damillano (fferrando at elemento.cloud) # +# #------------------------------------------------------------------------------# +# + + +FROM nginx:trixie + +# gather daemons +# copy app code +# daemons must run in background - ./daemons +# entrypoint must run the electros app on port 80 +COPY docker/nginx.conf /etc/nginx/nginx.conf +COPY electros-daemons/linux/x64/* /opt/daemons/ +COPY elemento-gui-new/ /usr/share/nginx/html/ +COPY elemento-gui-new/electros/configs/atomosFlags.json /usr/share/nginx/html/electros/configs/flags.json +COPY elemento-gui-new/electros/electrosOnAtomos.html /usr/share/nginx/html/electros/electros.html +COPY docker/startup.sh /opt/app/startup.sh + +RUN chmod +x /opt/daemons/* +RUN chmod +x /opt/app/startup.sh + +RUN mkdir -p /var/log/elemento + +RUN ls -lah /usr/share/nginx/html/ + +EXPOSE 80 + +ENTRYPOINT [ "/opt/app/startup.sh" ] + + + + + + diff --git a/docker/arm_Dockerfile b/docker/arm_Dockerfile new file mode 100644 index 0000000..b58cb98 --- /dev/null +++ b/docker/arm_Dockerfile @@ -0,0 +1,57 @@ +# #******************************************************************************# +# # Copyright(c) 2019-2023, Elemento srl, All rights reserved # +# # Author: Elemento srl # +# # Contributors are mentioned in the code where appropriate. # +# # Permission to use and modify this software and its documentation strictly # +# # for personal purposes is hereby granted without fee, # +# # provided that the above copyright notice appears in all copies # +# # and that both the copyright notice and this permission notice appear in the # +# # supporting documentation. # +# # Modifications to this work are allowed for personal use. # +# # Such modifications have to be licensed under a # +# # Creative Commons BY-NC-ND 4.0 International License available at # +# # http://creativecommons.org/licenses/by-nc-nd/4.0/ and have to be made # +# # available to the Elemento user community # +# # through the original distribution channels. # +# # The authors make no claims about the suitability # +# # of this software for any purpose. # +# # It is provided "as is" without express or implied warranty. # +# #******************************************************************************# +# +# #------------------------------------------------------------------------------# +# #Electros # +# #Authors: # +# #- Filippo Ferrando Damillano (fferrando at elemento.cloud) # +# #- Simone Robaldo (srobaldo at elemento.cloud) # +# #------------------------------------------------------------------------------# +# + + +FROM nginx:trixie + +# gather daemons +# copy app code +# daemons must run in background - ./daemons +# entrypoint must run the electros app on port 80 +COPY ./docker/nginx.conf /etc/nginx/nginx.conf +COPY electros-daemons/linux/arm64/* /opt/daemons/ +COPY elemento-gui-new/ /usr/share/nginx/html/ +COPY elemento-gui-new/electros/configs/atomosFlags.json /usr/share/nginx/html/electros/configs/flags.json +COPY ./docker/arm_startup.sh /opt/app/startup.sh + +RUN chmod +x /opt/daemons/* +RUN chmod +x /opt/app/startup.sh + +RUN mkdir -p /var/log/elemento + +RUN ls -lah /usr/share/nginx/html/ + +EXPOSE 80 + +ENTRYPOINT [ "/opt/app/startup.sh" ] + + + + + + diff --git a/docker/arm_startup.sh b/docker/arm_startup.sh new file mode 100644 index 0000000..03570a2 --- /dev/null +++ b/docker/arm_startup.sh @@ -0,0 +1,37 @@ +#! /bin/bash +# #******************************************************************************# +# # Copyright(c) 2019-2023, Elemento srl, All rights reserved # +# # Author: Elemento srl # +# # Contributors are mentioned in the code where appropriate. # +# # Permission to use and modify this software and its documentation strictly # +# # for personal purposes is hereby granted without fee, # +# # provided that the above copyright notice appears in all copies # +# # and that both the copyright notice and this permission notice appear in the # +# # supporting documentation. # +# # Modifications to this work are allowed for personal use. # +# # Such modifications have to be licensed under a # +# # Creative Commons BY-NC-ND 4.0 International License available at # +# # http://creativecommons.org/licenses/by-nc-nd/4.0/ and have to be made # +# # available to the Elemento user community # +# # through the original distribution channels. # +# # The authors make no claims about the suitability # +# # of this software for any purpose. # +# # It is provided "as is" without express or implied warranty. # +# #******************************************************************************# +# +# #------------------------------------------------------------------------------# +# #Electros # +# #Authors: # +# #- Filippo Ferrando Damillano (fferrando at elemento.cloud) # +# #- Simone Robaldo (srobaldo at elemento.cloud) # +# #------------------------------------------------------------------------------# +# + +# start background daemons + +#/opt/daemons/Elemento_Daemons_linux_x86 > /var/log/elemento/elemento_daemons.log 2>&1 & +/opt/daemons/Elemento_Daemons_linux_arm >/var/log/elemento/elemento_daemons.log 2>&1 & + +# run the project + +nginx -g 'daemon off;' diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..6b8e5e5 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,158 @@ +# #******************************************************************************# +# # Copyright(c) 2019-2023, Elemento srl, All rights reserved # +# # Author: Elemento srl # +# # Contributors are mentioned in the code where appropriate. # +# # Permission to use and modify this software and its documentation strictly # +# # for personal purposes is hereby granted without fee, # +# # provided that the above copyright notice appears in all copies # +# # and that both the copyright notice and this permission notice appear in the # +# # supporting documentation. # +# # Modifications to this work are allowed for personal use. # +# # Such modifications have to be licensed under a # +# # Creative Commons BY-NC-ND 4.0 International License available at # +# # http://creativecommons.org/licenses/by-nc-nd/4.0/ and have to be made # +# # available to the Elemento user community # +# # through the original distribution channels. # +# # The authors make no claims about the suitability # +# # of this software for any purpose. # +# # It is provided "as is" without express or implied warranty. # +# #******************************************************************************# +# +# #------------------------------------------------------------------------------# +# #Electros # +# #Authors: # +# #- Filippo Ferrando Damillano (fferrando at elemento.cloud) # +# #- Simone Robaldo (srobaldo at elemento.cloud) # +# #------------------------------------------------------------------------------# +# + + +events {} + +http { + + include /etc/nginx/mime.types; # important + default_type application/json; + + server { + set $authenticate http://127.0.0.1:47777; + set $compute http://127.0.0.1:17777; + set $storage http://127.0.0.1:27777; + set $network http://127.0.0.1:37777; + set $services http://127.0.0.1:6777; + set $targets http://127.0.0.1:57777; + + listen 80; + + root /usr/share/nginx/html; + index electros/electros.html; + + location / { + try_files $uri $uri/ =404; + } + + # Static asset aliases + location /js/ { + alias /usr/share/nginx/html/electros/js/; + } + + location /css/ { + alias /usr/share/nginx/html/electros/css/; + } + + location /pages/ { + alias /usr/share/nginx/html/electros/pages/; + } + + location /assets/ { + alias /usr/share/nginx/html/electros/assets/; + } + + location /Electros.svg { + alias /usr/share/nginx/html/electros/Electros.svg; + } + + location /configs/ { + alias /usr/share/nginx/html/electros/configs/; + } + + location /remotes/ { + alias /usr/share/nginx/html/electros/remotes/; + } + + location /favicon/ { + alias /usr/share/nginx/html/electros/favicon/; + } + + location /ecd/ { + alias /usr/share/nginx/html/electros/ecd/; + } + + location /epm/ { + alias /usr/share/nginx/html/electros/epm/; + } + + location /ist/ { + alias /usr/share/nginx/html/electros/ist/; + } + + # Daemon statuses handlers + location /authStatus { + proxy_pass $authenticate/; + } + + location /computeStatus { + proxy_pass $compute/; + } + + location /storageStatus { + proxy_pass $storage/; + } + + location /networksStatus { + proxy_pass $network/; + } + + location /servicesStatus { + proxy_pass $services/; + } + + location /targetsStatus { + proxy_pass $targets/; + } + + # Daemon handlers + location /api/v1/authenticate/ { + proxy_pass $authenticate; + } + + location /api/v1.0/client/backups/ { + proxy_pass $compute; + } + + location /api/v1.0/client/vm/ { + proxy_pass $compute; + } + + # special case for network attach/detach for a VM + location ~ ^/api/v1.0/client/network/(attach|detach) { + proxy_pass $compute; + } + + location /api/v1.0/client/volume/ { + proxy_pass $storage; + } + + location /api/v1.0/client/network/ { + proxy_pass $network; + } + + location /api/v1.0/service/ { + proxy_pass $services; + } + + location /api/v1.0/client/target/ { + proxy_pass $targets; + } + } +} diff --git a/docker/startup.sh b/docker/startup.sh new file mode 100644 index 0000000..06dced7 --- /dev/null +++ b/docker/startup.sh @@ -0,0 +1,35 @@ +#! /bin/bash +# #******************************************************************************# +# # Copyright(c) 2019-2023, Elemento srl, All rights reserved # +# # Author: Elemento srl # +# # Contributors are mentioned in the code where appropriate. # +# # Permission to use and modify this software and its documentation strictly # +# # for personal purposes is hereby granted without fee, # +# # provided that the above copyright notice appears in all copies # +# # and that both the copyright notice and this permission notice appear in the # +# # supporting documentation. # +# # Modifications to this work are allowed for personal use. # +# # Such modifications have to be licensed under a # +# # Creative Commons BY-NC-ND 4.0 International License available at # +# # http://creativecommons.org/licenses/by-nc-nd/4.0/ and have to be made # +# # available to the Elemento user community # +# # through the original distribution channels. # +# # The authors make no claims about the suitability # +# # of this software for any purpose. # +# # It is provided "as is" without express or implied warranty. # +# #******************************************************************************# +# +# #------------------------------------------------------------------------------# +# #Electros # +# #Authors: # +# #- Filippo Ferrando Damillano (fferrando at elemento.cloud) # +# #------------------------------------------------------------------------------# +# +# start background daemons + +/opt/daemons/Elemento_Daemons_linux_x86 >/var/log/elemento/elemento_daemons.log 2>&1 & + +# run the project + +nginx -g 'daemon off;' + diff --git a/electros-electron/common/Daemons.js b/electros-electron/common/Daemons.js new file mode 100644 index 0000000..75de9ae --- /dev/null +++ b/electros-electron/common/Daemons.js @@ -0,0 +1,111 @@ +import {app} from "electron"; +import path from "path"; +import fs from "fs"; +import {spawn, execSync} from "child_process"; +import {Terminal} from "../windows/Terminal.js"; + + +export class DaemonsNotEnabledError extends Error {} + + +export class Daemons { + static _Process = null; + static _Ports = {}; + + static stdoutBuffer = ""; + static stderrBuffer = ""; + static BUFFER_SIZE = 1024; + static flushInterval = null; + + static DataUpdateCriticalHook = null; + + static Launch(platform, __dirname) { + if (app.commandLine.hasSwitch("no-daemons")) { + console.log("Daemons have been disabled by `--no-daemons`"); + Terminal.Write("Elemento Client Daemons have been disabled by `--no-daemons`."); + throw new DaemonsNotEnabledError(); + } + + if (!app.isPackaged) { + Terminal.Write("Electros is not packaged. Daemons might have to be manually started.") + console.log("Electros is not packaged. Daemons might have to be manually started.") + } + + const execPath = Daemons._GetCommand(platform, __dirname); + console.trace(execPath); + + Daemons._Process = spawn( + execPath, [], { + env: { ...process.env, GUI_APP: '1' }, + stdio: ['pipe', 'pipe', 'pipe'], + detached: false, + } + ); + + Daemons._Process.stdout.on("data", (data) => { + // this.stdoutBuffer += data.toString(); + Terminal.Write(data); + }); + + Daemons._Process.stderr.on("data", (data) => { + // this.stderrBuffer += data.toString(); + Terminal.Write(data); + }); + + Daemons._Process.on("error", (data) => { + if (Daemons.DataUpdateCriticalHook) { Daemons.DataUpdateCriticalHook(data); } + }); + } + + static Terminate() { + let r = true; + if (Daemons._Process !== null) { + if (!Daemons._Process.killed) { + if (process.platform === 'win32') { + try { + execSync(`taskkill /pid ${Daemons._Process.pid} /T /F`); + } catch (e) { + console.error("Failed to kill process with taskkill:", e); + } + } else { + r = Daemons._Process.kill(); + } + } + Daemons._Process = null; + } + + return r; + } + + static _GetPath(platform, __dirname) { + const baseDir = app.isPackaged ? process.resourcesPath : path.join(__dirname, '..'); + return path.join(baseDir, 'electros-daemons', platform.os, platform.arch); + } + + static _GetCommand(platform, __dirname) { + const daemonsPath = Daemons._GetPath(platform, __dirname); + let daemonsCmd = ''; + + if (platform.isMac()) { + daemonsCmd = path.join(daemonsPath, "elemento_client_daemons.app/Contents/MacOS/elemento_client_daemons"); + } else if (platform.isLinux()) { + if (platform.arch === 'arm64') { + daemonsCmd = path.join(daemonsPath, `elemento_daemons_linux_arm`); + } else { + daemonsCmd = path.join(daemonsPath, `elemento_daemons_linux_x86`); + } + } else if (platform.isWin()) { + if (platform.arch === 'x64' || platform.arch === 'x86') { + daemonsCmd = path.join(daemonsPath, `elemento_daemons_win_x86.exe`); + if (!fs.existsSync(daemonsCmd)) { + daemonsCmd = path.join(daemonsPath, `elemento_daemons_win_x64.exe`); + } + } else { + daemonsCmd = path.join(daemonsPath, `elemento_daemons_win_arm64.exe`); + } + } + + return daemonsCmd; + } +} + diff --git a/electros-electron/common/Loaders.js b/electros-electron/common/Loaders.js new file mode 100644 index 0000000..53f5514 --- /dev/null +++ b/electros-electron/common/Loaders.js @@ -0,0 +1,67 @@ +import fs from "fs"; +import path from "path"; + + +export class Loaders { + Css = { + Themes: null, + FormControl: null, + Titlebar: null + } + + Js = { + Titlebar: null, + Themes: null, + FormStyle: null + } + + constructor(__dirname) { + try { + this.Css.Titlebar = fs.readFileSync(path.join(__dirname, 'titlebar', 'titlebar.css'), 'utf8'); + const titlebarTempJs = fs.readFileSync(path.join(__dirname, 'titlebar', 'titlebar.js'), 'utf8'); + this.Css.Themes = fs.readFileSync(path.join(__dirname, 'electros', 'css', 'themes.css'), 'utf8'); + this.Css.FormControl = fs.readFileSync(path.join(__dirname, 'electros', 'css', 'form-controls.css'), 'utf8'); + + this.Js.Themes = ` + var theme = document.createElement('style'); + theme.textContent = ${JSON.stringify(this.Css.Themes || '')}; + document.head.appendChild(theme); + `; + + this.Js.FormStyle = ` + var formstyle = document.createElement('style'); + formstyle.textContent = ${JSON.stringify(this.Css.FormControl || '')}; + document.head.appendChild(formstyle); + `; + + this.Js.Titlebar = ` + ${this.Js.Themes} + ${this.Js.FormControl} + + var style = document.createElement('style'); + style.textContent = ${JSON.stringify(this.Css.Titlebar || '')}; + document.head.appendChild(style); + + // Create titlebar div and title element + var titlebar = document.createElement('div'); + titlebar.className = 'electros-titlebar'; + + var titleElement = document.createElement('div'); + titleElement.className = 'electros-titlebar-title'; + titleElement.textContent = document.title; + + document.body.insertBefore(titlebar, document.body.firstChild); + + // Load and execute titlebar.js content + ${titlebarTempJs} + + initializeTitlebar(options = { minimizeOnly: false }); + + titlebar.appendChild(titleElement); + `; + } catch (e) { + console.error(e); + throw e; + } + } +} diff --git a/electros-electron/common/MenuBar.js b/electros-electron/common/MenuBar.js new file mode 100644 index 0000000..9733a3e --- /dev/null +++ b/electros-electron/common/MenuBar.js @@ -0,0 +1,111 @@ +import { Daemons } from "./Daemons.js"; +import { Terminal } from "../windows/Terminal.js"; +import {app, Notification} from "electron"; + + +export function BuildMenuTemplate() { + const baseMenu = [ + { + label: 'Electros', + submenu:[ + {label: 'Reload', role: 'reload'}, + { + label: 'Open Terminal', + accelerator: 'CmdOrCtrl+T', + click: () => { Terminal.ToggleVisibility(); } + }, + {type: "separator"}, + {role: "close"}, + {role: 'quit'}, + ] + }, + { + label: 'Edit', + submenu: [ + {role: 'undo'}, + {role: 'redo'}, + {type: 'separator'}, + {role: 'cut'}, + {role: 'copy'}, + {role: 'paste'}, + {role: 'delete'}, + {type: 'separator'}, + {role: 'selectAll'} + ] + }, + { + label: 'View', + submenu: [ + { + label: 'Actual Size', + accelerator: 'CmdOrCtrl+0', + click: (menuItem, browserWindow) => { + if (browserWindow) { + browserWindow.webContents.setZoomFactor(1); + } + } + }, + { + label: 'Zoom In', + accelerator: 'CmdOrCtrl+=', + click: (menuItem, browserWindow) => { + if (browserWindow) { + const currentZoom = browserWindow.webContents.getZoomFactor(); + browserWindow.webContents.setZoomFactor(currentZoom + 0.1); + } + } + }, + { + label: 'Zoom Out', + accelerator: 'CmdOrCtrl+-', + click: (menuItem, browserWindow) => { + if (browserWindow) { + const currentZoom = browserWindow.webContents.getZoomFactor(); + browserWindow.webContents.setZoomFactor(Math.max(0.1, currentZoom - 0.1)); + } + } + }, + {type: 'separator'}, + {role: 'togglefullscreen'} + ] + } + ]; + + if (!app.isPackaged) { + baseMenu.push({ + label: 'Developer', + submenu:[ + { + label: 'Terminate Daemons', + click: async () => { + console.log("manual daemon termination triggered"); + if(Daemons.Terminate()) { + if(Notification.isSupported()) { + new Notification({ + title: "Daemons Terminated", + body: "Electros Client Daemons successfully terminated.", + silent: true, + urgency: 'low' + }).show(); + } + } else { + if(Notification.isSupported()) { + new Notification({ + title: "Failed to Terminate Daemons", + body: "Electros Client Daemons were not terminated.", + silent: true, + urgency: 'low' + }).show(); + } + } + } + }, + {label: 'Toggle DevTools', role: 'toggleDevTools'}, + {label: 'Toggle Fullscreen', role: 'toggleFullScreen'}, + ] + }) + } + + return baseMenu; +} + diff --git a/electros-electron/common/Platform.js b/electros-electron/common/Platform.js new file mode 100644 index 0000000..ff0a2ef --- /dev/null +++ b/electros-electron/common/Platform.js @@ -0,0 +1,98 @@ +import * as os from "node:os"; +// import { execSync } from "node:child_process"; + + +/** + * @summary Utility class that has the current OS platform. + * + * @author srobaldo + */ +export class Platform { + os = os.platform(); + arch = os.arch(); + + constructor() { + if (this.os === 'darwin') { + this.os = 'mac'; + } else if (this.os === 'linux') { + this.os = 'linux'; + } else if (this.os.includes('win')) { + this.os = 'win'; + } + + if (this.arch.toLowerCase() === 'x86' || this.arch.toLowerCase() === 'x64') { + this.arch = 'x64'; + } + } + + /** + * Checks if the Current OS is `win` + * + * @return {boolean} + */ + isWin() { + return this.os === 'win'; + } + + /** + * Checks if the Current OS is `mac` (darwin) + * @return {boolean} + */ + isMac() { + return this.os === 'mac'; + } + + /** + * Checks if the Current OS is `linux` + * @return {boolean} + */ + isLinux() { + return this.os === 'linux'; + } + + /** + * Checks if the Current OS is either `linux`, `mac` or `darwin` + * @return {boolean} + */ + isUnix() { + return this.isLinux() || this.isMac(); + } + + /** + * Checks if the Current OS is neither UNIX nor Windows. + * @return {boolean} + */ + isUndefined() { + return (!this.isWin()) && (!this.isUnix()); + } + + /** + * Checks through DBus if the OS supports Tray Icons + * @return {boolean} + */ + dbusHasTraySupport() { + if (!this.isLinux()) { + return true; + } + + return false; + + // method doesn't always work, cannot be trusted; assuming that all Linux distros don't have Tray Icons to + // avoid making a permanent backg. process. + // @author srobaldo + + // try { + // execSync( + // "dbus-send --session --dest=org.freedesktop.DBus" + + // "--type=method_call /org/freedesktop/DBus" + + // "org.freedesktop.DBus.NameHasOwner" + + // "string:org.kde.StatusNotifierWatcher", + // { stdio: "ignore" } + // ); + // + // return true; + // } catch (e) { + // return false; + // } + } +} diff --git a/electros-electron/common/PortHandler.js b/electros-electron/common/PortHandler.js new file mode 100644 index 0000000..2371460 --- /dev/null +++ b/electros-electron/common/PortHandler.js @@ -0,0 +1,18 @@ + +export class PortHandler { + static _ActivePorts = new Set(); + static _NextPort = 49152; + + static GetAvailablePort () { + while (this._ActivePorts.has(this._NextPort)) { + this._NextPort++; + if (this._NextPort > 65535) this._NextPort = 49152; + } + this._ActivePorts.add(this._NextPort); + return this._NextPort++; + } + + static ReleasePort (port) { + this._ActivePorts.delete(port); + } +} diff --git a/electros-electron/common/TrayIcon.js b/electros-electron/common/TrayIcon.js new file mode 100644 index 0000000..6fe5b5f --- /dev/null +++ b/electros-electron/common/TrayIcon.js @@ -0,0 +1,80 @@ +import { Tray, nativeImage, nativeTheme, Menu, app } from 'electron'; +import path from 'path'; +import {Terminal} from "../windows/Terminal.js"; + + +export class TrayIcon { + get tray() { return this._tray; } + + _tray = null; + + /** + * + * @param {Platform} platform + * @param {string} __dirname + * @return {TrayIcon} + */ + constructor(platform, __dirname) { + this._tray = new Tray(this._getIcon(platform, __dirname)); + + nativeTheme.on('updated', () => { + this._tray.setImage(this._getIcon(platform, __dirname)); + }); + + this._tray.setToolTip('Electros Daemons'); + this._tray.setContextMenu(this._getMenu()); + } + + _getIcon(platform, __dirname) { + const isLight = nativeTheme.shouldUseDarkColors; + console.log(`The theme is light: ${isLight}`); + + if (platform.isMac()) { + const templateIcon = path.join(__dirname, 'electros.iconset', 'tray_icon_black_32x32@2x.png'); + const icon = nativeImage.createFromPath(templateIcon); + icon.setTemplateImage(true); + return icon; + } + + if (platform.isWin()) { + const iconName = path.join(__dirname, 'electros.iconset', 'tray_icon.ico'); + return nativeImage.createFromPath(iconName); + } + + if (platform.isLinux()) { + const iconName = path.join(__dirname, 'electros.iconset', 'tray_icon.png'); + const icon = nativeImage.createFromPath(iconName); + return icon; + } + } + + _getMenu() { + return Menu.buildFromTemplate([ + { + label: 'Toggle Terminal', + click: () => { + Terminal.ToggleVisibility(); + } + }, + { type: 'separator' }, + { + label: 'Quit', + click: () => { + app.quit(); + } + } + ]); + } +} + + +/* + +let tray = null; + +function createTrayIcon() { + +} + + + */ diff --git a/electros-electron/common/WindowOptions.js b/electros-electron/common/WindowOptions.js new file mode 100644 index 0000000..fec4e6b --- /dev/null +++ b/electros-electron/common/WindowOptions.js @@ -0,0 +1,14 @@ +export const WindowOptions = Object.freeze({ + Common: { + frame: false, + transparent: false, + titleBarStyle: 'hiddenInset', + trafficLightPosition: { x: -100, y: -100 }, + alwaysOnTop: false + }, + Default: { + frame: true, + transparent: false, + alwaysOnTop: false + } +}); \ No newline at end of file diff --git a/electros-electron/common/package.json b/electros-electron/common/package.json new file mode 100644 index 0000000..aead43d --- /dev/null +++ b/electros-electron/common/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/electros-electron/main.js b/electros-electron/main.js index 8c4b345..35f2997 100644 --- a/electros-electron/main.js +++ b/electros-electron/main.js @@ -1,293 +1,51 @@ -const { app, BrowserWindow, ipcMain, Menu, Tray, Notification, nativeTheme , shell } = require('electron'); +const {app, BrowserWindow, ipcMain, Menu, nativeTheme, shell} = require('electron'); + +app.commandLine.appendSwitch('disable-zoom-level-persistence'); const path = require('path'); -const fs = require('fs'); -const os = require('os'); -const { spawn } = require('child_process'); -const nativeImage = require('electron').nativeImage; +const {spawn} = require('child_process'); const net = require('net'); -const { globalShortcut } = require('electron'); +const {globalShortcut} = require('electron'); // Import handlers from separate files. They are apparently unused since called by name in the preload.js const configHandlers = require('./js/config-ipc'); const titlebarHandlers = require('./js/titlebar-ipc'); +const {Loaders} = require("./common/Loaders"); +const {WindowOptions} = require("./common/WindowOptions"); +const {BuildMenuTemplate} = require("./common/MenuBar"); +const {PortHandler} = require("./common/PortHandler"); +const {Platform} = require("./common/Platform"); +const {Daemons} = require("./common/Daemons"); +const {Terminal} = require("./windows/Terminal"); +const {RdpWindow} = require("./windows/Rdp.js"); +const {DaemonsNotEnabledError} = require("./common/Daemons.js"); + let mainWindow = null; let terminalWindow = null; -let daemonProcess = null; -let tray = null; - -// Add buffering variables for daemon output -let stdoutBuffer = ''; -let stderrBuffer = ''; -const BUFFER_SIZE = 1024; // 1KB chunks -let flushInterval = null; - -let platform = os.platform(); - -const commonWindowOptions = { - frame: false, - transparent: false, - titleBarStyle: 'hiddenInset', - trafficLightPosition: { x: -100, y: -100 }, - alwaysOnTop: false -} - -const defaultWindowOptions = { - frame: true, - transparent: false, - alwaysOnTop: false -} - -// Pre-load CSS files to avoid synchronous file reading during startup -let themesCSS = null; -let formControlsCSS = null; -let titlebarCSS = null; -let titlebarJS = null; - -function preloadCSSFiles() { - try { - themesCSS = fs.readFileSync(path.join(__dirname, 'electros', 'css', 'themes.css'), 'utf8'); - formControlsCSS = fs.readFileSync(path.join(__dirname, 'electros', 'css', 'form-controls.css'), 'utf8'); - titlebarCSS = fs.readFileSync(path.join(__dirname, 'titlebar', 'titlebar.css'), 'utf8'); - titlebarJS = fs.readFileSync(path.join(__dirname, 'titlebar', 'titlebar.js'), 'utf8'); - } catch (error) { - console.error('Error preloading CSS files:', error); - } -} - -// Pre-load files before creating windows -preloadCSSFiles(); - -const themesLoaderJS = ` - // Create and load CSS inline - var theme = document.createElement('style'); - theme.textContent = ${JSON.stringify(themesCSS || '')}; - document.head.appendChild(theme); -` - -const formStyleLoaderJS = ` - // Create and load CSS inline - var formstyle = document.createElement('style'); - formstyle.textContent = ${JSON.stringify(formControlsCSS || '')}; - document.head.appendChild(formstyle); -` - -const titlebarCustomJS = ` - ${themesLoaderJS} - ${formStyleLoaderJS} - - var style = document.createElement('style'); - style.textContent = ${JSON.stringify(titlebarCSS || '')}; - document.head.appendChild(style); - - // Create titlebar div and title element - var titlebar = document.createElement('div'); - titlebar.className = 'electros-titlebar'; - - var titleElement = document.createElement('div'); - titleElement.className = 'electros-titlebar-title'; - titleElement.textContent = document.title; - - document.body.insertBefore(titlebar, document.body.firstChild); - - // Load and execute titlebar.js content - ${titlebarJS || ''} - - initializeTitlebar(options = { minimizeOnly: false }); - - titlebar.appendChild(titleElement); -`; - -const menuTemplate = [ - { - label: 'Electros', - submenu:[ - {role: 'quit'} - ] - }, - { - label: 'Edit', - submenu: [ - {role: 'undo'}, - {role: 'redo'}, - {type: 'separator'}, - {role: 'cut'}, - {role: 'copy'}, - {role: 'paste'}, - {role: 'delete'}, - {type: 'separator'}, - {role: 'selectAll'} - ] - }, - { - label: 'View', - submenu: [ - { - label: 'Toggle Terminal', - accelerator: 'CmdOrCtrl+T', - click: () => { - if (terminalWindow) { - terminalWindow.isVisible() ? terminalWindow.hide() : terminalWindow.show(); - } - } - }, - {type: 'separator'}, - {role: 'resetZoom', zoomFactor: 0.8}, - {role: 'zoomIn'}, - {role: 'zoomOut'}, - {type: 'separator'}, - {role: 'togglefullscreen'} - ] - }, - { - label: 'Developer', - submenu:[ - { - label: 'Terminate Daemons', - click: async () => { - console.log("manual daemon termination triggered"); - try { - if (daemonProcess === null) { - killDaemons(true); - } - - if(Notification.isSupported()) { - new Notification({ - title: "Daemons Terminated", - body: "Electros Client Daemons successfully terminated.", - silent: true, - icon: './electros.iconset/icon_256x256.png', - urgency: 'low' - }).show(); - } - } catch (e) { - console.error(e); - if(Notification.isSupported()) { - new Notification({ - title: "Failed to Terminate Daemons", - body: "Electros Client Daemons were not terminated.", - silent: true, - urgency: 'low' - }).show(); - } - } - } - }, - {label: 'Reload', role: 'reload'}, - {label: 'Toggle DevTools', role: 'toggleDevTools'}, - {label: 'Toggle Fullscreen', role: 'toggleFullScreen'}, - {label: 'Toggle Zoom', role: 'toggleZoom'}, - ] - } -] - -const activePorts = new Set(); -let nextPort = 49152; - -function getAvailablePort() { - while (activePorts.has(nextPort)) { - nextPort++; - if (nextPort > 65535) nextPort = 49152; - } - activePorts.add(nextPort); - return nextPort++; -} - -function releasePort(port) { - activePorts.delete(port); -} - -function getDaemonCommand() { - console.log(`The platform is: ${platform}`); - var arch = os.arch(); - console.log(`The CPU architecture is: ${arch}`); - - if (platform === 'darwin') { - platform = 'mac'; - } else if (platform === 'linux') { - platform = 'linux'; - } else if (platform.includes('win')) { - platform = 'win'; - } - - if (arch.toLowerCase() === 'x86' || arch.toLowerCase() === 'x64') { - arch = 'x64'; - } - - // Use process.resourcesPath in production, fallback to __dirname in development - const baseDir = app.isPackaged ? process.resourcesPath : path.join(__dirname, '..'); - const deamons_path = path.join(baseDir, 'electros-daemons', platform, arch); - console.log(`The deamons path is: ${deamons_path}`); - - let daemons_cmd = ''; - - if (platform === 'mac') { - daemons_cmd = path.join(deamons_path, "elemento_client_daemons.app/Contents/MacOS/elemento_client_daemons"); - } else if (platform === 'linux') { - if (arch === 'arm64') { - daemons_cmd = path.join(deamons_path, `elemento_daemons_linux_arm`); - } else { - daemons_cmd = path.join(deamons_path, `elemento_daemons_linux_x86`); - } - } else if (platform === 'win') { - if (arch === 'x64' || arch === 'x86') { - daemons_cmd = path.join(deamons_path, `elemento_daemons_windows_x86.exe`); - if (!fs.existsSync(daemons_cmd)) { - daemons_cmd = path.join(deamons_path, `elemento_daemons_windows_x64.exe`); - } - } else { - daemons_cmd = path.join(deamons_path, `elemento_daemons_windows_arm64.exe`); - } - } - - console.log(`The daemons command is: ${daemons_cmd}`); - return daemons_cmd; -} - -const daemons_cmd = getDaemonCommand(); -// Add this function to create different tray icons -function createTrayIcon() { - const isLight = nativeTheme.shouldUseDarkColors; - console.log(`The theme is light: ${isLight}`); - - if (platform === 'mac') { - const templateIcon = path.join(__dirname, 'electros.iconset', 'tray_icon_black_32x32@2x.png'); - const icon = nativeImage.createFromPath(templateIcon); - icon.setTemplateImage(true); - return icon; - } +const PreloadedContent = new Loaders(__dirname); +const platform = new Platform(); +const Rdp = new RdpWindow(PreloadedContent, platform, __dirname); - if (platform === 'win') { - const iconName = path.join(__dirname, 'electros.iconset', 'tray_icon.ico'); - const icon = nativeImage.createFromPath(iconName); - return icon; - } - - if (platform === 'linux') { - const iconName = path.join(__dirname, 'electros.iconset', 'tray_icon.png'); - const icon = nativeImage.createFromPath(iconName); - return icon; - } -} function createMainWindow() { const win = new BrowserWindow({ width: 1800, height: 1200, - ...commonWindowOptions, + ...WindowOptions.Common, webPreferences: { nodeIntegration: true, contextIsolation: true, preload: path.join(__dirname, 'preload.js'), - zoomFactor: 0.8, + zoomFactor: 1.0, backgroundThrottling: false, enableRemoteModule: false, - experimentalFeatures: false + experimentalFeatures: false, + devTools: !app.isPackaged || app.commandLine.hasSwitch("allow-debug"), } }); - if (platform === 'mac') { + if (platform.os === 'mac') { win.setWindowButtonVisibility(false); } @@ -296,164 +54,52 @@ function createMainWindow() { // Inject custom titlebar after the page loads win.webContents.on('did-finish-load', () => { try { - // Ensure titlebarCustomJS is a string and properly escaped - const safeJS = typeof titlebarCustomJS === 'string' - ? titlebarCustomJS - : JSON.stringify(titlebarCustomJS); + const safeJS = typeof PreloadedContent.Js.Titlebar === 'string' + ? PreloadedContent.Js.Titlebar + : JSON.stringify(PreloadedContent.Js.Titlebar); - win.webContents.executeJavaScript(safeJS).catch(err => {}); + win.webContents.executeJavaScript(safeJS).catch(err => { + throw err; + }); } catch (error) { console.error('Error injecting titlebar:', error); } }); + win.webContents.setVisualZoomLevelLimits(1, 1); + return win; } -// Replace the daemon spawn code with this function createTerminalWindow() { - terminalWindow = new BrowserWindow({ - width: 800, - height: 600, - ...commonWindowOptions, - show: false, - frame: false, - webPreferences: { - nodeIntegration: true, - contextIsolation: false, - zoomFactor: 0.8, - backgroundThrottling: false, - enableRemoteModule: false, - experimentalFeatures: false, - webSecurity: false - }, - backgroundColor: '#000000', - title: 'Electros Daemons' - }); - - terminalWindow.loadFile('terminal/terminal.html'); - - // Wait for the terminal window to be ready terminalWindow.webContents.on('did-finish-load', () => { - - // Start the daemon process with the actual daemon command - daemonProcess = spawn(daemons_cmd, [], { - env: { - ...process.env, - GUI_APP: '1' - }, - stdio: ['pipe', 'pipe', 'pipe'], - detached: false - }); - - // Buffered process output to the renderer - daemonProcess.stdout.on('data', (data) => { - stdoutBuffer += data.toString(); - if (stdoutBuffer.length >= BUFFER_SIZE) { - terminalWindow.webContents.send('terminal-output', stdoutBuffer); - stdoutBuffer = ''; - } - }); - - daemonProcess.stderr.on('data', (data) => { - stderrBuffer += data.toString(); - if (stderrBuffer.length >= BUFFER_SIZE) { - terminalWindow.webContents.send('terminal-output', stderrBuffer); - stderrBuffer = ''; - } - }); - - daemonProcess.on('error', (error) => { - terminalWindow.webContents.send('terminal-output', `Error: ${error.message}\n`); - }); - - // Set up periodic buffer flushing - flushInterval = setInterval(() => { - if (stdoutBuffer) { - terminalWindow.webContents.send('terminal-output', stdoutBuffer); - stdoutBuffer = ''; - } - if (stderrBuffer) { - terminalWindow.webContents.send('terminal-output', stderrBuffer); - stderrBuffer = ''; - } - }, 100); // Flush every 100ms - - // Inject custom titlebar - const popupTitlebarJS = titlebarCustomJS.replace( - 'titleElement.textContent = document.title;', - `titleElement.textContent = ${JSON.stringify(terminalWindow.title)};` - ) //.replace( -// 'initializeTitlebar(options = { minimizeOnly: false });', -// 'initializeTitlebar(options = { minimizeOnly: true });' -// ); - - terminalWindow.webContents.executeJavaScript(popupTitlebarJS); - }); - - // Create tray icon if it doesn't exist - if (!tray) { - tray = new Tray(createTrayIcon()); - - // Update icon when system theme changes - nativeTheme.on('updated', () => { - tray.setImage(createTrayIcon()); - }); - - const contextMenu = Menu.buildFromTemplate([ - { - label: 'Show Terminal', - click: () => { - terminalWindow.show(); - } - }, - { - label: 'Hide Terminal', - click: () => { - terminalWindow.hide(); - } - }, - { type: 'separator' }, - { - label: 'Quit', - click: () => { - app.quit(); - } - } - ]); - - tray.setToolTip('Electros Daemons'); - tray.setContextMenu(contextMenu); - } - - // Handle window close button - terminalWindow.on('close', (event) => { - event.preventDefault(); // Prevent window from closing - terminalWindow.hide(); // Hide instead of close - }); - - // Add IPC handlers for window controls - ipcMain.on('minimize-window', () => { - terminalWindow.minimize(); - }); - - ipcMain.on('hide-window', () => { - terminalWindow.hide(); + Daemons.Launch(platform, __dirname); + + // // Set up periodic buffer flushing + // flushInterval = setInterval(() => { + // if (stdoutBuffer) { + // terminalWindow.webContents.send('terminal-output', stdoutBuffer); + // stdoutBuffer = ''; + // } + // if (stderrBuffer) { + // terminalWindow.webContents.send('terminal-output', stderrBuffer); + // stderrBuffer = ''; + // } + // }, 100); // Flush every 100ms + + terminalWindow.webContents.executeJavaScript(PreloadedContent.Js.Titlebar); }); } function createWindows() { try { - createTerminalWindow(); + Terminal.CreateWindow(PreloadedContent, platform, __dirname); mainWindow = createMainWindow(); mainWindow.on('closed', () => { - if (terminalWindow) { - terminalWindow.hide(); - } + app.quit(); }); - // Register a local shortcut for the main window setupWindowShortcuts(mainWindow); } catch (error) { console.error('Error creating windows:', error); @@ -462,47 +108,27 @@ function createWindows() { // Add this function to set up window-specific shortcuts function setupWindowShortcuts(window) { - // Keep track of registered shortcuts for this window - const shortcuts = []; + let quitShortcut = false; - // When window is focused, set up its shortcuts window.on('focus', () => { - // Only register if not already registered - if (shortcuts.length === 0) { - // For Windows/Linux: Alt+F4 closes current window instead of app -// const altF4Registered = globalShortcut.register('Alt+F4', () => { -// const focusedWindow = BrowserWindow.getFocusedWindow(); -// if (focusedWindow) { -// focusedWindow.close(); -// return true; // Prevent default behavior -// } -// // For main window, let the default Alt+F4 behavior happen -// return false; -// }); -// if (altF4Registered) { -// shortcuts.push('Alt+F4'); -// } - - // For macOS: Cmd+Q closes current window if it's not main - // const cmdQRegistered = globalShortcut.register('Command+Q', () => { - // const focusedWindow = BrowserWindow.getFocusedWindow(); - // if (focusedWindow) { - // focusedWindow.close(); - // return true; // Prevent default quit - // } - // // Let the default Cmd+Q behavior happen for main window - // return false; - // }); - // if (cmdQRegistered) { - // shortcuts.push('Command+Q'); - // } + if (platform.isMac()) { + } else { + if (!quitShortcut) { + globalShortcut.register('Alt+F4', () => { + Daemons.Terminate(); + app.quit(); + }); + quitShortcut = true; + } } }); - // Clean up when window is closed + window.on('blur', () => { + globalShortcut.unregister('Alt+F4'); + }); + window.on('closed', () => { - shortcuts.forEach(shortcut => globalShortcut.unregister(shortcut)); - shortcuts.length = 0; // Clear the array + globalShortcut.unregister('Alt+F4'); }); } @@ -520,7 +146,7 @@ ipcMain.handle('create-popup', async (event, options = {}) => { contextIsolation: true, preload: path.join(__dirname, 'preload.js'), webSecurity: false, - zoomFactor: 0.8, + zoomFactor: 1, backgroundThrottling: false, enableRemoteModule: false, experimentalFeatures: false @@ -528,8 +154,7 @@ ipcMain.handle('create-popup', async (event, options = {}) => { ...options }); - // Set up window-specific shortcuts for this popup - setupWindowShortcuts(popup); + popup.webContents.setVisualZoomLevelLimits(1, 1); if (options.title) { popup.setTitle(options.title); @@ -547,7 +172,7 @@ ipcMain.handle('create-popup', async (event, options = {}) => { // Inject custom titlebar CSS and HTML before loading the URL if (!options.defaultTitlebar) { popup.webContents.on('did-finish-load', () => { - const popupTitlebarJS = titlebarCustomJS.replace( + const popupTitlebarJS = PreloadedContent.Js.Titlebar.replace( 'titleElement.textContent = document.title;', `titleElement.textContent = ${JSON.stringify(options.title)};` ); @@ -578,165 +203,51 @@ ipcMain.handle('create-popup', async (event, options = {}) => { } }); -// First, let's define our cleanup function separately so we can reuse it -function cleanupRDPProcess(webContents, port) { - // Release the port - if (port) { - releasePort(parseInt(port)); - } - - // Kill the mstsc process if it exists - if (webContents.mstscProcess) { - try { - const proc = webContents.mstscProcess; - - if (platform === 'mac' || platform === 'linux') { - // On Unix systems, kill the entire process group - try { - process.kill(-proc.pid, 'SIGTERM'); - setTimeout(() => { - try { - process.kill(-proc.pid, 'SIGKILL'); - } catch (e) { - // Process already dead, ignore - } - }, 1000); - } catch (err) { - if (err.code !== 'ESRCH') { - console.error('Error killing mstsc process group:', err); - } - } - } else if (platform === 'win') { - // On Windows, use taskkill to force kill the process tree - try { - spawn('taskkill', ['/pid', proc.pid.toString(), '/T', '/F']); - } catch (err) { - console.error('Error killing mstsc process on Windows:', err); - } - } - - // Also try to kill the process directly - try { - if (!proc.killed) { - proc.kill('SIGTERM'); - setTimeout(() => { - try { - if (!proc.killed) { - proc.kill('SIGKILL'); - } - } catch (e) { - // Process already dead, ignore - } - }, 1000); - } - } catch (err) { - if (err.code !== 'ESRCH') { - console.error('Error killing mstsc process directly:', err); - } - } - } finally { - // Clear the process reference - webContents.mstscProcess = null; - if (webContents.mstscPort) { - releasePort(parseInt(webContents.mstscPort)); - webContents.mstscPort = null; - } - } - } -} - -function killDaemons(rethrowExceptions = false) { - if (daemonProcess) { - if (platform === 'mac') { - try { - if (daemonProcess.pid) { - process.kill(daemonProcess.pid, 'SIGTERM'); - process.kill(-daemonProcess.pid); // Kill process group - } - } catch (err) { - if (err.code === 'ESRCH') { - console.log('Daemon process or group already terminated'); - } else { - console.error('Error killing daemon process on macOS:', err); - } - - if(rethrowExceptions) throw err; - } - } else if (platform === 'linux' && daemonProcess.pid) { - try { - process.kill(daemonProcess.pid, 0); - process.kill(-daemonProcess.pid); - } catch (err) { - if (err.code === 'ESRCH') { - console.log('Daemon process group already terminated'); - } else { - console.error('Error killing daemon process group:', err); - } - - if(rethrowExceptions) throw err; - } - } - - try { - if (!daemonProcess.killed) { - daemonProcess.kill(); - } - } catch (err) { - if (err.code === 'ESRCH') { - console.log('Daemon process already terminated'); - } else { - console.error('Error killing daemon process:', err); - } - - if(rethrowExceptions) throw err; - } - } -} // Then modify the before-quit handler to use the cleanup function directly app.on('before-quit', () => { console.log('Quitting app, killing processes'); // Clean up flush interval - if (flushInterval) { - clearInterval(flushInterval); - flushInterval = null; - } - - // Flush any remaining buffers - if (stdoutBuffer && terminalWindow && !terminalWindow.isDestroyed()) { - terminalWindow.webContents.send('terminal-output', stdoutBuffer); - stdoutBuffer = ''; - } - if (stderrBuffer && terminalWindow && !terminalWindow.isDestroyed()) { - terminalWindow.webContents.send('terminal-output', stderrBuffer); - stderrBuffer = ''; - } + // if (flushInterval) { + // clearInterval(flushInterval); + // flushInterval = null; + // } + // + // // Flush any remaining buffers + // if (stdoutBuffer && terminalWindow && !terminalWindow.isDestroyed()) { + // terminalWindow.webContents.send('terminal-output', stdoutBuffer); + // stdoutBuffer = ''; + // } + // if (stderrBuffer && terminalWindow && !terminalWindow.isDestroyed()) { + // terminalWindow.webContents.send('terminal-output', stderrBuffer); + // stderrBuffer = ''; + // } // Close all windows except terminal - const windows = BrowserWindow.getAllWindows(); - windows.forEach(window => { - // Clean up any mstsc processes - if (window.webContents.mstscProcess) { - cleanupRDPProcess(window.webContents); - } - - if (window !== terminalWindow && !window.isDestroyed()) { - window.destroy(); - } - }); + try { + const windows = BrowserWindow.getAllWindows(); + windows.forEach(window => { + // Clean up any mstsc processes + if (window.webContents.mstscProcess) { + Rdp.handleCloseRdpProcess(window.webContents); + } - // Kill daemon process - killDaemons(); + if (window !== terminalWindow && !window.isDestroyed()) { + window.destroy(); + } + }); - // Finally destroy the terminal window - if (terminalWindow && !terminalWindow.isDestroyed()) { - terminalWindow.destroy(); + Terminal.DestroyWindow(); + } catch (error) { + console.error(error); } + + Daemons.Terminate(); }); // Add this IPC handler before app.whenReady() -ipcMain.handle('check-port', async (event, { ip, port }) => { +ipcMain.handle('check-port', async (event, {ip, port}) => { return new Promise((resolve) => { const socket = new net.Socket(); @@ -763,15 +274,24 @@ ipcMain.handle('check-port', async (event, { ip, port }) => { }); app.whenReady().then(() => { - const menu = Menu.buildFromTemplate(menuTemplate); + const menu = Menu.buildFromTemplate( + BuildMenuTemplate(), + ); Menu.setApplicationMenu(menu); + + try { + Daemons.Launch(platform, __dirname); + } catch (e) { + if (!(e instanceof DaemonsNotEnabledError)) { + console.error(e); + } + } + createWindows(); }); app.on('window-all-closed', () => { - if (process.platform !== 'mac') { - app.quit(); - } + app.quit(); }); app.on('activate', () => { @@ -792,235 +312,62 @@ app.on('activate', () => { } }); -ipcMain.handle('open-rdp', async (event, connectionDetails) => { - const rdpWindow = new BrowserWindow({ - width: 1024, - height: 768, - ...commonWindowOptions, - webPreferences: { - nodeIntegration: true, - contextIsolation: true, - preload: path.join(__dirname, 'preload.js'), - webSecurity: false, - zoomFactor: 0.8, - backgroundThrottling: false, - enableRemoteModule: false, - experimentalFeatures: false - } - }); - - // Set up ALL certificate handlers before loading the page - rdpWindow.webContents.session.setCertificateVerifyProc((request, callback) => { - callback(0); // 0 means success - }); - - rdpWindow.webContents.on('certificate-error', (event, url, error, certificate, callback) => { - event.preventDefault(); - callback(true); - }); - - // Additional certificate bypass for self-signed certificates - rdpWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { - callback(true); - }); - - // Set additional security options - rdpWindow.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { - callback({ requestHeaders: { ...details.requestHeaders } }); - }); - - // Inject custom titlebar after the page loads - rdpWindow.webContents.on('did-finish-load', () => { - const rdpTitlebarJS = titlebarCustomJS.replace( - 'titleElement.textContent = document.title;', - `titleElement.textContent = "RDP connection to ${connectionDetails.vmName}";` - ); - rdpWindow.webContents.executeJavaScript(rdpTitlebarJS); - }); - - // Set up window-specific shortcuts - setupWindowShortcuts(rdpWindow); - - // Load the RDP client page with connection details - try { - await rdpWindow.loadFile('electros/remotes/rdp/index.html', { - query: connectionDetails - }); - } catch (error) { - console.error('Error loading RDP window:', error); - throw error; - } - - // Modify the RDP window close handler - rdpWindow.on('close', async (event) => { - try { - // Prevent the window from closing immediately - event.preventDefault(); - - // Send the close event to the renderer - if (!rdpWindow.isDestroyed()) { - rdpWindow.webContents.send('window-close'); - } - - // Clean up the RDP process directly - if (rdpWindow.webContents.mstscProcess) { - cleanupRDPProcess(rdpWindow.webContents); - } - - // Wait a moment for cleanup - await new Promise(resolve => setTimeout(resolve, 100)); - - // Now destroy the window - if (!rdpWindow.isDestroyed()) { - rdpWindow.destroy(); - } - } catch (error) { - console.error('Error during window cleanup:', error); - // Ensure the window is destroyed even if there's an error - if (!rdpWindow.isDestroyed()) { - rdpWindow.destroy(); - } - } - }); - - return rdpWindow.id; -}); - ipcMain.handle('os-prefers-dark-theme', (event) => { return nativeTheme?.shouldUseDarkColors ?? false; -}) +}); ipcMain.handle('os-prefers-reduced-transparency', (event) => { - return nativeTheme?.prefersReducedTransparency ?? false; -}) - -// Add these IPC handlers -ipcMain.handle('launch-rdp-process', async (event, { credentials, width, height }) => { - try { - const ws_port = getAvailablePort().toString(); - - const args = [ - '--target', credentials.ip, - '--user', credentials.username, - '--dom', credentials.domain, - '--pass', credentials.password, - '--width', width.toString(), - '--height', height.toString(), - '--ws_port', ws_port - ]; - - // Use process.resourcesPath in production, fallback to __dirname in development - const baseDir = app.isPackaged ? process.resourcesPath : __dirname; - const mstscPath = path.join( - baseDir, - app.isPackaged ? 'app.asar.unpacked' : '', - 'electros', 'remotes', 'rdp', 'mstsc-rs' - ); - - // Set detached option for proper process group handling on Unix systems - const spawnOptions = { - detached: platform === 'mac' || platform === 'linux', - env: process.env - }; - - const mstscProcess = spawn(mstscPath, args, spawnOptions); - - // Store process reference and port - event.sender.mstscProcess = mstscProcess; - event.sender.mstscPort = ws_port; - - mstscProcess.stdout.on('data', (data) => { - console.log(`stdout: ${data}`); - }); - - mstscProcess.stderr.on('data', (data) => { - console.error(`stderr: ${data}`); - }); - - mstscProcess.on('close', (code) => { - console.log(`mstsc-rs process exited with code ${code}`); - // Release the port when the process closes - if (event.sender.mstscPort) { - releasePort(parseInt(event.sender.mstscPort)); - event.sender.mstscPort = null; - } - // Check if the window still exists before sending the message - if (!event.sender.isDestroyed()) { - event.sender.send('rdp-process-closed'); - } - }); - - return { success: true, ws_port: ws_port }; - } catch (error) { - console.error('Error launching RDP process:', error); - if (ws_port) { - releasePort(parseInt(ws_port)); - } - return { success: false, error: error.message }; - } + return nativeTheme?.prefersReducedTransparency ?? false; }); -// Keep only one IPC handler for cleanup-rdp-process -ipcMain.handle('cleanup-rdp-process', (event, port) => { - cleanupRDPProcess(event.sender, port); +ipcMain.handle('open-browser', async (event, {url}) => { + await shell.openExternal(url); }); -ipcMain.handle('open-browser', async (event, { - url -}) => { - await shell.openExternal(url); -}) ipcMain.handle('open-ssh', async (event, connectionDetails) => { const sshWindow = new BrowserWindow({ width: 1024, height: 768, - ...commonWindowOptions, + ...WindowOptions.Common, webPreferences: { nodeIntegration: true, contextIsolation: true, preload: path.join(__dirname, 'preload.js'), webSecurity: false, - zoomFactor: 0.8, + zoomFactor: 1, backgroundThrottling: false, enableRemoteModule: false, experimentalFeatures: false } }); - var ssh_port = undefined; + let ssh_port = undefined; try { - // Get an available port for the SSH server - ssh_port = getAvailablePort().toString(); + ssh_port = PortHandler.GetAvailablePort().toString(); - console.log("connectionDetails: ", connectionDetails) const baseDir = app.isPackaged ? process.resourcesPath : __dirname; const sshPath = path.join( baseDir, app.isPackaged ? 'app.asar.unpacked' : '', - 'electros', 'remotes', 'ssh', 'ssh.js' + 'electros', 'remotes', 'ssh', 'ssh.cjs' ); // Start the SSH server process const sshServer = require(sshPath); await sshServer.runSSHServer(ssh_port, baseDir); - // Store port reference event.sender.ssh_port = ssh_port; - // Inject custom titlebar after the page loads sshWindow.webContents.on('did-finish-load', () => { - const sshTitlebarJS = titlebarCustomJS.replace( + const sshTitlebarJS = PreloadedContent.Js.Titlebar.replace( 'titleElement.textContent = document.title;', `titleElement.textContent = "SSH connection to ${connectionDetails.vmName}";` ); sshWindow.webContents.executeJavaScript(sshTitlebarJS); }); - // Set up window-specific shortcuts - setupWindowShortcuts(sshWindow); - // Add connection cleanup on window close sshWindow.on('close', async (event) => { try { @@ -1031,7 +378,7 @@ ipcMain.handle('open-ssh', async (event, connectionDetails) => { } // Release the port - releasePort(parseInt(ssh_port)); + PortHandler.ReleasePort(parseInt(ssh_port)); // Wait a moment for cleanup await new Promise(resolve => setTimeout(resolve, 100)); @@ -1051,11 +398,10 @@ ipcMain.handle('open-ssh', async (event, connectionDetails) => { await sshWindow.loadURL(`http://localhost:${ssh_port}/?host=${encodeURIComponent(connectionDetails.ip)}&username=${encodeURIComponent(connectionDetails.username)}&password=${encodeURIComponent(connectionDetails.password)}`); return sshWindow.id; - } catch (error) { console.error('Error setting up SSH window:', error); if (ssh_port) { - releasePort(parseInt(ssh_port)); + PortHandler.ReleasePort(parseInt(ssh_port)); } throw error; } diff --git a/electros-electron/package.json b/electros-electron/package.json index 510e2b4..a5dc637 100644 --- a/electros-electron/package.json +++ b/electros-electron/package.json @@ -1,12 +1,12 @@ { - "name": "Electros", - "version": "3.1.4", + "name": "electros", + "version": "3.1.5", "description": "Electros Desktop Application", "main": "main.js", "homepage": "https://elemento.cloud", "repository": { "type": "git", - "url": "https://github.com/Elemento-Modular-Cloud/ElectrosGUI.git" + "url": "https://github.com/Elemento-Modular-Cloud/Electros.git" }, "author": { "name": "Elemento Cloud Srl", @@ -42,7 +42,7 @@ "!**/{.env,.env.*,.venv,venv,*.venv}", "!**/node_modules/*/{CONTRIBUTING.md,HISTORY.md,History.md,AUTHORS,CONTRIBUTORS,CHANGES,CHANGELOG.md}", "!.github", - "!docs", + "!**/docs/*", "!**/node_modules/*/{benchmark,coverage,example,examples}/**/*", "!**/node_modules/**/*.{ts,map,md,txt,log,yml,yaml}" ], @@ -60,7 +60,7 @@ ] } ], - "requestedExecutionLevel": "requireAdministrator" + "requestedExecutionLevel": "asInvoker" }, "mac": { "icon": "electros.iconset/electros.icns", diff --git a/electros-electron/remotes/Ssh.js b/electros-electron/remotes/Ssh.js new file mode 100644 index 0000000..038af38 --- /dev/null +++ b/electros-electron/remotes/Ssh.js @@ -0,0 +1,134 @@ +import {app, BrowserWindow, ipcMain} from "electron"; +import path from "path"; +import {WindowOptions} from "../common/WindowOptions.js"; +import {PortHandler} from "../common/PortHandler.js"; + + +class SshWindow { + /** @type {Array} */ + _windows = []; + + constructor(PreloadedRes, platform, __dirname) { + this._preloaded = PreloadedRes; + this._platform = platform; + this.__dirname = __dirname; + } + + _constructWindow() { + const sshWindow = new BrowserWindow({ + width: 1024, + height: 768, + ...WindowOptions.Common, + webPreferences: { + nodeIntegration: true, + contextIsolation: true, + preload: path.join(this.__dirname, 'preload.js'), + webSecurity: false, + zoomFactor: 1, + backgroundThrottling: false, + enableRemoteModule: false, + experimentalFeatures: false + } + }); + + sshWindow.webContents.setVisualZoomLevelLimits(1, 1); + } + + _setupSsh() { + const port = PortHandler.GetAvailablePort(); + + try { + const baseDir = app.isPackaged ? process.resourcesPath : this.__dirname; + const sshPath = path.join( + baseDir, app.isPackaged ? 'app.asar.unpacked' : '', + 'electros', 'remotes', 'ssh', 'ssh.cjs' + ); + + + } + } +} + + + + +ipcMain.handle('open-ssh', async (event, connectionDetails) => { + const sshWindow = new BrowserWindow({ + width: 1024, + height: 768, + ...WindowOptions.Common, + webPreferences: { + nodeIntegration: true, + contextIsolation: true, + preload: path.join(__dirname, 'preload.js'), + webSecurity: false, + zoomFactor: 1, + backgroundThrottling: false, + enableRemoteModule: false, + experimentalFeatures: false + } + }); + + sshWindow.webContents.setVisualZoomLevelLimits(1, 1); + + let ssh_port = undefined; + + try { + + + // Start the SSH server process + const sshServer = require(sshPath); + await sshServer.runSSHServer(ssh_port, baseDir); + + event.sender.ssh_port = ssh_port; + + sshWindow.webContents.on('did-finish-load', () => { + const sshTitlebarJS = titlebarCustomJS.replace( + 'titleElement.textContent = document.title;', + `titleElement.textContent = "SSH connection to ${connectionDetails.vmName}";` + ); + sshWindow.webContents.executeJavaScript(sshTitlebarJS); + }); + + // Set up window-specific shortcuts + setupWindowShortcuts(sshWindow); + + // Add connection cleanup on window close + sshWindow.on('close', async (event) => { + try { + event.preventDefault(); + + if (!sshWindow.isDestroyed()) { + sshWindow.webContents.send('window-close'); + } + + // Release the port + releasePort(parseInt(ssh_port)); + + // Wait a moment for cleanup + await new Promise(resolve => setTimeout(resolve, 100)); + + if (!sshWindow.isDestroyed()) { + sshWindow.destroy(); + } + } catch (error) { + console.error('Error during SSH window cleanup:', error); + if (!sshWindow.isDestroyed()) { + sshWindow.destroy(); + } + } + }); + + // Load the SSH client page with connection details and port + await sshWindow.loadURL(`http://localhost:${ssh_port}/?host=${encodeURIComponent(connectionDetails.ip)}&username=${encodeURIComponent(connectionDetails.username)}&password=${encodeURIComponent(connectionDetails.password)}`); + + return sshWindow.id; + } catch (error) { + console.error('Error setting up SSH window:', error); + if (ssh_port) { + releasePort(parseInt(ssh_port)); + } + throw error; + } +}); + diff --git a/electros-electron/start-venv.sh b/electros-electron/start-venv.sh new file mode 100755 index 0000000..fef3819 --- /dev/null +++ b/electros-electron/start-venv.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Script to start electros-electron app with dependencies + +set -e # Exit on error + +# Get the directory where this script is located (handles symlinks) +SCRIPT_PATH="${BASH_SOURCE[0]}" +while [ -L "$SCRIPT_PATH" ]; do + SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" && pwd)" + SCRIPT_PATH="$(readlink "$SCRIPT_PATH")" + [[ $SCRIPT_PATH != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH" +done +SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" && pwd)" +cd "$SCRIPT_DIR" + +echo "==========================================" +echo "ElectrOS Electron App - Setup & Start" +echo "==========================================" + +# Check if Node.js is available +if ! command -v node &> /dev/null; then + echo "Error: Node.js is not installed or not in PATH" + echo "Please install Node.js from https://nodejs.org/" + exit 1 +fi + +# Check if npm is available +if ! command -v npm &> /dev/null; then + echo "Error: npm is not installed or not in PATH" + echo "Please install npm (usually comes with Node.js)" + exit 1 +fi + +echo "Node.js version: $(node --version)" +echo "npm version: $(npm --version)" +echo "" + +# Check if package.json exists +if [ ! -f "package.json" ]; then + echo "Error: package.json not found" + exit 1 +fi + +# Install dependencies if node_modules doesn't exist or package.json is newer +if [ ! -d "node_modules" ] || [ "package.json" -nt "node_modules" ]; then + echo "Installing npm dependencies..." + npm install + echo "Dependencies installed" +else + echo "Dependencies already installed" +fi + +echo "" +echo "==========================================" +echo "Starting ElectrOS Electron App..." +echo "==========================================" +echo "" + +# Start the electron app +npm run start diff --git a/electros-electron/terminal/terminal.html b/electros-electron/terminal/terminal.html index eedc247..cc6d71d 100644 --- a/electros-electron/terminal/terminal.html +++ b/electros-electron/terminal/terminal.html @@ -1,5 +1,5 @@ - + @@ -15,12 +15,14 @@ font-size: 12px; /* Smaller font size */ padding-top: 43px; /* Add top padding */ padding-left: 13px; + font-variant-ligatures: none; } .electros-titlebar-title { color: #ffa600 !important; } + Electros Daemons
@@ -28,73 +30,82 @@