Skip to content

Commit

Permalink
Merge branch 'tabarra:develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
Gravxd authored Mar 2, 2025
2 parents 35ab379 + 1ab3c47 commit d277e92
Show file tree
Hide file tree
Showing 160 changed files with 3,296 additions and 2,248 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/publish-tagged.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ jobs:
- name: Download all modules
run: npm ci

# Not truly necessary for build, but for now the vite config requires it
- name: Create .env file
run: |
echo TXDEV_FXSERVER_PATH=$(pwd)/fxserver > .env
echo TXDEV_VITE_URL='http://localhost:40122' >> .env
- name: Build project
run: |
npm run build
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ jobs:
- name: Download all modules
run: npm ci

- name: Create .env file
run: |
echo TXDEV_FXSERVER_PATH=$(pwd)/fxserver > .env
echo TXDEV_VITE_URL='http://localhost:40122' >> .env
- name: Run Tests
env:
CI: true
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ monitor-*.zip
bundle_size_report.html
.github/.cienv
scripts/**/*.local.*
.runtime/

# IDE Specific
.idea
Expand Down
13 changes: 12 additions & 1 deletion core/boot/getHostVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,23 @@ import consts from '@shared/consts';
*/
export const hostEnvVarSchemas = {
//General
API_TOKEN: z.union([
z.literal('disabled'),
z.string().regex(
/^[A-Za-z0-9_-]{16,48}$/,
'Token must be alphanumeric, underscores or dashes only, and between 16 and 48 characters long.'
),
]),
DATA_PATH: z.string().min(1).refine(
(val) => path.isAbsolute(val),
'DATA_PATH must be an absolute path'
),
QUIET_MODE: z.preprocess((val) => val === 'true', z.boolean()),
GAME_NAME: z.enum(
['fivem', 'redm'],
{ message: 'GAME_NAME must be either "gta5", "rdr3", or undefined' }
),
MAX_SLOTS: z.coerce.number().int().positive(),
QUIET_MODE: z.preprocess((val) => val === 'true', z.boolean()),

//Networking
TXA_URL: z.string().url(),
Expand Down
21 changes: 13 additions & 8 deletions core/boot/getNativeVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ const cleanNativeResp = (resp: any) => {
};

//Warning for convar usage
const convarWarning = (convarName: string, newName: string) => {
console.warn(`WARNING: The ConVar '${convarName}' is deprecated and will be removed in the next update.`);
const replacedConvarWarning = (convarName: string, newName: string) => {
console.warn(`WARNING: The '${convarName}' ConVar is deprecated and will be removed in the next update.`);
console.warn(`WARNING: Please use the '${newName}' environment variable instead.`);
console.warn(`WARNING: For more information: https://aka.cfx.re/txadmin-env-config`);
}
Expand All @@ -33,18 +33,23 @@ export const getNativeVars = (ignoreDeprecatedConfigs: boolean) => {
const txaResourceVersion = cleanNativeResp(GetResourceMetadata(resourceName, 'version', 0));
const txaResourcePath = cleanNativeResp(GetResourcePath(resourceName));

//Convars
//Profile Convar - with warning
const txAdminProfile = getConvarString('serverProfile');
if (txAdminProfile) {
console.warn(`WARNING: The 'serverProfile' ConVar is deprecated and will be removed in a future update.`);
console.warn(`WARNING: To create multiple servers, set up a different TXHOST_DATA_PATH instead.`);
console.warn(`WARNING: For more information: https://aka.cfx.re/txadmin-env-config`);
}

//Deprecated convars
//Convars replaced by TXHOST_* env vars
let txDataPath, txAdminPort, txAdminInterface;
if(!ignoreDeprecatedConfigs) {
if (!ignoreDeprecatedConfigs) {
txDataPath = getConvarString('txDataPath');
if (txDataPath) convarWarning('txDataPath', 'TXHOST_DATA_PATH');
if (txDataPath) replacedConvarWarning('txDataPath', 'TXHOST_DATA_PATH');
txAdminPort = getConvarString('txAdminPort');
if (txAdminPort) convarWarning('txAdminPort', 'TXHOST_TXA_PORT');
if (txAdminPort) replacedConvarWarning('txAdminPort', 'TXHOST_TXA_PORT');
txAdminInterface = getConvarString('txAdminInterface');
if (txAdminInterface) convarWarning('txAdminInterface', 'TXHOST_INTERFACE');
if (txAdminInterface) replacedConvarWarning('txAdminInterface', 'TXHOST_INTERFACE');
}

//Final object
Expand Down
22 changes: 8 additions & 14 deletions core/boot/setup.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
const modulename = 'Setup';
import path from 'node:path';
import fs from 'node:fs';

import consoleFactory from '@lib/console';
import fatalError from '@lib/fatalError';
import { txEnv } from '@core/globalData';
import ConfigStore from '@modules/ConfigStore';
import { chalkInversePad } from '@lib/misc';
const console = consoleFactory(modulename);


/**
Expand All @@ -30,37 +27,35 @@ export const ensureProfileStructure = () => {
* Setup the profile folder structure
*/
export const setupProfile = () => {
console.log(console.DIVIDER);
//Create new profile folder
console.log('Creating new profile folder...');
try {
fs.mkdirSync(txEnv.profilePath);
const configStructure = ConfigStore.getEmptyConfigFile();
fs.writeFileSync(
path.join(txEnv.profilePath, 'config.json'),
path.join(txEnv.profilePath, 'config.json'),
JSON.stringify(configStructure, null, 2)
);
ensureProfileStructure();
} catch (error) {
fatalError.Boot(4, [
'Failed to set up folder structure for the new profile.',
'Failed to set up data folder structure.',
['Path', txEnv.profilePath],
], error);
}
console.ok(`Server profile was saved in '${txEnv.profilePath}'`);
console.log(`Server data will be saved in ${chalkInversePad(txEnv.profilePath)}`);

//Saving start.bat (yes, I also wish this didn't exist)
if (txEnv.osType == 'windows') {
const batFilename = `start_${txEnv.fxsVersion}_${txEnv.profile}.bat`;
if (txEnv.isWindows && txEnv.profileName !== 'default') {
const batFilename = `start_${txEnv.fxsVersion}_${txEnv.profileName}.bat`;
try {
const fxsPath = path.join(txEnv.fxServerPath, 'FXServer.exe');
const fxsPath = path.join(txEnv.fxsPath, 'FXServer.exe');
const batLines = [
//TODO: add note to not add any server convars in here
`@echo off`,
`"${fxsPath}" +set serverProfile "${txEnv.profile}"`,
`"${fxsPath}" +set serverProfile "${txEnv.profileName}"`,
`pause`
];
const batFolder = path.resolve(txEnv.fxServerPath, '..');
const batFolder = path.resolve(txEnv.fxsPath, '..');
const batPath = path.join(batFolder, batFilename);
fs.writeFileSync(batPath, batLines.join('\r\n'));
console.ok(`You can use ${chalkInversePad(batPath)} to start this profile.`);
Expand All @@ -69,5 +64,4 @@ export const setupProfile = () => {
console.dir(error);
}
}
console.log(console.DIVIDER);
};
4 changes: 2 additions & 2 deletions core/boot/setupProcessHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ export default function setupProcessHandlers() {

//Handling warnings (ignoring some)
Error.stackTraceLimit = 25;
process.removeAllListeners('warning'); //FIXME: this errors in Bun
process.removeAllListeners('warning'); //FIXME: this causes errors in Bun
process.on('warning', (warning) => {
//totally ignoring the warning, we know this is bad and shouldn't happen
if (warning.name === 'UnhandledPromiseRejectionWarning') return;

if (warning.name !== 'ExperimentalWarning' || txDevEnv.ENABLED) {
console.dir(warning, { multilineError: true });
console.verbose.dir(warning, { multilineError: true });
}
});

Expand Down
18 changes: 9 additions & 9 deletions core/boot/startReadyWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { z } from 'zod';

import got from '@lib/got';
import getOsDistro from '@lib/host/getOsDistro.js';
import { convars, txDevEnv, txEnv } from '@core/globalData';
import { txDevEnv, txEnv, txHostConfig } from '@core/globalData';
import consoleFactory from '@lib/console';
import { addLocalIpAddress } from '@lib/host/isIpAddressLocal';
import { chalkInversePad } from '@lib/misc';
Expand Down Expand Up @@ -40,15 +40,15 @@ const getPublicIp = async () => {

const getOSMessage = async () => {
const serverMessage = [
`To be able to access txAdmin from the internet open port ${convars.txAdminPort}`,
`To be able to access txAdmin from the internet open port ${txHostConfig.txaPort}`,
'on your OS Firewall as well as in the hosting company.',
];
const winWorkstationMessage = [
'[!] Home-hosting fxserver is not recommended [!]',
'You need to open the fxserver port (usually 30120) on Windows Firewall',
'and set up port forwarding on your router so other players can access it.',
];
if (convars.displayAds) {
if (txEnv.displayAds) {
winWorkstationMessage.push('We recommend renting a server from ' + chalk.inverse(' https://zap-hosting.com/txAdmin ') + '.');
}

Expand Down Expand Up @@ -139,8 +139,8 @@ export const startReadyWatcher = async (cb: () => void) => {

//Addresses
let detectedUrls;
if (convars.forceInterface && convars.forceInterface !== '0.0.0.0') {
detectedUrls = [convars.forceInterface];
if (txHostConfig.netInterface && txHostConfig.netInterface !== '0.0.0.0') {
detectedUrls = [txHostConfig.netInterface];
} else {
detectedUrls = [
(txEnv.isWindows) ? 'localhost' : 'your-public-ip',
Expand All @@ -150,9 +150,9 @@ export const startReadyWatcher = async (cb: () => void) => {
addLocalIpAddress(publicIpResp.value);
}
}
const bannerUrls = convars.txAdminUrl
? [convars.txAdminUrl]
: detectedUrls.map((addr) => `http://${addr}:${convars.txAdminPort}/`);
const bannerUrls = txHostConfig.txaUrl
? [txHostConfig.txaUrl]
: detectedUrls.map((addr) => `http://${addr}:${txHostConfig.txaPort}/`);

//Admin PIN
const adminMasterPin = 'value' in adminPinRes && adminPinRes.value ? adminPinRes.value : false;
Expand All @@ -176,7 +176,7 @@ export const startReadyWatcher = async (cb: () => void) => {
...adminPinLines,
];
console.multiline(boxen(boxLines.join('\n'), boxOptions), chalk.bgGreen);
if (!txDevEnv.ENABLED && !convars.forceInterface && 'value' in msgRes && msgRes.value) {
if (!txDevEnv.ENABLED && !txHostConfig.netInterface && 'value' in msgRes && msgRes.value) {
console.multiline(msgRes.value, chalk.bgBlue);
}

Expand Down
2 changes: 1 addition & 1 deletion core/deployer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class Deployer {
this.deployFailed = true;
try {
const filePath = path.join(this.deployPath, '_DEPLOY_FAILED_DO_NOT_USE');
await fse.outputFile(filePath, 'This deploy was failed, please do not use these files.');
await fse.outputFile(filePath, 'This deploy has failed, please do not use these files.');
} catch (error) { }
}

Expand Down
71 changes: 26 additions & 45 deletions core/deployer/utils.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,40 @@
import path from 'node:path';
import fse from 'fs-extra';
import { canWriteToPath, getPathFiles } from '@lib/fs';


/**
* Check if its possible to create a file in a folder
*/
const canCreateFile = async (targetPath: string) => {
try {
const filePath = path.join(targetPath, '.empty');
await fse.outputFile(filePath, '#save_attempt_please_ignore');
await fse.remove(filePath);
return true;
} catch (error) {
return false;
}
};
//File created up to v7.3.2
const EMPTY_FILE_NAME = '.empty';


/**
* Perform deployer local target path permission/emptiness checking
* FIXME: timeout to remove folders, or just autoremove them idk
* Perform deployer local target path permission/emptiness checking.
*/
export const validateTargetPath = async (deployPath: string) => {
if (await fse.pathExists(deployPath)) {
const pathFiles = await fse.readdir(deployPath);
if (pathFiles.some((x) => x !== '.empty')) {
throw new Error('This folder is not empty!');
} else {
if (await canCreateFile(deployPath)) {
return 'Exists, empty, and writtable!';
} else {
throw new Error('Path exists, but its not a folder, or its not writtable.');
}
}
} else {
if (await canCreateFile(deployPath)) {
await fse.remove(deployPath);
return 'Path didn\'t existed, we created one (then deleted it).';
} else {
throw new Error('Path doesn\'t exist, and we could not create it. Please check parent folder permissions.');
const canCreateFolder = await canWriteToPath(deployPath);
if(!canCreateFolder) {
throw new Error('Path is not writable due to missing permissions or invalid path.');
}
try {
const pathFiles = await getPathFiles(deployPath);
if (pathFiles.some((x) => x.name !== EMPTY_FILE_NAME)) {
throw new Error('This folder already exists and is not empty!');
}
} catch (error) {
if ((error as any).code !== 'ENOENT') throw error;
}
return true as const;
};


/**
* Create a template recipe file
*/
export const makeTemplateRecipe = (serverName: string, author: string) => `name: ${serverName}
author: ${author}
# This is just a placeholder, please don't run it!
tasks:
- action: waste_time
seconds: 5
- action: waste_time
seconds: 5
`;
export const makeTemplateRecipe = (serverName: string, author: string) => [
`name: ${serverName}`,
`author: ${author}`,
'',
'# This is just a placeholder, please don\'t use it!',
'tasks: ',
' - action: waste_time',
' seconds: 5',
' - action: waste_time',
' seconds: 5',
].join('\n');
1 change: 1 addition & 0 deletions core/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type RefreshConfigFunc = import('@modules/ConfigStore/').RefreshConfigFunc;
interface GenericTxModuleInstance {
public handleConfigUpdate?: RefreshConfigFunc;
public handleShutdown?: () => void;
public timers?: NodeJS.Timer[];
// public measureMemory?: () => { [key: string]: number };
}
declare interface GenericTxModule<T> {
Expand Down
Loading

0 comments on commit d277e92

Please sign in to comment.