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 @@