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
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,24 @@ public interface Listener {
*/
@NonNull
public Result drive(boolean includeRepos, @Nullable Listener l) {
return drive(includeRepos, false, l);
}

/**
* As {@link #drive(boolean, Listener)}, but {@code force} ignores a leftover "done" status and
* (re)starts a fresh seed. The status file is a single, shared marker, so a prior op's "done"
* would otherwise short-circuit an INTENTIONAL re-seed (K2GO-422 post-install repos) in a few ms
* without running anything. The box POST rewrites the status to "running" synchronously, so the
* poll then follows the fresh run. Non-forced keeps the re-attach optimization (an app that died
* mid-seed reads the finished box status instead of re-running the whole seed).
*/
@NonNull
public Result drive(boolean includeRepos, boolean force, @Nullable Listener l) {
String state = readState(l);
if ("done".equals(state)) return Result.DONE;
if (!force && "done".equals(state)) return Result.DONE;
if (!"running".equals(state)) {
// idle / error / missing -> (re)start it. A 409 "already running" is fine: fall through to poll.
// idle / error / missing (or forced past a stale done) -> (re)start it. A 409 "already
// running" is fine: fall through to poll.
if (!postStart(includeRepos)) return Result.ERROR;
}
final long deadline = System.currentTimeMillis() + MAX_WAIT_MS;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* ============================================================================
* Name : ForgejoStatusClient.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : K2GO-422. App-side client of the dash-node Forgejo status endpoint
* (static/dashboard GET /k2go-api/forgejo/status, dash-node 1.3.6).
* Read-only: it tells the module detail whether to offer "Install
* repos", show the repos as present, or block (an admin exists that
* K2Go cannot authenticate). The box does the work over its own API,
* so this is a plain HTTP GET the app runs on an IO thread.
* ============================================================================
*/
package org.appdevforall.k2go.forgejo.data;

import androidx.annotation.Nullable;

import org.appdevforall.k2go.config.BoxEndpoints;
import org.json.JSONArray;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

/** Fetches the box Forgejo status. {@link #fetch()} BLOCKS (one HTTP GET), so callers run it off the
* main thread. Returns {@code null} when the box did not answer (unreachable / not ready). */
public final class ForgejoStatusClient {

/** Immutable snapshot of the box Forgejo, as the status endpoint reports it. */
public static final class Status {
public final boolean reachable;
public final boolean adminExists;
public final boolean adminAuthenticable;
public final boolean manageable;
public final int repoCount;

Status(boolean reachable, boolean adminExists, boolean adminAuthenticable,
boolean manageable, int repoCount) {
this.reachable = reachable;
this.adminExists = adminExists;
this.adminAuthenticable = adminAuthenticable;
this.manageable = manageable;
this.repoCount = repoCount;
}

/** K2Go can administer the forge and it has no example repos yet -> offer "Install repos". */
public boolean canInstallRepos() { return manageable && repoCount == 0; }

/** The example repos are present -> nothing to install (refresh/update is K2GO-422 section A). */
public boolean hasRepos() { return repoCount > 0; }

/** An admin exists that K2Go cannot authenticate -> do not offer any repo action. */
public boolean blocked() { return !manageable; }
}

private static final String STATUS_URL = BoxEndpoints.API + "/forgejo/status";

/** GET the status; null on any read/parse error (treat as "cannot tell", offer nothing). */
@Nullable
public Status fetch() {
try {
HttpURLConnection c = (HttpURLConnection) new URL(STATUS_URL).openConnection();
c.setUseCaches(false);
c.setConnectTimeout(4000);
// The box may do a couple of short HTTP calls to the forge before answering; keep the read
// timeout comfortably above their combined worst case so a warming forge does not make the
// status read time out (which would silently hide the button).
c.setReadTimeout(30000);
c.setRequestMethod("GET");
c.setRequestProperty("Accept", "application/json");
int code = c.getResponseCode();
boolean ok = code >= 200 && code < 300;
String text = readAll(ok ? c.getInputStream() : c.getErrorStream());
c.disconnect();
if (!ok) return null;
JSONObject j = new JSONObject(text.isEmpty() ? "{}" : text);
JSONArray repos = j.optJSONArray("repos");
int repoCount = repos != null ? repos.length() : 0;
return new Status(
j.optBoolean("reachable", false),
j.optBoolean("adminExists", false),
j.optBoolean("adminAuthenticable", false),
j.optBoolean("manageable", false),
repoCount);
} catch (Exception e) {
return null;
}
}

private static String readAll(@Nullable InputStream is) throws Exception {
if (is == null) return "";
ByteArrayOutputStream buf = new ByteArrayOutputStream();
byte[] chunk = new byte[4096];
int n;
while ((n = is.read(chunk)) != -1) buf.write(chunk, 0, n);
is.close();
return buf.toString(StandardCharsets.UTF_8.name());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,19 @@ public final class ForgejoSeedService extends Service {
private static final int NOTIFICATION_ID = 8;

public static final String ACTION_START = "org.iiab.controller.FORGEJO_SEED_START";
/** K2GO-422: force a fresh seed past a leftover "done" status (an intentional post-install re-seed). */
private static final String EXTRA_FORCE = "force";

private final Handler main = new Handler(Looper.getMainLooper());

/** Start (or re-attach to) the seed. Idempotent: a running session is left alone. */
public static void start(Context ctx) {
public static void start(Context ctx) { start(ctx, false); }

/** As {@link #start(Context)}, but {@code force} re-runs even if the box status is a stale "done"
* (K2GO-422: the post-install "Install repos" is an intentional re-seed). */
public static void start(Context ctx, boolean force) {
ContextCompat.startForegroundService(ctx,
new Intent(ctx, ForgejoSeedService.class).setAction(ACTION_START));
new Intent(ctx, ForgejoSeedService.class).setAction(ACTION_START).putExtra(EXTRA_FORCE, force));
}

/** Clear the finished session so a later install starts clean. */
Expand Down Expand Up @@ -96,16 +102,17 @@ public int onStartCommand(Intent intent, int flags, int startId) {
return START_NOT_STICKY;
}
boolean includeRepos = ForgejoInstallPrefs.includeRepos(this);
boolean force = intent != null && intent.getBooleanExtra(EXTRA_FORCE, false);
repo.startSession(includeRepos);
drive(includeRepos);
drive(includeRepos, force);
return START_NOT_STICKY;
}

/** Total drive attempts before the seed is marked failed (best-effort give-up). */
private static final int MAX_ATTEMPTS = ForgejoInstallPrefs.MAX_ATTEMPTS;
private static final long RETRY_DELAY_MS = 4000L;

private void drive(final boolean includeRepos) {
private void drive(final boolean includeRepos, final boolean force) {
final Context app = getApplicationContext();
AppExecutors.get().io().execute(() -> {
ForgejoSeedRepository repo = ForgejoSeedRepository.get();
Expand All @@ -127,7 +134,7 @@ private void drive(final boolean includeRepos) {
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
Log.i(TAG, "forgejo seed: driving (includeRepos=" + includeRepos + ", attempt "
+ attempt + "/" + MAX_ATTEMPTS + ")");
r = new ForgejoSeedClient().drive(includeRepos, repo::appendLog);
r = new ForgejoSeedClient().drive(includeRepos, force, repo::appendLog);
if (r == ForgejoSeedClient.Result.DONE) break;
if (attempt < MAX_ATTEMPTS) {
repo.appendLog("seed attempt " + attempt + " failed; retrying");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,39 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
}
if (isInstalled) {
addStatus(statusRow, K2GoStatusBadge.create(requireContext(), getString(R.string.k2go_mod_phase_done), R.color.k2go_leaf));
return; // nothing to offer: a module cannot be uninstalled or reinstalled here
// K2GO-422: an installed Forgejo can still be missing its example repos (installed
// with the opt-in off, or a seed that never completed). Offer "Install repos" when
// the box status says the forge is manageable and has no repos yet. Read the status
// off the main thread; a null read (box not ready) or repos already present offers
// nothing, and an admin we cannot authenticate shows a blocked note.
if (isForgejo) {
org.appdevforall.k2go.util.AppExecutors.get().io().execute(() -> {
final org.appdevforall.k2go.forgejo.data.ForgejoStatusClient.Status st =
new org.appdevforall.k2go.forgejo.data.ForgejoStatusClient().fetch();
root.post(() -> {
if (!isAdded() || st == null) return;
if (st.canInstallRepos()) {
installNowBtn.setText(R.string.k2go_forgejo_install_repos);
installNowBtn.setOnClickListener(v -> {
// Get More style: bank the seed, start the foreground service, and
// hand off to the tasks index (SetupProgressActivity), which tracks
// the running seed to completion (EXTRA_FORGEJO_SEED).
org.appdevforall.k2go.forgejo.data.ForgejoInstallPrefs.bankSeed(requireContext(), true);
// force: this is an intentional re-seed; do not let a prior seed's
// leftover "done" status short-circuit it (K2GO-422).
org.appdevforall.k2go.forgejo.presentation.ForgejoSeedService.start(requireContext(), true);
startActivity(new android.content.Intent(requireContext(), SetupProgressActivity.class)
.putExtra(SetupProgressActivity.EXTRA_FORGEJO_SEED, true));
});
installNowBtn.setVisibility(View.VISIBLE);
} else if (st.blocked()) {
addStatus(statusRow, K2GoStatusBadge.create(requireContext(),
getString(R.string.k2go_forgejo_repos_blocked), R.color.k2go_amber_text));
}
});
});
}
return; // a module cannot be uninstalled or reinstalled here (repos action aside)
}
// ADFA-4898: this module's runrole failed in the last finished batch — the SAME per-module
// didFail(key) that colours the hub's "Couldn't install" pill, so only the module that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public class SetupProgressActivity extends AppCompatActivity implements org.appd
/** ADFA-5011: this screen is driving a dash-node REST-core rebuild (not an install/content drain).
* Latched so the screen stays on the animation and blocks leaving until the rebuild is SUCCESS/FAILED. */
public static final String EXTRA_REBUILD = "rebuild";
/** K2GO-422: this run is a post-install Forgejo repo seed (the "Install repos" button), so the
* index tracks and waits on the seed even though there is no module install this run. */
public static final String EXTRA_FORGEJO_SEED = "forgejoSeed";

private static final long READY_POLL_MS = 2000L;
private static final long REDIRECT_MS = 3000L;
Expand Down Expand Up @@ -116,6 +119,7 @@ public class SetupProgressActivity extends AppCompatActivity implements org.appd
private boolean moduleStartFailed = false;
private boolean moduleSeen = false; // latched once a non-maps proot batch is seen
private boolean forgejoSeedSeen = false; // K2GO-423: latched once a Forgejo seed belongs to this session
private boolean postInstallSeed = false; // K2GO-422: this run was launched to seed repos post-install
private int readyPolls = 0; // ADFA-4874: failed readiness polls so far (slow-start message)
// ADFA-4842: a real module batch stops the server (pdsm stop) for its runroles. When the queue is
// DONE, the index restarts the server and WAITS here — showing "Starting services…" — until the REST
Expand All @@ -136,6 +140,8 @@ public class SetupProgressActivity extends AppCompatActivity implements org.appd
protected void onCreate(@Nullable Bundle s) {
super.onCreate(s);
setContentView(R.layout.activity_k2go_setup_progress);
// K2GO-422: launched by the "Install repos" button, so this run tracks the post-install seed.
postInstallSeed = getIntent() != null && getIntent().getBooleanExtra(EXTRA_FORGEJO_SEED, false);

dot = findViewById(R.id.k2go_sp_dot);
statusText = findViewById(R.id.k2go_sp_status);
Expand Down Expand Up @@ -709,7 +715,8 @@ private void render() {
// once it is imminent (module server up) or already has a session, so it does not sit as
// "Queued" through the whole runrole while the module row already tells that story.
if (forgejoSeedInSession()
&& (serverObservedUp() || org.appdevforall.k2go.forgejo.presentation.ForgejoSeedRepository.get().hasSession())) {
&& (serverObservedUp() || postInstallSeed
|| org.appdevforall.k2go.forgejo.presentation.ForgejoSeedRepository.get().hasSession())) {
sections.addView(forgejoSeedRow());
}

Expand Down Expand Up @@ -760,8 +767,14 @@ private void render() {
// - not when the forgejo runrole FAILED -- the seed never starts then (it would clear the
// marker), so waiting on it would hang; that run is already a failure (Finish + Retry);
// - not when the server is slow/failed -- also already a failure.
boolean seedPendingRun = forgejoSeedActive() && !batchServerSlow && moduleShown
&& !mq.didFail("forgejo");
// K2GO-422: also wait in a post-install seed run (the "Install repos" button, no module this
// run). The stranded case (a banked seed leaking into an unrelated Get More flow) has neither
// moduleShown nor the launch extra, so it still does not block -- no hang reintroduced.
boolean seedPendingRun = forgejoSeedActive() && !batchServerSlow
&& (moduleShown || postInstallSeed)
// The forgejo runrole failing releases the gate ONLY in a module-install flow; a
// post-install seed run (postInstallSeed) must not read a stale/unrelated queue verdict.
&& !(moduleShown && mq.didFail("forgejo"));
if (noRest && prootShown) {
// proot-only: complete when the queue is terminal — plus, for a module batch, once the server
// is back (up) or the restart has failed (a dead home that wakes up seconds later is exactly
Expand Down
2 changes: 2 additions & 0 deletions controller/app/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1359,4 +1359,6 @@
<string name="k2go_forgejo_seed_repos_fmt">تمت إضافة %1$d مستودعات</string>
<string name="k2go_forgejo_seed_leave_title">يمكنك ترك هذا قيد التشغيل</string>
<string name="k2go_forgejo_seed_leave_body">يستمر الإعداد في الخلفية. عُد في أي وقت للتحقق من التقدم.</string>
<string name="k2go_forgejo_install_repos">تثبيت مستودعات نموذجية</string>
<string name="k2go_forgejo_repos_blocked">يحتوي مضيف الشيفرة هذا بالفعل على مسؤول لا يستطيع K2Go إدارته.</string>
</resources>
2 changes: 2 additions & 0 deletions controller/app/src/main/res/values-az/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1379,4 +1379,6 @@
<string name="k2go_forgejo_seed_repos_fmt">%1$d anbar əlavə edildi</string>
<string name="k2go_forgejo_seed_leave_title">Bunu işləməyə buraxa bilərsiniz</string>
<string name="k2go_forgejo_seed_leave_body">Quraşdırma arxa planda davam edir. Gedişatı yoxlamaq üçün istənilən vaxt geri qayıdın.</string>
<string name="k2go_forgejo_install_repos">Nümunə anbarlarını quraşdır</string>
<string name="k2go_forgejo_repos_blocked">Bu kod hostunda artıq K2Go-nun idarə edə bilmədiyi bir admin var.</string>
</resources>
2 changes: 2 additions & 0 deletions controller/app/src/main/res/values-bg/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1366,4 +1366,6 @@
<string name="k2go_forgejo_seed_repos_fmt">Добавени хранилища: %1$d</string>
<string name="k2go_forgejo_seed_leave_title">Можете да оставите това да работи</string>
<string name="k2go_forgejo_seed_leave_body">Настройката продължава на заден план. Върнете се по всяко време, за да проверите напредъка.</string>
<string name="k2go_forgejo_install_repos">Инсталиране на примерни хранилища</string>
<string name="k2go_forgejo_repos_blocked">Този хост за код вече има администратор, който K2Go не може да управлява.</string>
</resources>
2 changes: 2 additions & 0 deletions controller/app/src/main/res/values-bn/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1372,4 +1372,6 @@
<string name="k2go_forgejo_seed_repos_fmt">%1$d রিপোজিটরি যোগ করা হয়েছে</string>
<string name="k2go_forgejo_seed_leave_title">আপনি এটি চলতে রেখে দিতে পারেন</string>
<string name="k2go_forgejo_seed_leave_body">সেটআপ পটভূমিতে চলতে থাকে। অগ্রগতি দেখতে যেকোনো সময় ফিরে আসুন।</string>
<string name="k2go_forgejo_install_repos">উদাহরণ রিপোজিটরি ইনস্টল করুন</string>
<string name="k2go_forgejo_repos_blocked">এই কোড হোস্টে ইতিমধ্যে একজন অ্যাডমিন আছে যা K2Go পরিচালনা করতে পারে না।</string>
</resources>
2 changes: 2 additions & 0 deletions controller/app/src/main/res/values-cs/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1366,4 +1366,6 @@
<string name="k2go_forgejo_seed_repos_fmt">Přidáno repozitářů: %1$d</string>
<string name="k2go_forgejo_seed_leave_title">Můžete to nechat běžet</string>
<string name="k2go_forgejo_seed_leave_body">Nastavení pokračuje na pozadí. Kdykoli se vraťte a zkontrolujte průběh.</string>
<string name="k2go_forgejo_install_repos">Nainstalovat ukázkové repozitáře</string>
<string name="k2go_forgejo_repos_blocked">Tento hostitel kódu už má správce, kterého K2Go nemůže spravovat.</string>
</resources>
Loading
Loading