Services: real download lifecycle + cookie persistence (R4 + R2)

Downloads were captured at the MIME-handoff only; nsExternalAppHandler then
failed CreateTransfer and cancelled every one. Registering an
@mozilla.org/transfer;1 implementation makes saveToDisk actually run and gives
the daemon the nsICancelable that cancelDownload needs.

  msgDownloadStart/Progress/Finished + cancelDownload -> msgDownloadError
  DOWNLOAD-LIFECYCLE PASS (build-download2-test.sh)
  cookies.sqlite present + survives an engine restart
  COOKIE-PERSISTENCE PASS (build-cookie-test.sh)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDvWTswwcSHQHVA1gD795L
This commit is contained in:
NotAlexNoyleandClaude Opus 5 committed 2026-07-31 16:46:12 -04:00
1 parent 76401efecc
commit 8953eab6de
12 files changed
+975 -16

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
#!/bin/bash
# Copyright 2026 the Jihad Browser project.
# Licensed under the Apache License, Version 2.0 (see ../../LICENSE).
#
# Cookie PERSISTENCE test (cavekit-browser-services R2): a local HTTP server
# hands out a PERSISTENT cookie (future Max-Age); the engine is started, shut
# down cleanly, and started AGAIN against the same profile dir. The cookie must
# come back on the second run and cookies.sqlite must exist in the profile.
# This is the desktop reproduction of the device gap logged 2026-07-19 s4.
set -uo pipefail
DIST=/out/obj-jihad-goanna/dist
SRC=/jihad/render/goanna
OUT=/out/cookie_test
PROF=/out/cookieprofile
PORT=${JIHAD_COOKIE_PORT:-8138}
[ -e "$DIST/bin/libxul.so" ] || { echo "ERROR: build the engine first"; exit 1; }
CXX=${CXX:-g++}
GTK_CFLAGS=$(pkg-config --cflags gtk+-2.0 glib-2.0); GTK_LIBS=$(pkg-config --libs gtk+-2.0 glib-2.0)
CXXFLAGS="-std=gnu++17 -fPIC -fno-rtti -fno-exceptions -O2 -g0"
INCS="-include $DIST/include/mozilla-config.h -I$DIST/include -I$DIST/include/nspr $GTK_CFLAGS"
$CXX $CXXFLAGS $INCS -c "$SRC/EngineHost.cpp" -o /out/EngineHost.o || exit 10
$CXX $CXXFLAGS $INCS -c "$SRC/DialogService.cpp" -o /out/DialogService.o || exit 10
$CXX $CXXFLAGS $INCS -c "$SRC/DownloadService.cpp" -o /out/DownloadService.o || exit 10
$CXX $CXXFLAGS $INCS -c "$SRC/GoannaRenderPage.cpp" -o /out/GoannaRenderPage.o || exit 11
$CXX $CXXFLAGS $INCS -c "$SRC/test/cookie_test.cpp" -o /out/cookie_test.o || exit 12
$CXX /out/cookie_test.o /out/GoannaRenderPage.o /out/EngineHost.o /out/DialogService.o /out/DownloadService.o \
"$DIST/sdk/lib/libxpcomglue_s.a" -L"$DIST/bin" -lxul "$DIST/sdk/lib/libmozglue.a" \
-lnspr4 -lplc4 -lplds4 $GTK_LIBS -Wl,-rpath,"$DIST/bin" -ldl -lpthread -o "$OUT" || exit 13
echo "== built $OUT =="
cat > /out/cookiesrv.py <<'PYEOF'
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
PORT = int(sys.argv[1])
class H(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, *a): pass
def _html(self, body, extra=None):
data = body.encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
if extra:
for k, v in extra:
self.send_header(k, v)
self.end_headers()
self.wfile.write(data)
def do_GET(self):
if self.path.startswith("/set"):
# PERSISTENT cookie: a session cookie is not meant to survive a restart,
# so only a Max-Age/Expires cookie proves persistence.
self._html("<title>SET</title>set",
[("Set-Cookie", "jihadpersist=1; Max-Age=3600; Path=/")])
elif self.path.startswith("/echo"):
c = self.headers.get("Cookie", "")
self._html("<title>COOKIE[%s]</title>echo" % c)
else:
self._html("<title>404</title>no")
ThreadingHTTPServer.daemon_threads = True
ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever()
PYEOF
python3 /out/cookiesrv.py "$PORT" &
httpd=$!
sleep 1
if ! grep -q 'jihad-embed' "$DIST/bin/goanna.js" 2>/dev/null; then
echo 'pref("layers.offmainthreadcomposition.force-disabled", true);' >> "$DIST/bin/goanna.js"
fi
rm -rf "$PROF"
export JIHAD_DISABLE_OMTC=1
export JIHAD_PROFILE_DIR="$PROF"
export LD_LIBRARY_PATH="$DIST/bin"
BASE="http://127.0.0.1:$PORT"
echo "== run 1: set the cookie =="
xvfb-run -a -s "-screen 0 1024x768x24" "$OUT" "$DIST/bin" set "$BASE"
rc1=$?
echo "== profile after run 1 =="
ls -l "$PROF" || true
echo "== run 2: RESTART, cookie must still be sent =="
xvfb-run -a -s "-screen 0 1024x768x24" "$OUT" "$DIST/bin" check "$BASE"
rc2=$?
kill "$httpd" 2>/dev/null
echo "== run1=$rc1 run2=$rc2 =="
if [ "$rc1" = 0 ] && [ "$rc2" = 0 ]; then
echo "COOKIE-PERSISTENCE PASS"; exit 0
else
echo "COOKIE-PERSISTENCE FAIL"; exit 1
fi
+128
View File
@@ -0,0 +1,128 @@
#!/bin/bash
# Copyright 2026 the Jihad Browser project.
# Licensed under the Apache License, Version 2.0 (see ../../LICENSE).
#
# Download LIFECYCLE test (cavekit-browser-services R4): builds the real
# jihad-browserserver daemon + a YAP adapter stand-in and drives two downloads
# from a local HTTP server, asserting the frozen daemon->adapter messages:
# msgDownloadStart (0x2010) -> msgDownloadProgress (0x2011) ->
# msgDownloadFinished (0x2013, mime + temp path), and
# cancelDownload (0x1015) aborting an in-progress download (msgDownloadError,
# never msgDownloadFinished).
# Sibling of build-download-test.sh, which covers only the MIME handoff capture.
set -uo pipefail
DIST=/out/obj-jihad-goanna/dist
R=/jihad/render
BS=/jihad/render/browserserver
[ -e "$DIST/bin/libxul.so" ] || { echo "ERROR: build the engine first"; exit 1; }
CXX=${CXX:-g++}
CXXFLAGS="-std=gnu++17 -fPIC -fno-rtti -fno-exceptions -O2 -g0"
GLIB=$(pkg-config --cflags glib-2.0 gthread-2.0); GLIBL=$(pkg-config --libs glib-2.0 gthread-2.0)
GTKC=$(pkg-config --cflags gtk+-2.0); GTKL=$(pkg-config --libs gtk+-2.0)
ENGINC="-include $DIST/include/mozilla-config.h -I$DIST/include -I$DIST/include/nspr"
YAPINC="-I$BS/Yap -I$BS/Src"
XULLINK="$DIST/sdk/lib/libxpcomglue_s.a -L$DIST/bin -lxul $DIST/sdk/lib/libmozglue.a -lnspr4 -lplc4 -lplds4"
echo "== compiling libYap (incl YapClient) + BrowserServerBase =="
for f in YapPacket YapProxy YapServer YapClient; do
$CXX $CXXFLAGS $YAPINC $GLIB -c "$BS/Yap/$f.cpp" -o "/out/$f.o" || exit 10
done
$CXX $CXXFLAGS $YAPINC $GLIB -c "$BS/Src/BrowserServerBase.cpp" -o /out/BrowserServerBase.o || exit 11
echo "== compiling Goanna backend + daemon =="
$CXX $CXXFLAGS $ENGINC $GTKC -c "$R/goanna/EngineHost.cpp" -o /out/EngineHost.o || exit 12
$CXX $CXXFLAGS $ENGINC $GTKC -c "$R/goanna/DialogService.cpp" -o /out/DialogService.o || exit 12
$CXX $CXXFLAGS $ENGINC $GTKC -c "$R/goanna/DownloadService.cpp" -o /out/DownloadService.o || exit 12
$CXX $CXXFLAGS $ENGINC $GTKC -c "$R/goanna/GoannaRenderPage.cpp" -o /out/GoannaRenderPage.o || exit 13
$CXX $CXXFLAGS $ENGINC $GTKC -c "$R/goanna/BrowserPageGoanna.cpp" -o /out/BrowserPageGoanna.o || exit 14
$CXX $CXXFLAGS $YAPINC $GLIB -c "$BS/JihadBrowserServer.cpp" -o /out/JihadBrowserServer.o || exit 15
$CXX $CXXFLAGS $YAPINC $ENGINC $GTKC $GLIB -c "$BS/Main.cpp" -o /out/bs_main.o || exit 16
$CXX /out/bs_main.o /out/JihadBrowserServer.o /out/BrowserServerBase.o \
/out/YapPacket.o /out/YapProxy.o /out/YapServer.o \
/out/BrowserPageGoanna.o /out/GoannaRenderPage.o /out/EngineHost.o /out/DialogService.o /out/DownloadService.o \
$XULLINK $GTKL $GLIBL -Wl,-rpath,"$DIST/bin" -ldl -lpthread -o /out/jihad-browserserver || exit 17
echo "== compiling download adapter stand-in =="
$CXX $CXXFLAGS $YAPINC $GLIB -c "$BS/test/download_client.cpp" -o /out/download_client.o || exit 18
$CXX /out/download_client.o /out/YapClient.o /out/YapPacket.o \
$GLIBL -ldl -lpthread -o /out/jihad-download-client || exit 19
# --- local HTTP server: one fast attachment + one deliberately slow one -------
PORT=${JIHAD_DL_PORT:-8137}
SIZE=${JIHAD_DL_SIZE:-524288}
cat > /out/dlserver.py <<'PYEOF'
import sys, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
PORT = int(sys.argv[1]); SIZE = int(sys.argv[2])
CHUNK = 32768
SLOW_SIZE = 32 * 1024 * 1024 # never actually completes within the test
class H(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, *a): pass
def _attach(self, total, name):
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Disposition", 'attachment; filename="%s"' % name)
self.send_header("Content-Length", str(total))
self.end_headers()
def do_GET(self):
try:
if self.path.startswith("/big"):
self._attach(SIZE, "jihad-payload.bin")
sent = 0
while sent < SIZE:
n = min(CHUNK, SIZE - sent)
self.wfile.write(b"J" * n); self.wfile.flush()
sent += n
time.sleep(0.03) # several progress ticks
elif self.path.startswith("/slow"):
self._attach(SLOW_SIZE, "jihad-slow.bin")
sent = 0
while sent < SLOW_SIZE:
self.wfile.write(b"S" * CHUNK); self.wfile.flush()
sent += CHUNK
time.sleep(0.25) # ~4 min: cancelled long before this ends
else:
self.send_response(404); self.send_header("Content-Length", "0"); self.end_headers()
except (BrokenPipeError, ConnectionResetError):
pass # the cancel closed the socket: expected
ThreadingHTTPServer.daemon_threads = True
ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever()
PYEOF
rm -rf /out/dltmp && mkdir -p /out/dltmp
python3 /out/dlserver.py "$PORT" "$SIZE" &
httpd=$!
sleep 1
echo "== download lifecycle round-trip under Xvfb =="
export DIST PORT SIZE
xvfb-run -a -s "-screen 0 1024x768x24" bash -c '
export JIHAD_DISABLE_OMTC=1
export JIHAD_OFFSCREEN=1
export JIHAD_DOWNLOAD_DIR=/out/dltmp
export JIHAD_PROFILE_DIR=/out/dlprofile
export LD_LIBRARY_PATH="$DIST/bin"
/out/jihad-browserserver "$DIST/bin" &
dpid=$!
sleep 8 # let the engine come up + open the socket
JIHAD_DL_URL="http://127.0.0.1:$PORT/big" \
JIHAD_DL_SLOW="http://127.0.0.1:$PORT/slow" \
JIHAD_DL_SIZE="$SIZE" \
timeout 70 /out/jihad-download-client
rc=$?
kill "$dpid" 2>/dev/null; wait 2>/dev/null
exit $rc
'
rc=$?
kill "$httpd" 2>/dev/null
echo "== files left in /out/dltmp: $(ls -l /out/dltmp | tail -n +2 | wc -l) =="
ls -l /out/dltmp || true
echo "== download2 exit: $rc =="
exit $rc
+2
View File
@@ -88,6 +88,8 @@ asserts real behavior under Xvfb (all PASS):
| `build-download-test.sh` | download / MIME handoff (DOWNLOAD PASS) |
| `build-resize-test.sh` / `build-scroll-test.sh` / `build-zoom-test.sh` | surface geometry |
| `build-geo-test.sh` | contents-size / meta-viewport / scrolled-to events (GEO PASS) |
| `build-download2-test.sh` | daemon-side download lifecycle over YAP: `msgDownloadStart`/`Progress`/`Finished` (temp path + MIME) and `cancelDownload` aborting an in-progress download (DOWNLOAD-LIFECYCLE PASS) |
| `build-cookie-test.sh` | persistent cookie survives an engine restart + `cookies.sqlite` exists in the profile (COOKIE-PERSISTENCE PASS) |
## R4 — isis Enyo UI against the desktop daemon `[human-review]`
+41 -1
View File
@@ -28,6 +28,10 @@ void ProxySink::msgUrlRedirected(const char* u, const char* ud) { mSrv->msgUrlRe
void ProxySink::msgSSLConfirm(const char* host, int32_t code, const char* certFile) { mSrv->msgDialogSSLConfirm(mProxy, "", host, code, certFile); }
void ProxySink::msgLinkClicked(const char* url) { mSrv->msgLinkClicked(mProxy, url); }
void ProxySink::msgMimeHandoffUrl(const char* mimeType, const char* url) { mSrv->msgMimeHandoffUrl(mProxy, mimeType, url); }
void ProxySink::msgDownloadStart(const char* url) { mSrv->msgDownloadStart(mProxy, url); }
void ProxySink::msgDownloadProgress(const char* url, int32_t soFar, int32_t total) { mSrv->msgDownloadProgress(mProxy, url, soFar, total); }
void ProxySink::msgDownloadFinished(const char* url, const char* mimeType, const char* tmpFilePath) { mSrv->msgDownloadFinished(mProxy, url, mimeType, tmpFilePath); }
void ProxySink::msgDownloadError(const char* url, const char* errorMsg) { mSrv->msgDownloadError(mProxy, url, errorMsg); }
// ---- server ----------------------------------------------------------------
JihadBrowserServer::JihadBrowserServer(const char* name, jihad::EngineHost& host)
@@ -55,6 +59,35 @@ void JihadBrowserServer::OnDownload(const char* url, const char* mimeType,
printf("[jihad-bs] download: no active page to hand off to\n");
}
// The frozen msgDownloadProgress carries int32 byte counts; the engine counts in
// int64. Saturate rather than wrap so a >2 GB transfer still reports monotonic
// progress (and keep -1 = "unknown total" intact).
static int32_t clampToI32(int64_t v) {
if (v < 0) return -1;
return v > 0x7fffffffLL ? 0x7fffffff : (int32_t)v;
}
// Lifecycle of the download the ENGINE performs (DownloadService drives the
// helper-app save). Like OnDownload these have no page handle, so they follow
// the same rule: route to the ACTIVE card.
void JihadBrowserServer::OnDownloadStart(const char* url) {
if (auto* p = pageFor(mLastProxy)) p->emitDownloadStart(url ? url : "");
}
void JihadBrowserServer::OnDownloadProgress(const char* url, int64_t bytesSoFar, int64_t totalBytes) {
if (auto* p = pageFor(mLastProxy))
p->emitDownloadProgress(url ? url : "", clampToI32(bytesSoFar), clampToI32(totalBytes));
}
void JihadBrowserServer::OnDownloadFinished(const char* url, const char* mimeType, const char* tmpFilePath) {
printf("[jihad-bs] download finished mime=%s path=%s\n",
mimeType ? mimeType : "", tmpFilePath ? tmpFilePath : "");
if (auto* p = pageFor(mLastProxy))
p->emitDownloadFinished(url ? url : "", mimeType ? mimeType : "", tmpFilePath ? tmpFilePath : "");
}
void JihadBrowserServer::OnDownloadError(const char* url, const char* errorMsg) {
printf("[jihad-bs] download error %s: %s\n", url ? url : "", errorMsg ? errorMsg : "");
if (auto* p = pageFor(mLastProxy)) p->emitDownloadError(url ? url : "", errorMsg ? errorMsg : "");
}
jihad::BrowserPageGoanna* JihadBrowserServer::pageFor(YapProxy* proxy) {
auto it = mPages.find(proxy);
return it == mPages.end() ? nullptr : it->second.page;
@@ -238,9 +271,16 @@ void JihadBrowserServer::asyncCmdExit(YapProxy* proxy)
(void)proxy; // TODO(T-016): route to pageFor(proxy) / GoannaRenderPage per PORT-MAP.md
}
// YAP cancelDownload(url): abort the matching in-flight engine download. The
// download is process-wide (the engine's helper-app service owns it, not a
// page), so this is not routed per-proxy. An empty url cancels all of them,
// matching the adapter's "stop whatever is downloading" intent.
void JihadBrowserServer::asyncCmdCancelDownload(YapProxy* proxy, const char* url)
{
(void)proxy; // TODO(T-016): route to pageFor(proxy) / GoannaRenderPage per PORT-MAP.md
(void)proxy;
bool aborted = jihad::CancelDownload(url);
printf("[jihad-bs] cancelDownload %s -> %s\n", url ? url : "(all)",
aborted ? "aborted" : "no match");
}
void JihadBrowserServer::asyncCmdInterrogateClicks(YapProxy* proxy, bool enable)
+10
View File
@@ -41,6 +41,10 @@ public:
void msgSSLConfirm(const char* host, int32_t code, const char* certFile) override;
void msgLinkClicked(const char* url) override;
void msgMimeHandoffUrl(const char* mimeType, const char* url) override;
void msgDownloadStart(const char* url) override;
void msgDownloadProgress(const char* url, int32_t soFar, int32_t total) override;
void msgDownloadFinished(const char* url, const char* mimeType, const char* tmpFilePath) override;
void msgDownloadError(const char* url, const char* errorMsg) override;
private:
JihadBrowserServer* mSrv;
YapProxy* mProxy;
@@ -56,6 +60,12 @@ public:
// that page's client so the app hands it to com.palm.downloadmanager.
void OnDownload(const char* url, const char* mimeType,
const char* suggestedName, int64_t contentLength) override;
// ... and the lifecycle of the download the engine itself performs, which maps
// 1:1 onto the frozen msgDownloadStart/Progress/Finished/Error contract.
void OnDownloadStart(const char* url) override;
void OnDownloadProgress(const char* url, int64_t bytesSoFar, int64_t totalBytes) override;
void OnDownloadFinished(const char* url, const char* mimeType, const char* tmpFilePath) override;
void OnDownloadError(const char* url, const char* errorMsg) override;
virtual void clientConnected(YapProxy* proxy);
virtual void clientDisconnected(YapProxy* proxy);
@@ -0,0 +1,244 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Jihad Browser — download-lifecycle adapter stand-in (domain G / R4).
*
* A YapClient that drives the real jihad-browserserver over the frozen YAP
* contract and asserts the daemon-side download messages:
*
* openUrl($JIHAD_DL_URL) -> msgDownloadStart (0x2010)
* -> msgDownloadProgress (0x2011) [>= 1]
* -> msgDownloadFinished (0x2013) with mime + temp path
* openUrl($JIHAD_DL_SLOW) -> msgDownloadStart, then
* cancelDownload (0x1015) -> msgDownloadError (0x2012), and NO finished.
*
* The finished temp file is stat()ed to prove the daemon really wrote the bytes
* (both processes run in the same container / filesystem).
*/
#include <YapClient.h>
#include <YapPacket.h>
#include <glib.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
static const char* envOr(const char* k, const char* dflt) {
const char* v = getenv(k);
return (v && *v) ? v : dflt;
}
class DownloadClient : public YapClient {
public:
explicit DownloadClient(const char* name)
: YapClient(name), W(640), H(480), sz(640*480*4),
key1(0x4a494451), key2(0x4a494452) {}
int W, H, sz, key1, key2;
std::string bigUrl, slowUrl;
GMainContext* ctx = nullptr; // the client's own loop context (timers attach here)
// Phase 1 (bigUrl) tallies.
int startBig = 0, progressBig = 0, finishedBig = 0, errorBig = 0;
long finishedSize = -1;
std::string finishedMime, finishedPath;
// Phase 2 (slowUrl) tallies.
int startSlow = 0, progressSlow = 0, finishedSlow = 0, errorSlow = 0;
bool cancelSent = false;
bool phase2 = false;
void serverConnected() override {}
void openUrl(const std::string& url) {
YapPacket* o = packetCommand();
(*o) << (int16_t)0x1004; (*o) << url.c_str();
sendAsyncCommand();
printf("[dlc] -> openUrl %s\n", url.c_str());
}
void cancelDownload(const std::string& url) {
YapPacket* c = packetCommand();
(*c) << (int16_t)0x1015; (*c) << url.c_str();
sendAsyncCommand();
cancelSent = true;
printf("[dlc] -> cancelDownload %s\n", url.c_str());
}
void start() {
shmget(key1, sz, IPC_CREAT | 0600);
shmget(key2, sz, IPC_CREAT | 0600);
YapPacket* c = packetCommand(); // Connect (0x1000)
(*c) << (int16_t)0x1000;
(*c) << (int32_t)W; (*c) << (int32_t)H;
(*c) << (int32_t)key1; (*c) << (int32_t)key2;
(*c) << (int32_t)sz; (*c) << (int32_t)1;
sendAsyncCommand();
openUrl(bigUrl);
}
void beginPhase2() {
if (phase2) return;
phase2 = true;
openUrl(slowUrl);
}
// Cancel the slow download only after it has really been running for a while,
// so the assertion is "aborts an IN-PROGRESS download", not "refuses to start".
void scheduleCancel(int ms) {
if (cancelArmed || !ctx) return;
cancelArmed = true;
GSource* s = g_timeout_source_new(ms);
g_source_set_callback(s, &DownloadClient::cancelCb, this, nullptr);
g_source_attach(s, ctx);
g_source_unref(s);
}
static gboolean cancelCb(gpointer d) {
DownloadClient* c = (DownloadClient*)d;
if (!c->cancelSent) c->cancelDownload(c->slowUrl);
return FALSE;
}
bool cancelArmed = false;
// End the run a beat after the cancel is reported — long enough that a stray
// msgDownloadFinished for the cancelled download would still be caught.
void scheduleQuit(int ms) {
if (quitArmed || !ctx) return;
quitArmed = true;
GSource* s = g_timeout_source_new(ms);
g_source_set_callback(s, &DownloadClient::quitCb2, this, nullptr);
g_source_attach(s, ctx);
g_source_unref(s);
}
static gboolean quitCb2(gpointer d) {
g_main_loop_quit(((DownloadClient*)d)->mainLoop());
return FALSE;
}
bool quitArmed = false;
void serverDisconnected() override { printf("[dlc] serverDisconnected\n"); }
void handleAsyncMessage(YapPacket* msg) override {
int16_t id = 0;
(*msg) >> id;
switch ((uint16_t)id) {
case 0x2000: { // msgPainted — hand the buffer straight back
int32_t k = 0; (*msg) >> k;
YapPacket* r = packetCommand();
(*r) << (int16_t)0x150d; (*r) << (int32_t)k;
sendAsyncCommand();
break; }
case 0x2010: { // msgDownloadStart(url)
char* u = 0; (*msg) >> u;
std::string url = u ? u : ""; if (u) free(u);
printf("[dlc] <- msgDownloadStart(%s)\n", url.c_str());
if (url == slowUrl) { ++startSlow; scheduleCancel(2000); }
else { ++startBig; }
break; }
case 0x2011: { // msgDownloadProgress(url, soFar, total)
char* u = 0; int32_t so = 0, tot = 0;
(*msg) >> u; (*msg) >> so; (*msg) >> tot;
std::string url = u ? u : ""; if (u) free(u);
if (url == slowUrl) {
if (++progressSlow <= 3)
printf("[dlc] <- msgDownloadProgress(slow, %d/%d)\n", so, tot);
} else {
if (++progressBig <= 3)
printf("[dlc] <- msgDownloadProgress(big, %d/%d)\n", so, tot);
}
break; }
case 0x2012: { // msgDownloadError(url, msg)
char* u = 0; char* e = 0; (*msg) >> u; (*msg) >> e;
std::string url = u ? u : "", err = e ? e : "";
if (u) free(u); if (e) free(e);
printf("[dlc] <- msgDownloadError(%s, %s)\n", url.c_str(), err.c_str());
if (url == slowUrl) { ++errorSlow; scheduleQuit(3000); } else ++errorBig;
break; }
case 0x2013: { // msgDownloadFinished(url, mime, tmpPath)
char* u = 0; char* m = 0; char* p = 0;
(*msg) >> u; (*msg) >> m; (*msg) >> p;
std::string url = u ? u : "", mime = m ? m : "", path = p ? p : "";
if (u) free(u); if (m) free(m); if (p) free(p);
printf("[dlc] <- msgDownloadFinished(%s, mime=%s, path=%s)\n",
url.c_str(), mime.c_str(), path.c_str());
if (url == slowUrl) { ++finishedSlow; break; }
++finishedBig;
finishedMime = mime; finishedPath = path;
struct stat st;
finishedSize = (!path.empty() && stat(path.c_str(), &st) == 0) ? (long)st.st_size : -1;
beginPhase2();
break; }
case 0x2015: { // msgMimeHandoffUrl(mime, url) — the app's existing path
char* m = 0; char* u = 0; (*msg) >> m; (*msg) >> u;
printf("[dlc] <- msgMimeHandoffUrl(%s, %s)\n", m ? m : "", u ? u : "");
if (m) free(m); if (u) free(u);
break; }
default: break;
}
}
};
static gboolean quitCb(gpointer data) {
g_main_loop_quit((GMainLoop*)data);
return FALSE;
}
// Phase 2 never completes on its own (the download is cancelled), so a second
// timer closes it out once the cancel has had time to be reported.
static gboolean phase2GuardCb(gpointer data) {
DownloadClient* c = (DownloadClient*)data;
if (!c->phase2) c->beginPhase2(); // phase 1 stalled; still exercise cancel
return FALSE;
}
int main(int argc, char** argv) {
(void)argc; (void)argv;
setvbuf(stdout, nullptr, _IONBF, 0);
const char* name = envOr("JIHAD_BS_NAME", "jihad-browserserver");
DownloadClient client(name);
client.bigUrl = envOr("JIHAD_DL_URL", "http://127.0.0.1:8137/big");
client.slowUrl = envOr("JIHAD_DL_SLOW", "http://127.0.0.1:8137/slow");
long expectSize = atol(envOr("JIHAD_DL_SIZE", "524288"));
if (!client.connect()) { fprintf(stderr, "[dlc] connect('%s') FAILED\n", name); return 1; }
printf("[dlc] connected to '%s'\n", name);
GMainContext* ctx = g_main_loop_get_context(client.mainLoop());
client.ctx = ctx;
client.start();
GSource* guard = g_timeout_source_new(25000);
g_source_set_callback(guard, phase2GuardCb, &client, nullptr);
g_source_attach(guard, ctx);
GSource* quit = g_timeout_source_new(45000);
g_source_set_callback(quit, quitCb, client.mainLoop(), nullptr);
g_source_attach(quit, ctx);
client.run();
{ int i1 = shmget(client.key1, client.sz, 0); if (i1 >= 0) shmctl(i1, IPC_RMID, nullptr);
int i2 = shmget(client.key2, client.sz, 0); if (i2 >= 0) shmctl(i2, IPC_RMID, nullptr); }
bool startOK = client.startBig >= 1;
bool progressOK = client.progressBig >= 1;
bool finishOK = client.finishedBig == 1 && !client.finishedPath.empty() &&
client.finishedMime.find("octet-stream") != std::string::npos &&
client.finishedSize == expectSize;
bool cancelOK = client.startSlow >= 1 && client.progressSlow >= 1 &&
client.cancelSent && client.finishedSlow == 0 &&
client.errorSlow >= 1;
printf("[dlc] big: start=%d progress=%d finished=%d mime='%s' size=%ld (expect %ld)\n",
client.startBig, client.progressBig, client.finishedBig,
client.finishedMime.c_str(), client.finishedSize, expectSize);
printf("[dlc] slow: start=%d progress=%d cancelSent=%d finished=%d error=%d\n",
client.startSlow, client.progressSlow, (int)client.cancelSent,
client.finishedSlow, client.errorSlow);
printf("[dlc] startOK=%d progressOK=%d finishOK=%d cancelOK=%d\n",
startOK, progressOK, finishOK, cancelOK);
bool ok = startOK && progressOK && finishOK && cancelOK;
printf("[dlc] %s\n", ok ? "DOWNLOAD-LIFECYCLE PASS" : "DOWNLOAD-LIFECYCLE FAIL");
return ok ? 0 : 2;
}
+23 -1
View File
@@ -67,12 +67,25 @@ public:
(void)focused; (void)fieldType; (void)fieldActions;
}
// A navigation resolved to a non-displayable resource (download / MIME handoff).
// Drives BrowserServerBase::msgMimeHandoffUrl (YAP 0x2014) -> adapter
// Drives BrowserServerBase::msgMimeHandoffUrl (YAP 0x2015) -> adapter
// mimeHandoffUrl -> BasicWebView.doFileLoad -> app handleResource, which routes
// it to com.palm.downloadmanager. (mimeType may be empty if unknown.)
virtual void msgMimeHandoffUrl(const char* mimeType, const char* url) {
(void)mimeType; (void)url;
}
// Lifecycle of a download the ENGINE performs to a temp file (YAP 0x2010..0x2013).
// Exactly one start, then >=0 progress, terminated by one finished OR one error.
virtual void msgDownloadStart(const char* url) { (void)url; }
virtual void msgDownloadProgress(const char* url, int32_t soFar, int32_t total) {
(void)url; (void)soFar; (void)total;
}
virtual void msgDownloadFinished(const char* url, const char* mimeType,
const char* tmpFilePath) {
(void)url; (void)mimeType; (void)tmpFilePath;
}
virtual void msgDownloadError(const char* url, const char* errorMsg) {
(void)url; (void)errorMsg;
}
};
class BrowserPageGoanna
@@ -89,6 +102,15 @@ public:
// Emit a download/MIME handoff for this page (from the process-wide download
// interceptor, routed to the active page). See IPageMessageSink::msgMimeHandoffUrl.
void emitMimeHandoff(const char* mimeType, const char* url) { mSink.msgMimeHandoffUrl(mimeType, url); }
// Same routing for the engine-performed download lifecycle (YAP msgDownload*).
void emitDownloadStart(const char* url) { mSink.msgDownloadStart(url); }
void emitDownloadProgress(const char* url, int32_t soFar, int32_t total) {
mSink.msgDownloadProgress(url, soFar, total);
}
void emitDownloadFinished(const char* url, const char* mime, const char* path) {
mSink.msgDownloadFinished(url, mime, path);
}
void emitDownloadError(const char* url, const char* msg) { mSink.msgDownloadError(url, msg); }
void returnBuffer(int sharedBufferKey); // YAP: returnBuffer (adapter freed it)
void freeze(); // YAP: freeze (card backgrounded — pause paint)
void thaw(int key1, int key2, int size); // YAP: thaw (reattach buffers, resume)
+286 -8
View File
@@ -13,9 +13,21 @@
#include "nsIExternalHelperAppService.h" // nsIHelperAppLauncher
#include "nsIMIMEInfo.h"
#include "nsIURI.h"
#include "nsIFileURL.h"
#include "nsIFile.h"
#include "nsICancelable.h"
#include "nsITransfer.h" // the embedding download-report hook
#include "nsIWebProgressListener.h" // STATE_STOP
#include "nsIComponentRegistrar.h"
#include "nsISupportsImpl.h" // NS_IMPL_ISUPPORTS
#include "nsDirectoryServiceDefs.h" // NS_OS_TEMP_DIR
#include "nsDirectoryServiceUtils.h" // NS_GetSpecialDirectory
#include "nsXPCOM.h"
#include "mozilla/RefPtr.h"
#include "mozilla/RefCountType.h"
#include <vector>
#include <cstdlib>
#include <cstdio>
namespace jihad {
@@ -28,6 +40,195 @@ static DownloadSink* gSink = nullptr;
static const nsCID kJihadHelperDialogCID = JIHAD_HELPERDIALOG_CID;
static const char* kHelperDialogContract = "@mozilla.org/helperapplauncherdialog;1";
// {b7c8d9ea-1f20-4a31-8b42-5c6d7e8f9a0b}
#define JIHAD_TRANSFER_CID \
{ 0xb7c8d9ea, 0x1f20, 0x4a31, \
{ 0x8b, 0x42, 0x5c, 0x6d, 0x7e, 0x8f, 0x9a, 0x0b } }
static const nsCID kJihadTransferCID = JIHAD_TRANSFER_CID;
// From nsITransfer.idl (NS_TRANSFER_CONTRACTID); spelled out so this TU does not
// depend on the %{C++ block's macro being exported.
static const char* kTransferContract = "@mozilla.org/transfer;1";
// ── the download the engine is performing ───────────────────────────────────
// nsExternalAppHandler creates ONE of these per download (via the
// "@mozilla.org/transfer;1" contract) and drives it with the standard
// nsIWebProgressListener2 notifications. That is the whole lifecycle:
// Init -> msgDownloadStart
// OnProgressChange64 -> msgDownloadProgress
// OnStateChange(STATE_STOP) -> msgDownloadFinished | msgDownloadError
// Init also hands us the nsICancelable, which is what makes the YAP
// cancelDownload command implementable (CancelDownload below).
class JihadTransfer final : public nsITransfer {
public:
NS_DECL_ISUPPORTS
NS_DECL_NSIWEBPROGRESSLISTENER
NS_DECL_NSIWEBPROGRESSLISTENER2
NS_DECL_NSITRANSFER
JihadTransfer() : mDone(false) {}
const nsCString& Url() const { return mUrl; }
bool Done() const { return mDone; }
// Abort this download. Cancelling the helper-app handler finishes its file
// saver with the abort status, which comes back as OnStateChange(STATE_STOP,
// <failure>) -> exactly one msgDownloadError and never a Finished. Safe to
// call re-entrantly (Finish is idempotent).
void Abort() {
if (mDone) return;
nsCOMPtr<nsICancelable> c = mCancelable;
if (c) c->Cancel(NS_BINDING_ABORTED);
// Nothing to cancel through: terminate the lifecycle ourselves so a client
// that asked for a cancel always gets a terminal message.
if (!c) Finish(NS_BINDING_ABORTED);
}
private:
~JihadTransfer() {}
void Finish(nsresult aStatus);
// Rename the "<target>.part" work file the helper-app service writes into over
// the final target. Firefox does this move in its own nsITransfer (Download.jsm);
// there is no other owner for it, so an embedder that supplies the contract must.
void FinalizePartFile();
nsCString mUrl;
nsCString mMime;
nsCOMPtr<nsIFile> mTarget; // where the finished file must end up
nsCOMPtr<nsIFile> mTempFile; // "<target>.part" while the download runs
nsCOMPtr<nsICancelable> mCancelable;
bool mDone;
};
NS_IMPL_ISUPPORTS(JihadTransfer, nsITransfer, nsIWebProgressListener,
nsIWebProgressListener2)
// Active (not yet finished) downloads, in start order. Strong refs: the entry is
// dropped in Finish(), so an in-flight download always has a live cancelable.
static std::vector<RefPtr<JihadTransfer>>* gActive = nullptr;
static void ActiveAdd(JihadTransfer* t) {
if (!gActive) gActive = new std::vector<RefPtr<JihadTransfer>>();
gActive->push_back(t);
}
static void ActiveRemove(JihadTransfer* t) {
if (!gActive) return;
for (size_t i = 0; i < gActive->size(); ++i) {
if ((*gActive)[i].get() == t) { gActive->erase(gActive->begin() + i); return; }
}
}
NS_IMETHODIMP
JihadTransfer::Init(nsIURI* aSource, nsIURI* aTarget, const nsAString& /*aDisplayName*/,
nsIMIMEInfo* aMIMEInfo, PRTime /*aStartTime*/, nsIFile* aTempFile,
nsICancelable* aCancelable, bool /*aIsPrivate*/) {
if (aSource) aSource->GetSpec(mUrl);
if (aMIMEInfo) aMIMEInfo->GetMIMEType(mMime);
mTempFile = aTempFile;
mCancelable = aCancelable;
// aTarget is a file:// URI for the destination we picked in the dialog.
nsCOMPtr<nsIFileURL> furl = do_QueryInterface(aTarget);
if (furl) furl->GetFile(getter_AddRefs(mTarget));
ActiveAdd(this);
printf("[jihad-dl] start %s (mime=%s)\n", mUrl.get(), mMime.get());
if (gSink) gSink->OnDownloadStart(mUrl.get());
return NS_OK;
}
NS_IMETHODIMP JihadTransfer::SetSha256Hash(const nsACString&) { return NS_OK; }
NS_IMETHODIMP JihadTransfer::SetSignatureInfo(nsIArray*) { return NS_OK; }
NS_IMETHODIMP JihadTransfer::SetRedirects(nsIArray*) { return NS_OK; }
NS_IMETHODIMP
JihadTransfer::OnProgressChange64(nsIWebProgress*, nsIRequest*,
int64_t, int64_t,
int64_t aCurTotalProgress,
int64_t aMaxTotalProgress) {
if (mDone) return NS_OK;
if (gSink) gSink->OnDownloadProgress(mUrl.get(), aCurTotalProgress, aMaxTotalProgress);
return NS_OK;
}
NS_IMETHODIMP
JihadTransfer::OnStateChange(nsIWebProgress*, nsIRequest*, uint32_t aStateFlags,
nsresult aStatus) {
if (aStateFlags & nsIWebProgressListener::STATE_STOP) Finish(aStatus);
return NS_OK;
}
// Unused halves of the listener surface.
NS_IMETHODIMP JihadTransfer::OnProgressChange(nsIWebProgress*, nsIRequest*, int32_t,
int32_t, int32_t, int32_t) { return NS_OK; }
NS_IMETHODIMP JihadTransfer::OnLocationChange(nsIWebProgress*, nsIRequest*, nsIURI*,
uint32_t) { return NS_OK; }
NS_IMETHODIMP JihadTransfer::OnStatusChange(nsIWebProgress*, nsIRequest*, nsresult,
const char16_t*) { return NS_OK; }
NS_IMETHODIMP JihadTransfer::OnSecurityChange(nsIWebProgress*, nsIRequest*,
uint32_t) { return NS_OK; }
NS_IMETHODIMP JihadTransfer::OnRefreshAttempted(nsIWebProgress*, nsIURI*, int32_t,
bool, bool* _retval) {
if (_retval) *_retval = true;
return NS_OK;
}
void JihadTransfer::FinalizePartFile() {
if (!mTempFile || !mTarget) return;
bool same = false;
if (NS_SUCCEEDED(mTempFile->Equals(mTarget, &same)) && same) return;
bool exists = false;
if (NS_FAILED(mTempFile->Exists(&exists)) || !exists) return;
nsAutoString leaf;
if (NS_FAILED(mTarget->GetLeafName(leaf))) return;
// Same directory (the service derived the .part name from our target), so a
// leaf-name move is an atomic rename over the placeholder we created.
mTempFile->MoveTo(nullptr, leaf);
}
void JihadTransfer::Finish(nsresult aStatus) {
if (mDone) return;
mDone = true;
// Keep ourselves alive across the sink callback + registry drop.
RefPtr<JihadTransfer> kungFuDeathGrip(this);
ActiveRemove(this);
mCancelable = nullptr;
if (NS_SUCCEEDED(aStatus)) {
FinalizePartFile();
nsAutoCString path;
if (mTarget) mTarget->GetNativePath(path);
printf("[jihad-dl] finished %s -> %s\n", mUrl.get(), path.get());
if (gSink) gSink->OnDownloadFinished(mUrl.get(), mMime.get(), path.get());
} else {
char msg[64];
snprintf(msg, sizeof msg, "0x%08x", (unsigned)static_cast<uint32_t>(aStatus));
printf("[jihad-dl] error %s (%s)\n", mUrl.get(), msg);
if (gSink) gSink->OnDownloadError(mUrl.get(), msg);
// A cancelled/failed download leaves two files behind: the "<target>.part"
// the saver was writing, and the empty target we reserved with CreateUnique.
// Both are ours; drop them so a cancel doesn't litter the download dir.
if (mTempFile) mTempFile->Remove(false);
if (mTarget) {
int64_t sz = -1;
if (NS_SUCCEEDED(mTarget->GetFileSize(&sz)) && sz == 0) mTarget->Remove(false);
}
}
}
bool CancelDownload(const char* url) {
if (!gActive || gActive->empty()) return false;
// Snapshot: Abort() runs the whole termination path synchronously and mutates
// gActive (Codex-style re-entrancy guard).
std::vector<RefPtr<JihadTransfer>> snap(*gActive);
bool any = false;
for (size_t i = 0; i < snap.size(); ++i) {
if (snap[i]->Done()) continue;
if (url && *url && !snap[i]->Url().Equals(nsDependentCString(url))) continue;
snap[i]->Abort();
any = true;
}
return any;
}
// ── the helper-app dialog override ──────────────────────────────────────────
class JihadHelperDialog final : public nsIFactory,
public nsIHelperAppLauncherDialog {
public:
@@ -40,6 +241,10 @@ class JihadHelperDialog final : public nsIFactory,
private:
static void Report(nsIHelperAppLauncher* aLauncher);
// Pick a temp destination and tell the launcher to save there. viaPrompt
// selects the callback the service is waiting on (saveDestinationAvailable
// for promptForSaveToFileAsync, saveToDisk for show).
static void BeginSave(nsIHelperAppLauncher* aLauncher, bool viaPrompt);
};
void JihadHelperDialog::Report(nsIHelperAppLauncher* aLauncher) {
@@ -58,6 +263,52 @@ void JihadHelperDialog::Report(nsIHelperAppLauncher* aLauncher) {
NS_ConvertUTF16toUTF8(name).get(), len);
}
// Destination directory for engine downloads: $JIHAD_DOWNLOAD_DIR, else the
// engine temp dir. On the device the launcher points this at the media
// partition so a finished file survives for the app to hand to the user.
static already_AddRefed<nsIFile> jihadDownloadDir() {
nsCOMPtr<nsIFile> dir;
const char* env = getenv("JIHAD_DOWNLOAD_DIR");
if (env && *env) {
if (NS_SUCCEEDED(NS_NewNativeLocalFile(nsDependentCString(env), true,
getter_AddRefs(dir))) && dir) {
nsresult crv = dir->Create(nsIFile::DIRECTORY_TYPE, 0700);
if (NS_FAILED(crv) && crv != NS_ERROR_FILE_ALREADY_EXISTS) dir = nullptr;
}
}
if (!dir) NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dir));
return dir.forget();
}
void JihadHelperDialog::BeginSave(nsIHelperAppLauncher* aLauncher, bool viaPrompt) {
if (!aLauncher) return;
nsCOMPtr<nsIFile> dir = jihadDownloadDir();
nsCOMPtr<nsIFile> file;
if (dir) dir->Clone(getter_AddRefs(file));
if (file) {
nsString name;
aLauncher->GetSuggestedFileName(name);
// The service already stripped path separators + illegal characters from
// the suggested name; guard against an empty one only.
if (name.IsEmpty()) name.AssignLiteral("jihad-download");
if (NS_FAILED(file->Append(name)) ||
NS_FAILED(file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600))) {
file = nullptr;
}
}
if (!file) {
// No writable destination: terminate the launcher rather than leave it (and
// its temp file) dangling. msgMimeHandoffUrl already went out, so the client
// can still fetch the URL itself.
fprintf(stderr, "[jihad-dl] no writable download dir — aborting engine save\n");
if (viaPrompt) aLauncher->SaveDestinationAvailable(nullptr);
else aLauncher->Cancel(NS_BINDING_ABORTED);
return;
}
if (viaPrompt) aLauncher->SaveDestinationAvailable(file);
else aLauncher->SaveToDisk(file, false);
}
NS_IMETHODIMP
JihadHelperDialog::QueryInterface(const nsIID& aIID, void** aResult) {
if (!aResult) return NS_ERROR_NULL_POINTER;
@@ -82,16 +333,12 @@ JihadHelperDialog::CreateInstance(nsISupports* aOuter, const nsIID& aIID, void**
}
NS_IMETHODIMP JihadHelperDialog::LockFactory(bool) { return NS_OK; }
// nsIHelperAppLauncherDialog — capture the handoff instead of opening a window.
// NOTE (Codex P1): this UXP's nsIHelperAppLauncher has no Cancel(); returning
// NS_OK without SaveToDisk leaves the launcher unresolved (the engine may leave a
// temp file). Actually saving/cancelling requires the adapter to choose a
// destination over the YAP contract (msgDownload* + a reply) -- device/adapter
// work. Here we only surface the handoff so the daemon never blocks on a chrome
// save dialog.
// nsIHelperAppLauncherDialog — capture the handoff instead of opening a window,
// then drive the save so the download actually completes and is reported.
NS_IMETHODIMP
JihadHelperDialog::Show(nsIHelperAppLauncher* aLauncher, nsISupports*, uint32_t) {
Report(aLauncher);
BeginSave(aLauncher, /* viaPrompt */ false);
return NS_OK;
}
NS_IMETHODIMP
@@ -99,11 +346,38 @@ JihadHelperDialog::PromptForSaveToFileAsync(nsIHelperAppLauncher* aLauncher,
nsISupports*, const char16_t*,
const char16_t*, bool) {
Report(aLauncher);
BeginSave(aLauncher, /* viaPrompt */ true);
return NS_OK;
}
static JihadHelperDialog& Singleton() { static JihadHelperDialog s; return s; }
// Factory for the per-download transfer objects (unlike the dialog, each
// CreateInstance MUST hand back a fresh, really-refcounted object).
class JihadTransferFactory final : public nsIFactory {
public:
NS_IMETHOD QueryInterface(const nsIID& aIID, void** aResult) override {
if (!aResult) return NS_ERROR_NULL_POINTER;
if (aIID.Equals(NS_GET_IID(nsISupports)) || aIID.Equals(NS_GET_IID(nsIFactory))) {
*aResult = static_cast<nsIFactory*>(this);
AddRef();
return NS_OK;
}
*aResult = nullptr;
return NS_NOINTERFACE;
}
NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override { return 2; }
NS_IMETHOD_(MozExternalRefCountType) Release(void) override { return 1; }
NS_IMETHOD CreateInstance(nsISupports* aOuter, const nsIID& aIID, void** aResult) override {
if (aOuter) return NS_ERROR_NO_AGGREGATION;
RefPtr<JihadTransfer> t = new JihadTransfer();
return t->QueryInterface(aIID, aResult);
}
NS_IMETHOD LockFactory(bool) override { return NS_OK; }
};
static JihadTransferFactory& TransferFactory() { static JihadTransferFactory f; return f; }
bool InstallDownloadService() {
static bool installed = false;
if (installed) return true;
@@ -111,7 +385,11 @@ bool InstallDownloadService() {
if (NS_FAILED(NS_GetComponentRegistrar(getter_AddRefs(reg))) || !reg) return false;
nsresult rv = reg->RegisterFactory(kJihadHelperDialogCID, "Jihad Helper Dialog",
kHelperDialogContract, &Singleton());
installed = NS_SUCCEEDED(rv);
// Without this second registration the helper-app service cannot create an
// nsITransfer and CANCELS every download (CreateTransfer failure => Cancel).
nsresult rv2 = reg->RegisterFactory(kJihadTransferCID, "Jihad Transfer",
kTransferContract, &TransferFactory());
installed = NS_SUCCEEDED(rv) && NS_SUCCEEDED(rv2);
return installed;
}
+45 -3
View File
@@ -10,6 +10,19 @@
* dialog opens a chrome save/open window -- meaningless in the headless daemon.
* We override that contract so every download/handoff is reported to a sink,
* which maps to the YAP msgDownload* / msgMimeNotSupported surface.
*
* Two things happen for one captured download:
* 1. OnDownload -> msgMimeHandoffUrl (YAP 0x2015): the isis app's existing
* path, which hands the URL to com.palm.downloadmanager.
* 2. the engine SAVES the response to a temp file and reports the lifecycle
* (OnDownloadStart / OnDownloadProgress / OnDownloadFinished |
* OnDownloadError) -> msgDownloadStart/Progress/Finished/Error
* (YAP 0x2010..0x2013), the same messages the stock isis BrowserServer
* emitted from its WebKit download callbacks.
* (2) costs no extra network traffic: nsExternalAppHandler has already opened
* the channel and is streaming the body into its own temp file by the time the
* dialog is consulted. Driving saveToDisk is what makes that download RESOLVE
* (and stops the temp file being orphaned) instead of hanging unreferenced.
*/
#ifndef JIHAD_DOWNLOAD_SERVICE_H
#define JIHAD_DOWNLOAD_SERVICE_H
@@ -20,17 +33,40 @@
namespace jihad {
// Process-wide receiver of download/handoff requests. Not owned by the service.
// Every call runs on the embedding (main) thread.
class DownloadSink {
public:
virtual ~DownloadSink() {}
// Called when content is handed off as a download. All strings are UTF-8;
// contentLength is -1 when unknown.
// contentLength is -1 when unknown. Drives msgMimeHandoffUrl.
virtual void OnDownload(const char* url, const char* mimeType,
const char* suggestedName, int64_t contentLength) = 0;
// --- engine-performed download lifecycle (YAP msgDownload*) ---------------
// Defaulted to no-ops so existing sinks (tests) need not override them.
// Exactly one Start is emitted per download, followed by zero or more
// Progress, terminated by exactly one Finished OR one Error.
virtual void OnDownloadStart(const char* url) { (void)url; }
// bytesSoFar/totalBytes are the raw 64-bit counts; totalBytes is -1 when the
// response had no Content-Length.
virtual void OnDownloadProgress(const char* url, int64_t bytesSoFar,
int64_t totalBytes) {
(void)url; (void)bytesSoFar; (void)totalBytes;
}
// tmpFilePath is the on-disk path of the completed file (see JIHAD_DOWNLOAD_DIR).
virtual void OnDownloadFinished(const char* url, const char* mimeType,
const char* tmpFilePath) {
(void)url; (void)mimeType; (void)tmpFilePath;
}
virtual void OnDownloadError(const char* url, const char* errorMsg) {
(void)url; (void)errorMsg;
}
};
// Install the override of "@mozilla.org/helperapplauncherdialog;1". Call once
// after the engine is initialised. Idempotent; returns false on failure.
// Install the override of "@mozilla.org/helperapplauncherdialog;1" and of
// "@mozilla.org/transfer;1" (the embedding hook the helper-app service uses to
// report download progress). Call once after the engine is initialised.
// Idempotent; returns false on failure.
bool InstallDownloadService();
// Set (or clear, with nullptr) the process-wide sink. Not owned. Threading/
@@ -39,6 +75,12 @@ bool InstallDownloadService();
// it as a process-lifetime backstop.
void SetDownloadSink(DownloadSink* sink);
// YAP: cancelDownload(url) — abort an in-flight engine download. Matches the
// source URL emitted by OnDownloadStart; an empty/null url cancels every active
// download. Returns true if at least one download was aborted. The aborted
// download reports OnDownloadError (never OnDownloadFinished).
bool CancelDownload(const char* url);
} // namespace jihad
#endif // JIHAD_DOWNLOAD_SERVICE_H
+27 -2
View File
@@ -173,9 +173,16 @@ public:
NS_IMETHOD GetFile(const char* aProp, bool* aPersistent, nsIFile** aResult) override
{
*aPersistent = true;
// The four profile keys nsXULAppAPI.h documents for embedders that pick the
// profile BEFORE XRE_InitEmbedding. "ProfDS"/"ProfLDS" are the *startup*
// aliases nsXREDirProvider falls back to for consumers that run before a
// profile is "selected" — the startup cache is one (StartupCache::Init asks
// for ProfLDS), so omitting them left it homeless.
if (mProfile &&
(!strcmp(aProp, "ProfD") || // NS_APP_USER_PROFILE_50_DIR
!strcmp(aProp, "ProfLD"))) { // NS_APP_USER_PROFILE_LOCAL_50_DIR
(!strcmp(aProp, "ProfD") || // NS_APP_USER_PROFILE_50_DIR
!strcmp(aProp, "ProfLD") || // NS_APP_USER_PROFILE_LOCAL_50_DIR
!strcmp(aProp, "ProfDS") || // NS_APP_PROFILE_DIR_STARTUP
!strcmp(aProp, "ProfLDS"))) { // NS_APP_PROFILE_LOCAL_DIR_STARTUP
nsCOMPtr<nsIFile> f;
if (NS_SUCCEEDED(mProfile->Clone(getter_AddRefs(f))) && f) {
f.forget(aResult);
@@ -258,6 +265,24 @@ EngineHost::Init(const char* greDir)
if (mInited) {
InstallDialogService();
InstallDownloadService();
// ── announce the profile ────────────────────────────────────────────────
// XRE_InitEmbedding2 takes the directory provider but does NOT arm it:
// nsXREDirProvider::GetFile short-circuits EVERY profile key with
// if (!mProfileNotified) return NS_ERROR_FAILURE;
// BEFORE it ever consults the embedder's provider, and mProfileNotified is
// only set by XRE_NotifyProfile()/DoStartup(). So without this call
// NS_GetSpecialDirectory("ProfD") fails process-wide, and every consumer
// that keys off it silently degrades to memory-only — nsCookieService takes
// its "couldn't get cookie file" branch in InitDBStates(), which is exactly
// why cookies.sqlite was never created and cookies died with the daemon
// (browser-services R2). nsXULAppAPI.h documents this call as mandatory for
// scenario 1 (profile chosen before XRE_InitEmbedding): "XRE_NotifyProfile
// should be called immediately after XRE_InitEmbedding".
// It also fires profile-do-change / profile-after-change, the notifications
// profile-scoped services wait on. Called BEFORE the pref block below
// because DoStartup() runs Preferences::ResetAndReadUserPrefs(), which would
// otherwise discard the runtime prefs we set here.
if (sJihadDirProvider) XRE_NotifyProfile();
// Mobile-browser defaults. <meta name=viewport> is off by default on desktop
// Goanna; a webOS phone browser must honor it (drives msgMetaViewportSet).
nsCOMPtr<nsIPrefBranch> pb =
+2 -1
View File
@@ -20,7 +20,7 @@ internals; the YAP interface and shared-buffer plumbing are unchanged.
| Selection/clipboard | `nsISelectionController`, `nsIClipboard`, `nsICommandManager` (`cmd_copy/cut/paste/selectAll`) | copy/cut/paste/selectAll, caret bounds via selection range rects. |
| Cookies / cache | `nsICookieManager2`, `nsICacheStorageService` | `clearCookies`, `clearCache`, `setAcceptCookies`. |
| User agent | `nsIHttpProtocolHandler` userAgent override / pref `general.useragent.override` | `setUserAgent`. |
| Downloads | `nsIWebBrowserPersist` / `nsIExternalHelperAppService` / `nsIHelperAppLauncherDialog` | download start/progress/error/finished, MIME not-supported handoff. |
| Downloads | `nsIHelperAppLauncherDialog` override (capture the handoff + drive `saveToDisk`) **and** an `@mozilla.org/transfer;1` (`nsITransfer`) implementation | `render/goanna/DownloadService.cpp`. The dialog override → `msgMimeHandoffUrl`; the transfer's `init`/`onProgressChange64`/`onStateChange(STATE_STOP)` → `msgDownloadStart/Progress/Finished/Error`; the `nsICancelable` handed to `init` is what `cancelDownload` aborts. Registering the transfer contract is mandatory: without it `nsExternalAppHandler::CreateTransfer` fails and cancels every download. |
| TLS/cert prompts | `nsICertOverrideService`, `nsIBadCertListener2` | `msgDialogSSLConfirm` (bridge to webOS CertificateMgr as the QtWebKit path did). |
| JS dialogs | `nsIPromptService` / `nsIPrompt` override | `msgDialogAlert/Confirm/Prompt/UserPassword`. |
| Network interface / DNS | proxy + `nsIDNSService` / interface binding via socket transport | `setNetworkInterface`, `setDNSServers` (may need engine patch for bind-to-iface). |
@@ -44,6 +44,7 @@ internals; the YAP interface and shared-buffer plumbing are unchanged.
| `clearCache/clearCookies` | cache storage clear / cookie manager `removeAll`. |
| `setEnableJavaScript/setBlockPopups/setAcceptCookies/setMinFontSize/setUserAgent` | set corresponding `nsIPrefBranch` prefs / per-docshell `nsIDocShell.allowJavascript`. |
| hit-test family | `nsIDOMWindowUtils.elementFromPoint` + element geometry → matching `msg…Response`. |
| `cancelDownload(url)` | `jihad::CancelDownload` → `nsICancelable.cancel(NS_BINDING_ABORTED)` on the matching in-flight transfer (process-wide, not per page). |
## Paint loop (the hard part)
+69
View File
@@ -0,0 +1,69 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Jihad Browser — cookie PERSISTENCE test (domain G / R2).
*
* Run twice against the same $JIHAD_PROFILE_DIR (that second run IS the
* "daemon restart"):
* set — load /set (Set-Cookie with a future Max-Age, i.e. a PERSISTENT
* cookie; session cookies are not supposed to survive), then /echo to
* prove the cookie is live in-session.
* check — load /echo only. The cookie can only come back if the engine read
* it from cookies.sqlite in the profile.
* Both modes report whether cookies.sqlite exists after a clean shutdown.
*/
#include <gtk/gtk.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <sys/stat.h>
#include "../GoannaRenderPage.h"
#include "../EngineHost.h"
static const char* kCookie = "jihadpersist=1";
static bool fileExists(const std::string& p) {
struct stat st;
return stat(p.c_str(), &st) == 0;
}
int main(int argc, char** argv) {
setvbuf(stdout, nullptr, _IONBF, 0);
const char* greDir = (argc > 1) ? argv[1] : ".";
const char* mode = (argc > 2) ? argv[2] : "set";
const char* base = (argc > 3) ? argv[3] : "http://127.0.0.1:8138";
const bool setMode = strcmp(mode, "set") == 0;
gtk_init(&argc, &argv);
jihad::EngineHost host;
if (!host.Init(greDir)) { fprintf(stderr, "[cookie] engine init FAIL\n"); return 1; }
printf("[cookie] engine up (mode=%s)\n", mode);
bool ok = false;
{
jihad::GoannaRenderPage page(host);
if (!page.Create(640, 480)) { host.Shutdown(); return 2; }
if (setMode) {
page.LoadUrlAndWait((std::string(base) + "/set").c_str(), 25);
page.PumpFor(1500);
}
page.LoadUrlAndWait((std::string(base) + "/echo").c_str(), 25);
page.PumpFor(1500);
std::string title = page.GetTitle();
printf("[cookie] %s-run echo title='%s'\n", mode, title.c_str());
ok = title.find(kCookie) != std::string::npos;
}
host.Shutdown(); // clean teardown: this is what flushes/closes cookies.sqlite
const char* prof = getenv("JIHAD_PROFILE_DIR");
std::string db = std::string(prof ? prof : "") + "/cookies.sqlite";
bool dbOK = prof && *prof && fileExists(db);
printf("[cookie] cookies.sqlite (%s): %s\n", db.c_str(), dbOK ? "PRESENT" : "MISSING");
bool pass = ok && dbOK;
printf("[cookie] %s %s\n", setMode ? "COOKIE-SET" : "COOKIE-PERSIST",
pass ? "PASS" : "FAIL");
return pass ? 0 : 4;
}