Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 27 additions & 19 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,33 +228,21 @@ jobs:
echo 'EOF'
} >> "$GITHUB_OUTPUT"

- name: Merge image tags for build
id: all_tags
env:
GH_TAGS: ${{ steps.tags.outputs.tags }}
DH_TAGS: ${{ steps.dh_tags.outputs.tags }}
run: |
set -euo pipefail
{
echo 'tags<<EOF'
printf '%s\n' "${GH_TAGS}"
if [ -n "${DH_TAGS:-}" ]; then
printf '%s\n' "${DH_TAGS}"
fi
echo 'EOF'
} >> "$GITHUB_OUTPUT"

- name: Build and push
# GHCR accepts OCI zstd layers; docker.io regular repos still
# expect gzip, so the Hub export is a second cache-hit build.
# build-push-action has no compression inputs; they go through the
# image exporter options in `outputs`.
- name: Build and push (GHCR, zstd layers)
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64,linux/arm64
push: true
sbom: true
provenance: mode=max
tags: ${{ steps.all_tags.outputs.tags }}
outputs: type=image,push=true,compression=zstd,force-compression=true,oci-mediatypes=true
tags: ${{ steps.tags.outputs.tags }}
cache-from: type=gha,scope=${{ matrix.variant }}
cache-to: type=gha,mode=max,scope=${{ matrix.variant }}
build-args: |
Expand All @@ -265,6 +253,26 @@ jobs:
OCI_DESCRIPTION=${{ matrix.oci_description }}
OCI_LICENSES=${{ matrix.oci_licenses }}

- name: Build and push (Docker Hub, gzip layers)
if: ${{ steps.dockerhub.outputs.configured == 'true' }}
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64,linux/arm64
push: true
sbom: true
provenance: mode=max
tags: ${{ steps.dh_tags.outputs.tags }}
cache-from: type=gha,scope=${{ matrix.variant }}
build-args: |
OCI_REVISION=${{ github.sha }}
OCI_VERSION=${{ steps.oci.outputs.version }}
OCI_CREATED=${{ steps.oci.outputs.created }}
VARIANT=${{ matrix.build_variant }}
OCI_DESCRIPTION=${{ matrix.oci_description }}
OCI_LICENSES=${{ matrix.oci_licenses }}

- name: Verify image has no package bloat
env:
IMAGE_NAME: ${{ steps.image.outputs.name }}
Expand Down
2 changes: 1 addition & 1 deletion electron/assets/css/electron-shell.css

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion electron/mainHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@

const path = require("node:path");

const IGNORED_CLI_ARGUMENTS = new Set(["--no-sandbox", "--ozone-platform-hint=auto"]);
const IGNORED_CLI_ARGUMENTS = new Set([
"--no-sandbox",
"--ozone-platform-hint=auto",
"--disable-gpu",
"--disable-gpu-sandbox",
"--disable-gpu-compositing",
"--disable-software-rasterizer",
"--enable-logging",
"--enable-logging=stderr",
]);

