Compare commits

...

20 Commits

Author SHA1 Message Date
49b2a7622b added debug text 2026-09-04 19:51:27 -07:00
b8e474cb92 Fix Mac tar extraction argument parsing 2026-09-04 19:35:21 -07:00
8b29c9e3c7 Fix Mac Node architecture and resources bundle 2026-09-04 19:30:40 -07:00
5dab61b359 Fix release installer path detection 2026-09-04 19:17:05 -07:00
eab843aa16 Detect and upload macOS DMG bundle 2026-09-04 19:10:21 -07:00
b757f741b7 Fix release log object and multi-platform extension replacement 2026-09-04 19:04:10 -07:00
5f4c47a07c Handle duplicate release asset links 2026-09-04 18:58:51 -07:00
32677c9fb2 Fix Tauri window builder for macOS 2026-09-04 18:53:56 -07:00
7eab57cfd9 Fix Rust PATH and lock release version 2026-09-04 18:50:17 -07:00
c8fc7da64d Configure free Mac cloud workflow 2026-09-04 18:47:30 -07:00
88c1824bf7 removed the gitlab yml 2026-09-04 18:45:22 -07:00
a50cb2b6b8 Fix Windows instance type 2026-09-04 18:45:15 -07:00
971eae4af1 Add Windows and macOS Codemagic workflows 2026-09-04 18:44:28 -07:00
1400d538f3 support mac user test 2026-09-04 18:32:40 -07:00
b1d01d94da v1.1.13 2026-09-04 14:38:43 -07:00
9d7e218ff6 v1.1.12 2026-09-01 20:20:53 -07:00
c19e1e2cf7 fixed downloading update ( sorry lmfao ) 2026-09-01 20:14:28 -07:00
6b06bc8170 v1.1.11 2026-09-01 19:45:46 -07:00
769c0dadcd possibly fixed the auto updater, and fixed the node processes closing 2026-09-01 19:39:27 -07:00
414ef73e1d v1.1.10 2026-09-01 13:41:06 -07:00
13 changed files with 354 additions and 124 deletions

View File

