Skip to content

Commit 49b447f

Browse files
fix(launcher): prevent kernel update corruption and improve extraction (#101)
* refactor: rename BotBrowserConsole to BotBrowserLauncher * feat: add kernel management, proxy management, and setup scripts * fix: enable macOS native Edit menu for Cmd+C/V/X/A keyboard shortcuts * chore: upgrade Neutralino runtime to 6.4.0 * chore: remove Neutralino binaries from repo, download via neu update * fix: improve macOS clipboard support with JS fallback for Cmd+V paste * fix: expand macOS clipboard fallback to Cmd+C/X, auto-download Neutralino via postinstall * chore: upgrade Neutralino to 6.5.0 and simplify setup scripts * fix: fix PowerShell setup script ZIP download corruption * feat: enhance proxy workflow with IP check, quick change, and reusable input component * fix: correct checkbox color override and setup script stdin handling * feat: improve profile deletion with auto-stop, group WebRTC settings, prioritize custom exec path * feat: add port protection toggle, fix kernel auto-update cleanup and UI refresh * fix: detect kernel assets by file extension to support renamed Windows/Linux packages * fix: handle non-browser process exits, add crash detection, validate kernel executable paths * docs: add --bot-time-seed documentation, changelog 2026-03-03, value range and per-context cross-links * feat: add proxy save/check, improve table layout and kernel date display - Add "Save to proxy list" button in proxy input for reusing manually entered proxies - Add batch proxy connectivity check with IP/status display in proxy management - Sort proxy list by creation time (newest first) - Fix kernel date display: parse from asset filename, fix timezone offset bug - Simplify profile status column to icon-only (play/stop) with tooltips - Remove year from "Last Launch" date to prevent truncation - Widen window to 1080px, use fixed table layout to prevent horizontal scrollbar - Add column width constraints for profile and proxy tables * feat: redesign profile editor with left nav, advanced config modes, and new fields - Add left-side anchor navigation with scrollspy (IntersectionObserver) - Split Fingerprint into separate Noise and Rendering sections - Replace expansion panel with flat Advanced section layout - Add mode toggles for Executable (kernel/custom), Cookies and Bookmarks (file/input) - Add FPS dropdown (profile/real/custom number) instead of free-text input - Add Save IP button in proxy section for explicit IP saving - Add new fields: Time Seed, Proxy Bypass Regex, Cookies, Bookmarks, Custom Headers - Put username/password on same row in proxy input - Widen status column to prevent icon clipping * fix: replace Save IP with Save to proxy list, unify dropdown option labels * fix: include username and password in proxy duplicate detection Normalize both sides with `|| ''` to handle undefined values, and add password to the comparison so proxies with the same host:port but different credentials are treated as distinct entries. * fix: prevent editing running profiles and remove auto-save IP on proxy check - Block profile editing when browser is running/launching/stopping, show alert dialog instead of silently darkening the background - Remove onIpCheckResult that auto-saved checked IP to proxyIp field * feat(launcher): add automatic self-update with GitHub commit tracking Check for launcher updates on startup and every hour by comparing the local commit hash against the latest launcher-specific commit from GitHub API (path=launcher). When an update is found, silently download, rebuild, and prompt the user to restart. - Add UpdateService with periodic check, ZIP download, and rebuild - Show update status (checking/downloading/building/ready) in sidebar - Display current version (commit hash) at sidebar bottom - Update setup scripts to save commit hash after install/build - Fix sidebar footer layout to stay at bottom via flex container * feat(launcher): improve table UX, add new CLI flags, and proxy input enhancements - Separate row highlight (single click) from checkbox selection (batch ops) - Add double-click to edit on both profile and proxy tables - Add clear proxy button in profile editor - Combine proxy type/host/port into single row layout - Add --bot-stack-seed and --bot-network-info-override CLI flags - Update noise seed from float to integer range - Stack seed uses dropdown (profile/real/custom) like FPS - Rearrange noise settings: seed+scale+timeseed row, fps+stackseed row - Widen proxy type column in proxy list table * fix(launcher): auto-highlight new profiles/proxies and default noise text rects off - Return created profile/proxy ID from dialog so the table highlights the new row after creation, matching the edit-then-highlight behavior - Change botConfigNoiseTextRects default from on to off * feat(launcher): add new CLI flags, clear user data, and unsaved changes guard - Add --bot-config-orientation dropdown in Display & Input section with all orientation variants, noted as mobile profiles only - Add --bot-gpu-emulation toggle in Behavior section for auto GPU rendering backend selection - Add "Clear User Data" action in both profile edit dialog and profile list context menu to delete user-data-dir - Guard profile and proxy edit dialogs against accidental close: Escape, backdrop click, and Cancel now prompt when forms have unsaved changes * feat(launcher): reorganize flag categories, add CLI preview, new 2026-03-26 flags - Restructure sections: Proxy → Proxy & Network, move GPU Emulation to Rendering, History Injection to Identity (now supports count) - Add --bot-enable-variations-in-context and --bot-inject-random-history={number} - Add CLI command preview in Advanced tab + Copy CLI in context menu - Backward compat for old profiles with flags in behavior.* - Fix history count type coercion, add copy snackbar feedback * fix(launcher): prevent kernel update corruption and improve extraction - Use unique directory per install (tagName_githubAssetId) to avoid overwriting running kernel files during auto-update - Verify directory deletion before removing kernel record to prevent orphaned records when files are locked on Windows - Clean up outdated kernels at runtime when browser exits, not only at launcher startup - Check 7z availability early at init and before download, show warning banner when missing - Fix nested 7z/zip extraction: use recursive dir search and trim \r from Windows dir output so chrome.7z is found and cleaned up - Add open-folder button in kernel list to reveal install directory - deleteKernel throws if files are locked instead of silently failing
1 parent b03d7a3 commit 49b447f

5 files changed

Lines changed: 226 additions & 47 deletions

File tree

launcher/src/app/data/kernel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export type KernelPlatform = 'win_x86_64' | 'mac_arm64' | 'mac_x86_64' | 'linux_x86_64' | 'linux_arm64';
22

33
export interface KernelAsset {
4+
id: number; // GitHub asset ID, globally unique
45
name: string;
56
platform: KernelPlatform;
67
downloadUrl: string;

launcher/src/app/kernel-management/kernel-management.component.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@
77
</button>
88
</mat-toolbar>
99

10+
@if (kernelService.isExtractorChecked() && !kernelService.isExtractorAvailable()) {
11+
<div class="error-banner">
12+
<mat-icon>warning</mat-icon>
13+
@if (kernelService.getCurrentPlatform() === 'win_x86_64') {
14+
7-Zip is required to extract kernels. Please install it from
15+
<a href="https://7-zip.org/" target="_blank">7-zip.org</a>
16+
} @else {
17+
dpkg-deb (or ar + tar) is required to extract kernels. Please install dpkg or binutils.
18+
}
19+
</div>
20+
}
21+
1022
@if (error) {
1123
<div class="error-banner">
1224
<mat-icon>error</mat-icon>
@@ -67,6 +79,9 @@
6779
<ng-container matColumnDef="actions">
6880
<th mat-header-cell *matHeaderCellDef></th>
6981
<td mat-cell *matCellDef="let element">
82+
<button mat-icon-button (click)="openInstallDir(element)" matTooltip="Open folder">
83+
<mat-icon>folder_open</mat-icon>
84+
</button>
7085
<button mat-icon-button color="warn" (click)="deleteKernel(element)" matTooltip="Delete">
7186
<mat-icon>delete</mat-icon>
7287
</button>

launcher/src/app/kernel-management/kernel-management.component.ts

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
77
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
88
import { MatToolbarModule } from '@angular/material/toolbar';
99
import { MatTooltipModule } from '@angular/material/tooltip';
10+
import * as Neutralino from '@neutralinojs/lib';
1011
import type { InstalledKernel, KernelRelease } from '../data/kernel';
12+
import { AlertDialogComponent } from '../shared/alert-dialog.component';
1113
import { ConfirmDialogComponent } from '../shared/confirm-dialog.component';
1214
import { KernelService } from '../shared/kernel.service';
1315

@@ -28,7 +30,7 @@ import { KernelService } from '../shared/kernel.service';
2830
styleUrl: './kernel-management.component.scss',
2931
})
3032
export class KernelManagementComponent implements OnInit {
31-
readonly #kernelService = inject(KernelService);
33+
readonly kernelService = inject(KernelService);
3234
readonly #dialog = inject(MatDialog);
3335

3436
readonly displayedColumns = ['version', 'platform', 'installedAt', 'actions'];
@@ -41,36 +43,36 @@ export class KernelManagementComponent implements OnInit {
4143

4244
constructor() {
4345
effect(() => {
44-
const version = this.#kernelService.installedKernelsVersion();
46+
const version = this.kernelService.installedKernelsVersion();
4547
if (version > 0) this.refresh();
4648
});
4749
}
4850

4951
// Check if a specific release is being downloaded
5052
isDownloading(tagName: string): boolean {
51-
return this.#kernelService.isDownloading(tagName);
53+
return this.kernelService.isDownloading(tagName);
5254
}
5355

5456
// Check if there are any active downloads
5557
get hasActiveDownloads(): boolean {
56-
return this.#kernelService.hasActiveDownloads();
58+
return this.kernelService.hasActiveDownloads();
5759
}
5860

5961
// Get all download progresses for display
6062
get downloadProgresses() {
61-
return this.#kernelService.downloadProgresses();
63+
return this.kernelService.downloadProgresses();
6264
}
6365

6466
async ngOnInit(): Promise<void> {
65-
await this.#kernelService.initialize();
67+
await this.kernelService.initialize();
6668
await this.refresh();
6769
await this.loadAvailableReleases();
6870
}
6971

7072
async refresh(): Promise<void> {
7173
this.loading = true;
7274
try {
73-
const kernels = await this.#kernelService.getInstalledKernels();
75+
const kernels = await this.kernelService.getInstalledKernels();
7476
this.dataSource.data = kernels;
7577
} finally {
7678
this.loading = false;
@@ -81,7 +83,7 @@ export class KernelManagementComponent implements OnInit {
8183
this.loadingReleases = true;
8284
this.error = null;
8385
try {
84-
this.availableReleases = await this.#kernelService.fetchAvailableReleases();
86+
this.availableReleases = await this.kernelService.fetchAvailableReleases();
8587
} catch (e) {
8688
this.error = e instanceof Error ? e.message : 'Failed to load releases';
8789
} finally {
@@ -93,21 +95,29 @@ export class KernelManagementComponent implements OnInit {
9395
// Filter by majorVersion - if a major version is installed, don't show it
9496
// (auto-update will handle upgrades to newer releases within the same major version)
9597
const installedMajorVersions = new Set(this.dataSource.data.map((k) => k.majorVersion));
96-
const platform = this.#kernelService.getCurrentPlatform();
98+
const platform = this.kernelService.getCurrentPlatform();
9799
return this.availableReleases.filter(
98100
(r) => !installedMajorVersions.has(r.majorVersion) && r.assets.some((a) => a.platform === platform)
99101
);
100102
}
101103

102104
async downloadKernel(release: KernelRelease): Promise<void> {
103105
try {
104-
await this.#kernelService.downloadAndInstall(release);
106+
await this.kernelService.downloadAndInstall(release);
105107
await this.refresh();
106108
} catch (e) {
107109
this.error = e instanceof Error ? e.message : 'Download failed';
108110
}
109111
}
110112

113+
async openInstallDir(kernel: InstalledKernel): Promise<void> {
114+
try {
115+
await Neutralino.os.open(kernel.installPath);
116+
} catch (error) {
117+
console.error('Failed to open directory:', error);
118+
}
119+
}
120+
111121
deleteKernel(kernel: InstalledKernel): void {
112122
this.#dialog
113123
.open(ConfirmDialogComponent, {
@@ -116,8 +126,14 @@ export class KernelManagementComponent implements OnInit {
116126
.afterClosed()
117127
.subscribe(async (result: boolean) => {
118128
if (!result) return;
119-
await this.#kernelService.deleteKernel(kernel.id);
120-
await this.refresh();
129+
try {
130+
await this.kernelService.deleteKernel(kernel.id);
131+
await this.refresh();
132+
} catch (error) {
133+
this.#dialog.open(AlertDialogComponent, {
134+
data: { message: error instanceof Error ? error.message : 'Failed to delete kernel.' },
135+
});
136+
}
121137
});
122138
}
123139

@@ -134,7 +150,7 @@ export class KernelManagementComponent implements OnInit {
134150
}
135151

136152
getReleaseAssetDate(release: KernelRelease): string | null {
137-
const platform = this.#kernelService.getCurrentPlatform();
153+
const platform = this.kernelService.getCurrentPlatform();
138154
const asset = release.assets.find((a) => a.platform === platform);
139155
return this.formatAssetDate(asset?.assetDate);
140156
}

launcher/src/app/shared/browser-launcher.service.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface RunningInfo {
1818
spawnProcessInfo?: Neutralino.SpawnedProcess;
1919
resolver?: any;
2020
startTime?: number;
21+
kernelId?: string;
2122
}
2223

2324
@Injectable({ providedIn: 'root' })
@@ -53,6 +54,7 @@ export class BrowserLauncherService {
5354
if (!runningInfo) break; // System process (7z, curl, etc.) — not a browser
5455

5556
const uptime = runningInfo.startTime ? Date.now() - runningInfo.startTime : Infinity;
57+
const exitedKernelId = runningInfo.kernelId;
5658
runningInfo.status = BrowserProfileStatus.Idle;
5759
runningInfo.spawnProcessInfo = undefined;
5860

@@ -63,6 +65,11 @@ export class BrowserLauncherService {
6365
},
6466
});
6567
}
68+
69+
// Try to clean up old kernels now that this browser exited
70+
if (exitedKernelId) {
71+
this.#tryCleanupOldKernel(exitedKernelId);
72+
}
6673
}
6774
break;
6875
}
@@ -205,7 +212,7 @@ export class BrowserLauncherService {
205212

206213
const args = this.#buildCliArgs(browserProfile, { botProfilePath, userDataDirPath, diskCacheDirPath });
207214

208-
const runningInfo: RunningInfo = { browserProfileId: browserProfile.id, status: BrowserProfileStatus.Running, startTime: Date.now() };
215+
const runningInfo: RunningInfo = { browserProfileId: browserProfile.id, status: BrowserProfileStatus.Running, startTime: Date.now(), kernelId: kernel?.id };
209216

210217
const warmupUrls = (browserProfile.warmupUrls ?? '').split('\n');
211218
if (!warmupUrls.length) warmup = false;
@@ -408,6 +415,17 @@ export class BrowserLauncherService {
408415
return args;
409416
}
410417

418+
#tryCleanupOldKernel(kernelId: string): void {
419+
// Check if any other running browser is still using this kernel
420+
const stillInUse = Array.from(this.#runningStatuses.values()).some(
421+
(info) => info.kernelId === kernelId && info.status === BrowserProfileStatus.Running
422+
);
423+
if (stillInUse) return;
424+
425+
// Ask kernel service to clean up old versions if this one is outdated
426+
this.#kernelService.cleanupOldKernelIfOutdated(kernelId).catch(console.error);
427+
}
428+
411429
async stop(browserProfile: BrowserProfile): Promise<void> {
412430
if (this.getRunningStatus(browserProfile) !== BrowserProfileStatus.Running) {
413431
throw new Error('The profile is not running');

0 commit comments

Comments
 (0)