/**
* Arguments after argv[0], excluding known Chromium/Electron noise flags.
Expand Down
4 changes: 3 additions & 1 deletion electron/preload.bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ function isTrustedShellFileUrl(url) {
const candidate = normalized.startsWith("/") && !base.startsWith("/") ? normalized.slice(1) : normalized;
return candidate === `${base}/loading.html` || candidate === `${base}/crash.html`;
}
return true;
// No module dir to pin the page to: refuse rather than trusting any
// file: URL that happens to end in loading.html or crash.html.
return false;
}

/**
Expand Down
84 changes: 76 additions & 8 deletions meshchatx/src/backend/nomadnet_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import RNS

from meshchatx.src.backend import reticulum_pathfinding
from meshchatx.src.backend.async_utils import AsyncUtils
from meshchatx.src.backend.path_utils import (
link_establishment_window,
path_response_window,
Expand All @@ -31,6 +32,10 @@
# Wait granularity while polling for path / link (seconds). Smaller = faster reaction, slightly more wakeups.
_POLL_INTERVAL_S = 0.02

# Floor for the request watchdog. RNS scales the window by link RTT, but a
# zero-ish RTT on loopback/test links would otherwise cut off slow nodes.
_MIN_REQUEST_WATCHDOG_S = 30.0

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -465,14 +470,77 @@ def link_established(self, link):

self._maybe_identify(link)

self.request_receipt = link.request(
self.path,
data=self.data,
response_callback=self.on_response,
failed_callback=self.on_failed,
progress_callback=self.on_progress,
timeout=self.timeout,
)
try:
receipt = link.request(
self.path,
data=self.data,
response_callback=self.on_response,
failed_callback=self.on_failed,
progress_callback=self.on_progress,
timeout=self.timeout,
)
except Exception as exc:
_uncache_link_if_matches(self.destination_hash, link)
self._deliver_failure(str(exc) or "Could not send request to node.")
self._maybe_teardown_abandoned_link()
return
if not receipt:
_uncache_link_if_matches(self.destination_hash, link)
self._deliver_failure("Could not send request to node.")
self._maybe_teardown_abandoned_link()
return
self.request_receipt = receipt
AsyncUtils.run_async(self._watch_request_receipt(link))

async def _watch_request_receipt(self, link) -> None:
"""Bound the wait for a link request to conclude.

RNS invokes failed_callback only once a receipt reaches DELIVERED.
A packet request stuck in SENT, an unanswered request, or a link
torn down mid-request, never times out upstream, so enforce the
window here.
"""
receipt = self.request_receipt
if receipt is None:
return
try:
if self.timeout is not None:
window = float(self.timeout)
else:
rtt = getattr(link, "rtt", None) or 0.0
factor = getattr(
link,
"traffic_timeout_factor",
RNS.Link.TRAFFIC_TIMEOUT_FACTOR,
)
window = rtt * factor + RNS.Resource.RESPONSE_MAX_GRACE_TIME * 1.125
window = max(window, _MIN_REQUEST_WATCHDOG_S)
except Exception:
window = _MIN_REQUEST_WATCHDOG_S

deadline = time.monotonic() + window
last_progress = getattr(receipt, "progress", None) or 0.0
reason = None
while not self._outcome_delivered and not self.is_cancelled:
if link.status is RNS.Link.CLOSED:
reason = "Link to node closed before the request completed."
break
if time.monotonic() > deadline:
reason = "Request timed out. The node did not respond."
break
await asyncio.sleep(_POLL_INTERVAL_S)
progress = getattr(receipt, "progress", None) or 0.0
if progress != last_progress:
last_progress = progress
deadline = time.monotonic() + window

if reason is None or self.is_cancelled or self._outcome_delivered:
return
# A request that never concluded means the link is dead or
# half-open: evict it so the next attempt does not reuse it.
_uncache_link_if_matches(self.destination_hash, link)
self._deliver_failure(reason)
self._maybe_teardown_abandoned_link()

def on_response(self, request_receipt: RNS.RequestReceipt):
if self.is_cancelled or self._outcome_delivered:
Expand Down
98 changes: 51 additions & 47 deletions meshchatx/src/frontend/components/TutorialModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,7 @@
</div>

<div class="flex-1 overflow-y-auto px-6 md:px-12 py-6 md:py-10">
<div class="w-full h-full flex flex-col justify-between">
<div class="w-full h-full flex flex-col">
<transition name="fade-slide" mode="out-in">
<!-- Step 1: Welcome -->
<div
Expand Down Expand Up @@ -2229,54 +2229,58 @@
</RouterLink>
</div>
</transition>
</div>
</div>

<!-- Navigation Buttons (Page Mode): pinned below the scroll area -->
<div
class="shrink-0 border-t border-sem-border bg-sem-surface-muted px-6 py-4 pb-[max(1rem,env(safe-area-inset-bottom))] dark:border-zinc-900 dark:bg-zinc-950/50 md:px-12"
>
<div class="flex justify-between items-center">
<button
v-if="currentStep > 1 && currentStep < totalSteps"
type="button"
class="tutorial-action-btn tutorial-action-btn-secondary"
:disabled="tutorialNavBusy"
@click="previousStep"
>
{{ $t("tutorial.back") }}
</button>
<div v-else></div>

<!-- Navigation Buttons (Page Mode) -->
<div class="flex justify-between items-center mt-12 border-t dark:border-zinc-900 pt-8">
<div class="flex gap-4">
<button
v-if="currentStep > 1 && currentStep < totalSteps"
v-if="currentStep < totalSteps"
type="button"
class="tutorial-action-btn tutorial-action-btn-secondary"
:disabled="tutorialNavBusy"
@click="previousStep"
@click="skipTutorial"
>
{{ $t("tutorial.back") }}
{{ $t("tutorial.skip_setup") }}
</button>
<div v-else></div>

<div class="flex gap-4">
<button
v-if="currentStep < totalSteps"
type="button"
class="tutorial-action-btn tutorial-action-btn-secondary"
:disabled="tutorialNavBusy"
@click="skipTutorial"
>
{{ $t("tutorial.skip_setup") }}
</button>

<button
v-if="showFooterContinue"
type="button"
class="tutorial-action-btn tutorial-action-btn-primary"
:disabled="
tutorialNavBusy ||
(currentStep === 2 && identityMode === 'import' && !hasIdentityImportInput)
"
@click="handlePrimaryAction"
>
{{ $t("tutorial.continue") }}
</button>
<button
v-if="showFooterContinue"
type="button"
class="tutorial-action-btn tutorial-action-btn-primary"
:disabled="
tutorialNavBusy ||
(currentStep === 2 && identityMode === 'import' && !hasIdentityImportInput)
"
@click="handlePrimaryAction"
>
{{ $t("tutorial.continue") }}
</button>

<button
v-else-if="currentStep === totalSteps"
type="button"
class="tutorial-action-btn tutorial-action-btn-success"
:disabled="finishingTutorial || tutorialNavBusy"
@click="finishTutorial"
>
{{ $t("tutorial.finish_setup") }}
</button>
</div>
<button
v-else-if="currentStep === totalSteps"
type="button"
class="tutorial-action-btn tutorial-action-btn-success"
:disabled="finishingTutorial || tutorialNavBusy"
@click="finishTutorial"
>
{{ $t("tutorial.finish_setup") }}
</button>
</div>
</div>
</div>
Expand Down Expand Up @@ -2352,7 +2356,7 @@ export default {
discoveryInterval: null,
markingSeen: false,
windowWidth: typeof window !== "undefined" ? window.innerWidth : 1024,
defaultBootstrapOnly: true,
defaultBootstrapOnly: false,
bootstrapListSearch: "",
bootstrapDiscoveredSectionOpen: true,
bootstrapCommunitySectionOpen: true,
Expand Down Expand Up @@ -2693,10 +2697,10 @@ export default {
const payload = {
discover_interfaces: true,
autoconnect_discovered_interfaces: 3,
default_bootstrap_only: true,
default_bootstrap_only: false,
};
await window.api.patch(apiPath("/reticulum/discovery"), payload);
this.defaultBootstrapOnly = true;
this.defaultBootstrapOnly = false;

ToastUtils.success(this.$t("tutorial.mode_recommended_added"));
this.connectionMode = "recommended";
Expand Down Expand Up @@ -2730,10 +2734,10 @@ export default {
const payload = {
discover_interfaces: true,
autoconnect_discovered_interfaces: 3,
default_bootstrap_only: true,
default_bootstrap_only: false,
};
await window.api.patch(apiPath("/reticulum/discovery"), payload);
this.defaultBootstrapOnly = true;
this.defaultBootstrapOnly = false;
ToastUtils.success(this.$t("tutorial.discovery_enabled"));
this.connectionMode = "discovery";
this.currentStep = 4;
Expand Down Expand Up @@ -3015,10 +3019,10 @@ export default {
try {
const response = await window.api.get(apiPath("/reticulum/discovery"));
const d = response.data?.discovery ?? {};
this.defaultBootstrapOnly = this.parseDiscoveryBool(d.default_bootstrap_only, true);
this.defaultBootstrapOnly = this.parseDiscoveryBool(d.default_bootstrap_only);
} catch (e) {
console.error(e);
this.defaultBootstrapOnly = true;
this.defaultBootstrapOnly = false;
}
},
async persistDefaultBootstrapOnly(value) {
Expand Down
Loading
Loading