@@ -18,20 +18,23 @@ const REPO_CONFIG = {
'stream-api': { name: 'stream-api', branch: 'main' }, 'stream-api': { name: 'stream-api', branch: 'main' },
}; };
const clonedRepos = new Set();
function clonePrivateRepo(repoConfig, targetDir) { function clonePrivateRepo(repoConfig, targetDir) {
if (fs.existsSync(targetDir)) {
console.log(`[DEV] ${repoConfig.name} already exists locally. Skipping clone to use local changes.`);
return;
}
const token = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN; const token = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN;
const auth = token ? `oauth2:${token}@` : ''; const auth = token ? `oauth2:${token}@` : '';
const repoUrl = `https://${auth}gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`; const repoUrl = `https://${auth}gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`;
try { try {
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
}
execSync(`git clone --depth 1 --branch ${repoConfig.branch} ${repoUrl} ${targetDir}`, { execSync(`git clone --depth 1 --branch ${repoConfig.branch} ${repoUrl} ${targetDir}`, {
stdio: 'inherit' stdio: 'inherit'
}); });
clonedRepos.add(repoConfig.name);
} catch (error) { } catch (error) {
throw new Error(`Failed to clone ${repoConfig.name}: ${error.message}`); throw new Error(`Failed to clone ${repoConfig.name}: ${error.message}`);
} }
@@ -332,9 +335,11 @@ async function build() {
fs.writeFileSync(path.join(__dirname, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8'); fs.writeFileSync(path.join(__dirname, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
for (const repo of REPOS) { for (const repo of REPOS) {
const repoPath = path.join(__dirname, repo); if (clonedRepos.has(repo)) {
if (fs.existsSync(repoPath)) { const repoPath = path.join(__dirname, repo);
fs.rmSync(repoPath, { recursive: true, force: true }); if (fs.existsSync(repoPath)) {
fs.rmSync(repoPath, { recursive: true, force: true });
}
} }
} }

32
codemagic.yaml Normal file
View File

@@ -0,0 +1,32 @@
workflows:
macos-release:
name: Build macOS Release
max_build_duration: 60
instance_type: mac_mini_m2
environment:
groups:
- gitlab_credentials
node: 22.14.0
cache:
cache_paths:
- $HOME/.cargo/registry
- $HOME/.cargo/git
- src-tauri/target
scripts:
- name: Setup Rust toolchain
script: |
if ! command -v cargo &> /dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
fi
- name: Install dependencies
script: |
export PATH="$HOME/.cargo/bin:$PATH"
npm install
- name: Build & Publish macOS Release
script: |
export PATH="$HOME/.cargo/bin:$PATH"
PACKAGE_VERSION=$(node -p "require('./package.json').version")
node release.js --installer --tag="v$PACKAGE_VERSION"
artifacts:
- src-tauri/target/release/bundle/dmg/*.dmg
- src-tauri/target/release/bundle/macos/*.app

81
main.js
View File

@@ -84,11 +84,20 @@ cmdApp.get('/__tauri_cmd/:cmd', (req, res) => {
}); });
cmdApp.listen(4892, '127.0.0.1', () => debugLog("[CMD-SERVER] Dedicated command server listening on 127.0.0.1:4892")); cmdApp.listen(4892, '127.0.0.1', () => debugLog("[CMD-SERVER] Dedicated command server listening on 127.0.0.1:4892"));
process.stdin.resume();
process.stdin.on('end', () => {
debugLog('stdin closed - parent died. Self-terminating...');
process.exit(0);
});
process.stdin.on('error', () => {
process.exit(0);
});
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
debugLog(`CRITICAL UNCAUGHT EXCEPTION: ${err.stack || err.message}`); debugLog(`CRITICAL UNCAUGHT EXCEPTION: ${err.stack || err.message}`);
if (err.code === 'EPIPE') { if (err.code === 'EPIPE' || err.code === 'EOF') {
debugLog('Pipe to parent closed - parent process gone, exiting'); debugLog('Pipe to parent closed - parent process gone, exiting');
process.exit(1); process.exit(0);
} }
try { sendToTauri('error', { message: err.message }); } catch (e) { } try { sendToTauri('error', { message: err.message }); } catch (e) { }
}); });
@@ -126,6 +135,9 @@ async function start() {
const installBuild = async (release) => { const installBuild = async (release) => {
const tempZipPath = path.join(APPDATA_DIR, 'download-temp.zip'); const tempZipPath = path.join(APPDATA_DIR, 'download-temp.zip');
if (fs.existsSync(tempZipPath)) {
try { fs.unlinkSync(tempZipPath); } catch (e) { }
}
let attempts = 0; let attempts = 0;
let downloadSuccess = false; let downloadSuccess = false;
@@ -135,9 +147,13 @@ async function start() {
debugLog(`Attempting to download update (Attempt ${attempts})...`); debugLog(`Attempting to download update (Attempt ${attempts})...`);
sendToTauri('status', { message: `Connecting to download servers (Attempt ${attempts}/3)...` }); sendToTauri('status', { message: `Connecting to download servers (Attempt ${attempts}/3)...` });
let lastPercent = -1;
await updateClient.downloadUpdate(release, tempZipPath, (downloaded, total) => { await updateClient.downloadUpdate(release, tempZipPath, (downloaded, total) => {
const percent = total > 0 ? Math.round((downloaded / total) * 100) : 0; const percent = total > 0 ? Math.round((downloaded / total) * 100) : 0;
sendToTauri('download-progress', { downloaded, total, percent }); if (percent !== lastPercent) {
lastPercent = percent;
sendToTauri('download-progress', { downloaded, total, percent });
}
}); });
debugLog('Download complete. Verifying checksum...'); debugLog('Download complete. Verifying checksum...');
@@ -187,10 +203,34 @@ async function start() {
try { try {
sendToTauri('status', { message: 'Inspecting local workspace and installed files...' }); sendToTauri('status', { message: 'Inspecting local workspace and installed files...' });
const isFirstRun = !patcher.verifyExtraction(); let isFirstRun = !patcher.verifyExtraction();
const localVersion = patcher.getCurrentVersion(); let localVersion = patcher.getCurrentVersion();
const localSha = patcher.getCurrentSha(); let localSha = patcher.getCurrentSha();
debugLog(`Is First Run: ${isFirstRun}, Local Version: ${localVersion}`); debugLog(`Is First Run: ${isFirstRun}, Local Version: ${localVersion}, Local SHA: ${localSha}`);
const localZipPath = path.join(__dirname, 'update-payload.zip');
const localManifestPath = path.join(__dirname, 'manifest.json');
let localZipManifest = null;
if (fs.existsSync(localZipPath) && fs.existsSync(localManifestPath)) {
try { localZipManifest = JSON.parse(fs.readFileSync(localManifestPath, 'utf8')); } catch (e) { }
}
if (localZipManifest && (isFirstRun || localZipManifest.sha256 !== localSha)) {
debugLog("New local payload detected! Applying local build...");
sendToTauri('status', { message: 'Extracting local development payload...' });
patcher.purgeOldCode();
patcher.extractZipPayload(localZipPath);
patcher.applyPatches(getReplacementsConfig());
patcher.bridgeCredentials(serviceManager.credentialManager);
patcher.saveCurrentVersion(localZipManifest.version, localZipManifest.sha256);
serviceManager.envConfig = getEnvConfig();
isFirstRun = false;
localVersion = localZipManifest.version;
localSha = localZipManifest.sha256;
debugLog("Local payload applied successfully.");
}
sendToTauri('status', { message: 'Connecting to update servers...' }); sendToTauri('status', { message: 'Connecting to update servers...' });
let release = null; let release = null;
@@ -261,7 +301,14 @@ async function start() {
patcher.extractZipPayload(localZip); patcher.extractZipPayload(localZip);
patcher.applyPatches(getReplacementsConfig()); patcher.applyPatches(getReplacementsConfig());
patcher.bridgeCredentials(serviceManager.credentialManager); patcher.bridgeCredentials(serviceManager.credentialManager);
patcher.saveCurrentVersion("1.0.0");
let ver = "1.0.0";
try {
const man = JSON.parse(fs.readFileSync(path.join(__dirname, 'manifest.json'), 'utf8'));
ver = man.version || ver;
} catch (e) { }
patcher.saveCurrentVersion(ver);
serviceManager.envConfig = getEnvConfig(); serviceManager.envConfig = getEnvConfig();
debugLog("Local payload extracted successfully."); debugLog("Local payload extracted successfully.");
} else { } else {
@@ -367,17 +414,23 @@ process.on('exit', () => {
if (serviceManager) { if (serviceManager) {
for (const name of Object.keys(serviceManager.processes)) { for (const name of Object.keys(serviceManager.processes)) {
const proc = serviceManager.processes[name]; const proc = serviceManager.processes[name];
if (proc && proc.child) { if (proc && proc.child && proc.child.pid) {
try { proc.child.kill('SIGKILL'); } catch (e) { } try {
if (process.platform === 'win32') {
require('child_process').spawnSync('taskkill', ['/F', '/T', '/PID', proc.child.pid.toString()], { stdio: 'ignore', windowsHide: true });
} else {
process.kill(-proc.child.pid, 'SIGKILL');
}
} catch (e) { }
} }
} }
} }
}); });
process.on('SIGINT', async () => { process.on('SIGINT', () => {
if (expressServer) expressServer.close(); process.exit(0);
if (playerExpressServer) playerExpressServer.close(); });
if (serviceManager) await serviceManager.stopAll(); process.on('SIGTERM', () => {
process.exit(0); process.exit(0);
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "vyla-home", "name": "vyla-home",
"version": "1.1.9", "version": "1.1.13",
"description": "Vyla Home Desktop App Launcher", "description": "Vyla Home Desktop App Launcher",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View File

@@ -63,17 +63,24 @@ class Patcher {
const fullPath = path.join(this.appDataPath, dir); const fullPath = path.join(this.appDataPath, dir);
if (fs.existsSync(fullPath)) { if (fs.existsSync(fullPath)) {
try { try {
fs.rmSync(fullPath, { recursive: true, force: true }); const trashPath = path.join(this.appDataPath, `.trash_${dir}_${Date.now()}`);
fs.renameSync(fullPath, trashPath);
fs.rmSync(trashPath, { recursive: true, force: true });
} catch (e) { } catch (e) {
try { try {
const trashPath = fullPath + '.trash.' + Date.now(); fs.rmSync(fullPath, { recursive: true, force: true });
fs.renameSync(fullPath, trashPath); } catch (err2) { }
} catch (renameErr) {
console.warn(`Could not remove or rename ${fullPath}. It will be overwritten.`);
}
} }
} }
} }
try {
const files = fs.readdirSync(this.appDataPath);
for (const file of files) {
if (file.startsWith('.trash_')) {
fs.rmSync(path.join(this.appDataPath, file), { recursive: true, force: true });
}
}
} catch (e) { }
} }
extractZipPayload(zipPath) { extractZipPayload(zipPath) {

View File

@@ -78,12 +78,12 @@ function uploadFileToGitLabPackage(filePath, packageName, version, fileName) {
} }
function createGitLabRelease(tag, releaseLinks) { function createGitLabRelease(tag, releaseLinks) {
const sendRequest = (method, path, data = null) => { const sendRequest = (method, reqPath, data = null) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const options = { const options = {
hostname: 'gitlab.com', hostname: 'gitlab.com',
port: 443, port: 443,
path: path, path: reqPath,
method: method, method: method,
headers: { headers: {
'PRIVATE-TOKEN': TOKEN, 'PRIVATE-TOKEN': TOKEN,
@@ -101,19 +101,9 @@ function createGitLabRelease(tag, releaseLinks) {
res.on('data', chunk => body += chunk); res.on('data', chunk => body += chunk);
res.on('end', () => { res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) { if (res.statusCode >= 200 && res.statusCode < 300) {
try { try { resolve(body ? JSON.parse(body) : {}); } catch (e) { resolve({}); }
resolve(body ? JSON.parse(body) : {});
} catch (e) {
resolve({});
}
} else if (res.statusCode === 409 && method === 'POST') {
console.log(`Release ${tag} already exists, deleting and recreating...`);
sendRequest('DELETE', `/api/v4/projects/${PROJECT_ID}/releases/${encodeURIComponent(tag)}`)
.then(() => sendRequest('POST', `/api/v4/projects/${PROJECT_ID}/releases`, data))
.then(resolve)
.catch(reject);
} else { } else {
reject(new Error(`Failed to publish release (HTTP ${res.statusCode}): ${body}`)); reject(new Error(`HTTP ${res.statusCode}: ${body}`));
} }
}); });
}); });
@@ -133,13 +123,37 @@ function createGitLabRelease(tag, releaseLinks) {
name: `Vyla Home Release ${tag}`, name: `Vyla Home Release ${tag}`,
tag_name: tag, tag_name: tag,
ref: currentRef, ref: currentRef,
description: `Automated local release ${tag}`, description: `Automated Multi-Platform Release ${tag}`,
assets: { assets: {
links: releaseLinks links: releaseLinks
} }
}; };
return sendRequest('POST', `/api/v4/projects/${PROJECT_ID}/releases`, releaseData); return sendRequest('POST', `/api/v4/projects/${PROJECT_ID}/releases`, releaseData)
.catch(err => {
if (err.message.includes('HTTP 409')) {
console.log(`[CI Sync] Release ${tag} already exists. Appending our OS-specific files to it...`);
let promiseChain = Promise.resolve();
for (const link of releaseLinks) {
promiseChain = promiseChain.then(() => {
return sendRequest('POST', `/api/v4/projects/${PROJECT_ID}/releases/${encodeURIComponent(tag)}/assets/links`, {
name: link.name,
url: link.url,
link_type: link.link_type
}).catch(e => {
if (e.message.includes('already been taken') || e.message.includes('HTTP 409') || e.message.includes('HTTP 400')) {
console.log(`[CI Sync] Asset link '${link.name}' is already attached. Skipping.`);
return Promise.resolve();
}
throw e;
});
});
}
return promiseChain.then(() => ({ name: `Vyla Home Release ${tag}`, tag_name: tag }));
}
throw err;
});
} }
async function main() { async function main() {
@@ -166,7 +180,6 @@ async function main() {
if (isFullBuild) { if (isFullBuild) {
console.log('Ensuring bundled Node.js runtime...'); console.log('Ensuring bundled Node.js runtime...');
await ensureBundledNode(); await ensureBundledNode();
await ensureWebView2Loader(); await ensureWebView2Loader();
const bundleDir = path.join(__dirname, 'src-tauri', 'target', 'release', 'bundle'); const bundleDir = path.join(__dirname, 'src-tauri', 'target', 'release', 'bundle');
@@ -174,20 +187,32 @@ async function main() {
fs.rmSync(bundleDir, { recursive: true, force: true }); fs.rmSync(bundleDir, { recursive: true, force: true });
} }
console.log('Compiling Windows Tauri installer locally...'); console.log('Compiling native Tauri application...');
execSync('npm run build', { execSync('npm run build', { stdio: 'inherit' });
stdio: 'inherit'
});
const nsisDir = path.join(__dirname, 'src-tauri/target/release/bundle/nsis'); const targetVersion = version.startsWith('v') ? version.substring(1) : version;
if (fs.existsSync(nsisDir)) {
const targetVersion = version.startsWith('v') ? version.substring(1) : version;
const files = fs.readdirSync(nsisDir).filter(f => f.endsWith('.exe') && f.includes(targetVersion));
if (files.length > 0) { if (process.platform === 'win32') {
const originalName = files[0]; const nsisDir = path.join(__dirname, 'src-tauri', 'target', 'release', 'bundle', 'nsis');
installerPath = path.join(nsisDir, originalName); if (fs.existsSync(nsisDir)) {
installerUploadName = originalName.replace(/\s+/g, '-'); const files = fs.readdirSync(nsisDir).filter(f => f.endsWith('.exe') && f.includes(targetVersion));
if (files.length > 0) {
installerPath = path.join(nsisDir, files[0]);
installerUploadName = files[0].replace(/\s+/g, '-');
}
}
} else {
const dmgDir = path.join(__dirname, 'src-tauri', 'target', 'release', 'bundle', 'dmg');
console.log(`Checking for Mac installer in: ${dmgDir}`);
if (fs.existsSync(dmgDir)) {
const files = fs.readdirSync(dmgDir).filter(f => f.endsWith('.dmg') && f.includes(targetVersion));
if (files.length > 0) {
installerPath = path.join(dmgDir, files[0]);
installerUploadName = files[0].replace(/\s+/g, '-');
console.log(`Found Mac installer: ${installerUploadName}`);
} else {
console.log(`[WARNING] No .dmg found containing version ${targetVersion}`);
}
} }
} }
} }
@@ -208,19 +233,25 @@ async function main() {
if (installerPath && installerUploadName) { if (installerPath && installerUploadName) {
console.log(`Uploading installer ${installerUploadName}...`); console.log(`Uploading installer ${installerUploadName}...`);
const installerUnique = installerUploadName.replace('.exe', `-${timestamp}.exe`); const ext = path.extname(installerUploadName);
const installerUnique = installerUploadName.replace(ext, `-${timestamp}${ext}`);
const installerUrl = await uploadFileToGitLabPackage(installerPath, 'vyla-home', version, installerUnique); const installerUrl = await uploadFileToGitLabPackage(installerPath, 'vyla-home', version, installerUnique);
releaseLinks.push({ name: installerUploadName, url: installerUrl, link_type: 'package' }); releaseLinks.push({ name: installerUploadName, url: installerUrl, link_type: 'package' });
} else {
console.log(`[WARNING] No installer was selected for upload! (isFullBuild: ${isFullBuild})`);
} }
console.log('[3/4] Publishing GitLab Release...'); console.log('[3/4] Publishing GitLab Release...');
const release = await createGitLabRelease(version, releaseLinks); await createGitLabRelease(version, releaseLinks);
console.log(`[4/4] Release successfully created: ${release.name} (${release.tag_name})`); console.log(`[4/4] Release process successfully completed for ${version}!`);
} }
function ensureBundledNode() { function ensureBundledNode() {
const os = require('os');
const binDir = path.join(__dirname, 'bin'); const binDir = path.join(__dirname, 'bin');
const nodeExePath = path.join(binDir, 'node.exe'); const isWin = os.platform() === 'win32';
const nodeExeName = isWin ? 'node.exe' : 'node';
const nodeExePath = path.join(binDir, nodeExeName);
const nodeVersion = 'v22.14.0'; const nodeVersion = 'v22.14.0';
const versionMarker = path.join(binDir, `node-${nodeVersion}.marker`); const versionMarker = path.join(binDir, `node-${nodeVersion}.marker`);
@@ -232,35 +263,58 @@ function ensureBundledNode() {
fs.mkdirSync(binDir, { recursive: true }); fs.mkdirSync(binDir, { recursive: true });
console.log(`Downloading standalone Node.js ${nodeVersion} runtime for bundling...`); console.log(`Downloading standalone Node.js ${nodeVersion} runtime for bundling...`);
const nodeUrl = `https://nodejs.org/dist/${nodeVersion}/win-x64/node.exe`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(nodeExePath); const download = (url, dest, cb) => {
https.get(nodeUrl, (res) => { https.get(url, (res) => {
if (res.statusCode === 302 || res.statusCode === 301) { if (res.statusCode === 302 || res.statusCode === 301) {
https.get(res.headers.location, (redirectRes) => { return download(res.headers.location, dest, cb);
redirectRes.pipe(fileStream); }
fileStream.on('finish', () => { const fileStream = fs.createWriteStream(dest);
fileStream.close(); res.pipe(fileStream);
fs.writeFileSync(versionMarker, nodeVersion, 'utf8'); fileStream.on('finish', () => {
resolve(); fileStream.close();
}); cb();
}).on('error', reject); });
return; }).on('error', reject);
} };
res.pipe(fileStream);
fileStream.on('finish', () => { if (isWin) {
fileStream.close(); const nodeUrl = `https://nodejs.org/dist/${nodeVersion}/win-x64/node.exe`;
download(nodeUrl, nodeExePath, () => {
fs.writeFileSync(versionMarker, nodeVersion, 'utf8'); fs.writeFileSync(versionMarker, nodeVersion, 'utf8');
resolve(); resolve();
}); });
}).on('error', (err) => { } else {
try { fs.unlinkSync(nodeExePath); } catch (e) { } const arch = os.arch() === 'arm64' ? 'arm64' : 'x64';
reject(err); const platform = os.platform() === 'darwin' ? 'darwin' : 'linux';
}); const tarName = `node-${nodeVersion}-${platform}-${arch}`;
const nodeUrl = `https://nodejs.org/dist/${nodeVersion}/${tarName}.tar.gz`;
const tarPath = path.join(binDir, 'node.tar.gz');
download(nodeUrl, tarPath, () => {
try {
execSync(`tar -xzf node.tar.gz ${tarName}/bin/node`, { cwd: binDir });
fs.renameSync(path.join(binDir, tarName, 'bin', 'node'), path.join(binDir, 'node'));
fs.rmSync(path.join(binDir, tarName), { recursive: true, force: true });
fs.unlinkSync(tarPath);
execSync(`chmod +x node`, { cwd: binDir });
fs.writeFileSync(versionMarker, nodeVersion, 'utf8');
resolve();
} catch (e) {
reject(e);
}
});
}
}); });
} }
function ensureWebView2Loader() { function ensureWebView2Loader() {
const os = require('os');
if (os.platform() !== 'win32') return Promise.resolve();
const releaseDir = path.join(__dirname, 'src-tauri', 'target', 'release'); const releaseDir = path.join(__dirname, 'src-tauri', 'target', 'release');
const rootDllPath = path.join(__dirname, 'src-tauri', 'WebView2Loader.dll'); const rootDllPath = path.join(__dirname, 'src-tauri', 'WebView2Loader.dll');
fs.mkdirSync(releaseDir, { recursive: true }); fs.mkdirSync(releaseDir, { recursive: true });

2
src-tauri/Cargo.lock generated
View File

@@ -4222,7 +4222,7 @@ dependencies = [
[[package]] [[package]]
name = "vyla-home" name = "vyla-home"
version = "1.1.9" version = "1.1.13"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"serde", "serde",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "vyla-home" name = "vyla-home"
version = "1.1.9" version = "1.1.13"
description = "Vyla Home Desktop App Launcher" description = "Vyla Home Desktop App Launcher"
edition = "2021" edition = "2021"

View File

@@ -1,5 +1,20 @@
fn main() { fn main() {
println!("cargo:rerun-if-changed=src/main.rs"); println!("cargo:rerun-if-changed=src/main.rs");
println!("cargo:rerun-if-changed=tauri.conf.json"); println!("cargo:rerun-if-changed=tauri.conf.json");
if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
if let (Ok(manifest_dir), Ok(out_dir)) = (std::env::var("CARGO_MANIFEST_DIR"), std::env::var("OUT_DIR")) {
let mut target_dir = std::path::PathBuf::from(out_dir);
target_dir.pop();
target_dir.pop();
target_dir.pop();
let dll_src = std::path::Path::new(&manifest_dir).join("WebView2Loader.dll");
let dll_dest = target_dir.join("WebView2Loader.dll");
if dll_src.exists() {
let _ = std::fs::copy(dll_src, dll_dest);
}
}
}
tauri_build::build(); tauri_build::build();
} }

View File

@@ -7,26 +7,19 @@ use tauri::{Emitter, Manager, State, WebviewUrl, WebviewWindowBuilder};
fn debug_log(msg: &str) { fn debug_log(msg: &str) {
let log_dir = if let Ok(app_data) = std::env::var("APPDATA") { let log_dir = if let Ok(app_data) = std::env::var("APPDATA") {
std::path::PathBuf::from(app_data) std::path::PathBuf::from(app_data).join("vyla-home").join("vyla-home-files").join("logs")
.join("vyla-home") } else if let Ok(home) = std::env::var("HOME") {
.join("vyla-home-files") std::path::PathBuf::from(home).join("Library").join("Logs").join("VylaHome")
.join("logs")
} else if let Ok(user_profile) = std::env::var("USERPROFILE") { } else if let Ok(user_profile) = std::env::var("USERPROFILE") {
std::path::PathBuf::from(user_profile) std::path::PathBuf::from(user_profile).join(".vyla-home").join("logs")
.join(".vyla-home")
.join("logs")
} else { } else {
std::path::PathBuf::from("logs") std::env::temp_dir().join("vyla-home-logs")
}; };
let _ = std::fs::create_dir_all(&log_dir); let _ = std::fs::create_dir_all(&log_dir);
let log_path = log_dir.join("rust-debug.log"); let log_path = log_dir.join("rust-debug.log");
if let Ok(mut file) = std::fs::OpenOptions::new() if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&log_path) {
.create(true)
.append(true)
.open(&log_path)
{
use std::io::Write; use std::io::Write;
let _ = writeln!(file, "[RUST] {}", msg); let _ = writeln!(file, "[RUST] {}", msg);
} }
@@ -50,6 +43,15 @@ fn clean_path(path: &std::path::Path) -> String {
} }
fn kill_child_process(child: &mut Child) { fn kill_child_process(child: &mut Child) {
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let _ = Command::new("taskkill")
.args(&["/F", "/T", "/PID", &child.id().to_string()])
.creation_flags(CREATE_NO_WINDOW)
.output();
}
let _ = child.kill(); let _ = child.kill();
let _ = child.wait(); let _ = child.wait();
} }
@@ -482,6 +484,30 @@ fn open_new_window(app: tauri::AppHandle, url: String) -> Result<(), String> {
} }
} }
#[tauri::command]
async fn kill_orphans() -> Result<(), String> {
debug_log("[KILL] User requested orphan process kill");
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let _ = std::process::Command::new("taskkill")
.args(&["/F", "/T", "/IM", "node.exe"])
.creation_flags(CREATE_NO_WINDOW)
.output();
}
#[cfg(not(windows))]
{
let _ = std::process::Command::new("pkill")
.args(&["-9", "-f", "node"])
.output();
}
Ok(())
}
#[tauri::command] #[tauri::command]
fn get_logo_base64(app: tauri::AppHandle) -> Result<String, String> { fn get_logo_base64(app: tauri::AppHandle) -> Result<String, String> {
use std::io::Read; use std::io::Read;
@@ -528,7 +554,8 @@ fn main() {
win_close, win_close,
win_drag, win_drag,
open_new_window, open_new_window,
get_logo_base64 get_logo_base64,
kill_orphans
]) ])
.manage(NodeServices(Mutex::new(None))) .manage(NodeServices(Mutex::new(None)))
.setup(|app| { .setup(|app| {
@@ -536,11 +563,45 @@ fn main() {
let app_data = app.path().app_data_dir().unwrap_or_default(); let app_data = app.path().app_data_dir().unwrap_or_default();
let app_data_str = clean_path(&app_data); let app_data_str = clean_path(&app_data);
let vyla_files = if let Ok(appdata_env) = std::env::var("APPDATA") {
std::path::PathBuf::from(appdata_env).join("vyla-home").join("vyla-home-files")
} else {
app_data.join("vyla-home-files")
};
let version_file = vyla_files.join("version.json");
let current_app_version = app.package_info().version.to_string();
let mut version_mismatch = false;
if version_file.exists() {
if let Ok(content) = std::fs::read_to_string(&version_file) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(version) = json.get("version").and_then(|v| v.as_str()) {
if version != current_app_version {
version_mismatch = true;
}
} else {
version_mismatch = true;
}
} else {
version_mismatch = true;
}
}
}
if version_mismatch {
debug_log(&format!("App version updated to {}. Cleaning old files.", current_app_version));
let dirs_to_remove = ["frontend", "player", "stream-api", "auth-proxy", "config"];
for d in dirs_to_remove {
let _ = std::fs::remove_dir_all(vyla_files.join(d));
}
let _ = std::fs::remove_file(&version_file);
}
let window = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into())) let window = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
.title("Vyla Home") .title("Vyla Home")
.inner_size(1280.0, 800.0) .inner_size(1280.0, 800.0)
.decorations(false) .decorations(false)
.transparent(false)
.resizable(true) .resizable(true)
.devtools(cfg!(debug_assertions)) .devtools(cfg!(debug_assertions))
.initialization_script("window.__VYLA_WINDOW_LABEL = 'main';") .initialization_script("window.__VYLA_WINDOW_LABEL = 'main';")
@@ -566,6 +627,7 @@ fn main() {
.current_dir(&root_dir) .current_dir(&root_dir)
.env("APPDATA_PATH", &app_data_str) .env("APPDATA_PATH", &app_data_str)
.env("NODE_EXE", &node_bin) .env("NODE_EXE", &node_bin)
.stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()); .stderr(Stdio::piped());
@@ -583,11 +645,15 @@ fn main() {
child.id() child.id()
)); ));
if let Some(stderr) = child.stderr.take() { if let Some(stderr) = child.stderr.take() {
let w = window.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
let reader = BufReader::new(stderr); let reader = BufReader::new(stderr);
for line in reader.lines().flatten() { for line in reader.lines().flatten() {
debug_log(&format!("[NODE STDERR] {}", line)); debug_log(&format!("[NODE STDERR] {}", line));
if line.contains("Error") || line.contains("Exception") || line.contains("MODULE_NOT_FOUND") {
let _ = w.emit("error", serde_json::json!({ "message": format!("Backend failed: {}", line) }));
}
} }
}); });
} }

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Vyla Home", "productName": "Vyla Home",
"version": "1.1.9", "version": "1.1.13",
"identifier": "com.vyla.home", "identifier": "com.vyla.home",
"build": { "build": {
"frontendDist": "../ui" "frontendDist": "../ui"
@@ -29,8 +29,9 @@
}, },
"resources": [ "resources": [
"WebView2Loader.dll", "WebView2Loader.dll",
"../bin/*", "../bin/**/*",
"../main.js", "../main.js",
"../preload.js",
"../service-manager.js", "../service-manager.js",
"../patcher.js", "../patcher.js",
"../crypto-utils.js", "../crypto-utils.js",
@@ -40,4 +41,4 @@
"../node_modules/**/*" "../node_modules/**/*"
] ]
} }
} }

View File

@@ -195,6 +195,9 @@
<div class="loader-container"> <div class="loader-container">
<div class="loader-card"> <div class="loader-card">
<div class="status-text" id="status-message">Starting runtime engine...</div> <div class="status-text" id="status-message">Starting runtime engine...</div>
<div id="debug-log-box"
style="display: none; width: 100%; max-height: 180px; overflow-y: auto; background: rgba(255, 50, 50, 0.12); border: 1px solid rgba(255, 100, 100, 0.3); border-radius: 8px; padding: 12px; font-family: monospace; font-size: 12px; color: #fca5a5; text-align: left; white-space: pre-wrap; word-break: break-all; margin-bottom: 20px; user-select: text;">
</div>
<div class="progress-track" id="progress-track" style="display: none;"> <div class="progress-track" id="progress-track" style="display: none;">
<div class="progress-fill" id="progress-fill"></div> <div class="progress-fill" id="progress-fill"></div>
</div> </div>
@@ -256,23 +259,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<div id="view-error" class="view-state">
<div class="container">
<svg class="icon icon-error" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="7" x2="12" y2="13" />
<circle cx="12" cy="17" r="1" />
</svg>
<h1>Couldn't start Vyla Home</h1>
<p id="error-msg"></p>
<div class="buttons">
<button class="primary" onclick="location.reload()">Try Again</button>
</div>
</div> </div>
</div> </div>
@@ -516,6 +503,7 @@ void main() {
<script> <script>
const statusEl = document.getElementById("status-message"); const statusEl = document.getElementById("status-message");
const debugBox = document.getElementById("debug-log-box");
const progressTrack = document.getElementById("progress-track"); const progressTrack = document.getElementById("progress-track");
const progressFill = document.getElementById("progress-fill"); const progressFill = document.getElementById("progress-fill");
const actionsEl = document.getElementById("update-actions"); const actionsEl = document.getElementById("update-actions");
@@ -540,6 +528,12 @@ void main() {
}); });
} }
function showDebug(msg) {
if (!debugBox) return;
debugBox.style.display = "block";
debugBox.textContent = msg;
}
async function initTauriEvents() { async function initTauriEvents() {
const eventApi = window.__TAURI__?.event || window.__TAURI_INTERNALS__; const eventApi = window.__TAURI__?.event || window.__TAURI_INTERNALS__;
if (!eventApi || typeof eventApi.listen !== 'function') { if (!eventApi || typeof eventApi.listen !== 'function') {
@@ -560,7 +554,9 @@ void main() {
if (progressTrack) progressTrack.style.display = "block"; if (progressTrack) progressTrack.style.display = "block";
const percent = data.percent || 0; const percent = data.percent || 0;
if (progressFill) progressFill.style.width = percent + "%"; if (progressFill) progressFill.style.width = percent + "%";
setStatus(`Downloading update... ${percent}%`); if (!currentStatusText.startsWith("Downloading update...")) {
setStatus("Downloading update...");
}
} }
}); });
@@ -582,9 +578,9 @@ void main() {
eventApi.listen("error", (e) => { eventApi.listen("error", (e) => {
const data = e.payload || e; const data = e.payload || e;
if (data && data.message) { const msg = data && data.message ? data.message : JSON.stringify(data);
setStatus(`Error: ${data.message}`, "#ef4444"); setStatus("Runtime Error", "#fca5a5");
} showDebug(msg);
}); });
} }

View File

@@ -228,7 +228,8 @@ class UpdateClient {
} }
async checkForUpdates() { async checkForUpdates() {
const releases = await this.fetchJson(RELEASES_API); const cacheBuster = `?cb=${Date.now()}`;
const releases = await this.fetchJson(RELEASES_API + cacheBuster);
if (!releases || releases.length === 0) { if (!releases || releases.length === 0) {
return null; return null;
} }