Skip to content

Update utility #267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 2, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
/trakman/
/tmf-config/
/tracks/
/.hashes.json

# Bun
bun.lockb
9 changes: 5 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ WORKDIR /app/server
COPY --chown=1001:1001 ./config ./trakmanbk/config
COPY --chown=1001:1001 ./plugins ./trakmanbk/plugins
COPY --chown=1001:1001 ./src ./trakmanbk/src
COPY --chown=1001:1001 ./Plugins.ts ./package.json ./tsconfig.json ./CHANGELOG.md ./trakmanbk/
COPY --chown=1001:1001 ./Plugins.ts ./package.json ./tsconfig.json ./CHANGELOG.md ./Update.js ./trakmanbk/
COPY --chown=1001:1001 --chmod=0755 ./docker_run.sh ./docker_run.sh
# download dedicated server and remove unnecessary files
RUN wget -O serv.zip http://files2.trackmaniaforever.com/TrackmaniaServer_2011-02-21.zip && \
if [ "$(sha256sum serv.zip | cut -d ' ' -f 1)" != "dd021f49c3d58d45ee09f333015bdb35b19243a38fa65f202ca8a88fb6550c0b" ]; then exit 1; fi && \
unzip serv.zip && \
rm -r serv.zip CommandLine.html ListCallbacks.html ListMethods.html \
Readme_Dedicated.html RemoteControlExamples TrackmaniaServer.exe manialink_dedicatedserver.txt
# get trakman dependencies and build
# generate file hashes, get trakman dependencies and build
WORKDIR /app/server/trakmanbk
RUN npm i && \
RUN node Update.js && \
npm i && \
npm run build
WORKDIR /app/server
# backup important files to prevent them being deleted by mounting the volume
Expand All @@ -37,4 +38,4 @@ VOLUME /app/server/trakman
VOLUME /app/server/.pm2/logs
# set user back to root to be able to chown volumes in the run script
USER root
CMD ["/app/server/docker_run.sh"]
ENTRYPOINT ["/app/server/docker_run.sh"]
157 changes: 157 additions & 0 deletions Update.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Update script (mostly) for docker purposes, if you use git, just do `git pull` instead
// Usage: to perform update: `node Update.js <new version's .hashes.json>`
// To generate hashes for files currently present: `node Update.js`
// Exits with code 0 if successful, code 1 if an error occurred, code 2 if there are conflicts
import fs from 'node:fs/promises'
import crypto from 'node:crypto'
import path from 'node:path'

if (process.argv.length < 3) {
const hashes = JSON.stringify(Object.fromEntries(await generateHashes()))
console.log('Writing hashes to file...')
try {
await fs.writeFile('.hashes.json', hashes, { encoding: 'utf-8' })
console.log('Hashes written.')
process.exit()
} catch(e) {
console.log('Failed to write hashes to file.')
console.log(e)
process.exit(1)
}
}

try {
const newHashes = new Map(Object.entries(JSON.parse(await fs.readFile(process.argv[2], { encoding: 'utf-8' }))))
const fromPath = path.dirname(process.argv[2])
try {
const oldHashes = new Map(Object.entries(JSON.parse(await fs.readFile('.hashes.json', { encoding: 'utf-8' }))))
if (oldHashes.has('__generated__') && oldHashes.get('__generated__') > newHashes.get('__generated__')) {
console.log('Local version newer than update, skipping...')
process.exit()
}
await doUpdate(fromPath, newHashes, oldHashes)
} catch(e) {
console.log('.hashes.json file does not exist.')
await doUpdate(fromPath, newHashes)
}
} catch(e) {
console.log('Failed to update Trakman.')
console.log('Usage: node Update.js <new version\'s hashes.json>')
console.log(e)
process.exit(1)
}

async function hashFile(path) {
const hash = crypto.createHash('md5')
hash.setEncoding('base64')
try {
const f = await fs.readFile(path, { encoding: 'utf-8' })
hash.write(f)
hash.end()
return hash.read()
} catch(e) {
console.log('Cannot read file ' + path)
return ''
}
}

async function generateHashes() {
console.log('Generating hashes for current files...')
const entries = (await fs.readdir('./src', { recursive: true, withFileTypes: true }))
.concat(await fs.readdir('./plugins', { recursive: true, withFileTypes: true }))
.concat(await fs.readdir('./config', { recursive: true, withFileTypes: true }))

const res = new Map(
await Promise.all(entries.filter(a => a.isFile() && (a.name.endsWith('.js') || a.name.endsWith('.ts')))
.map(async (a) => {
const p = path.join(a.parentPath, a.name)
return [p, await hashFile(p)]
})))
for (const name of ['Plugins.ts', 'package.json', 'tsconfig.json', 'CHANGELOG.md', 'Update.js']) {
res.set(name, await hashFile(name))
}
res.set('__generated__', Date.now())
console.log('Hashes generated.')
return res
}

async function doUpdate(fromPath, newHashes, oldHashes = null) {
const currHashes = await generateHashes()
const conflicts = []
let errorOccurred = false
let updatePerformed = false
for (const file of newHashes.keys()) {
if (file === '__generated__') {
continue
}
const newHash = newHashes.get(file)
const fullFromPath = path.join(fromPath, file)
try {
// NICE NOT AT ALL CONFUSING IF STATEMENTS!!!
if (currHashes.has(file) && currHashes.get(file) !== newHash && currHashes.get(file) !== '') {
if (oldHashes !== null && oldHashes.has(file)) {
if (oldHashes.get(file) === newHash) {
// no update since only the user's file is different (X Y X)
continue
} else if (oldHashes.get(file) === currHashes.get(file)) {
// normal update since user didn't alter the file (X X Y)
updatePerformed = true
await fs.copyFile(fullFromPath, file)
continue
}
}
// copy the new file with the .new extension since the user modified the file, and it might have been updated (X Y Z)
conflicts.push(file)
updatePerformed = true
await fs.copyFile(fullFromPath, file + '.new')
} else if (!currHashes.has(file) || currHashes.get(file) === '') {
// the file exists in the new version and not in the old
// make sure the directory exists
updatePerformed = true
await fs.mkdir(path.dirname(file), { recursive: true })
await fs.copyFile(fullFromPath, file)
}
// otherwise the file is already what it needs to be, no need to update it
} catch(e) {
console.log('Could not copy file ' + file)
console.log(e)
errorOccurred = true
}
}
// copy new hashes so you only have to update once
try {
await fs.copyFile(process.argv[2], '.hashes.json')
} catch(e) {
console.log('Failed to copy new hashes.')
console.log(e)
process.exit(1)
}
if (errorOccurred) {
console.log('Errors occurred during update, exiting...')
process.exit(1)
}
if (conflicts.length > 0) {
console.log('!!! Update conflicts, make sure to fix them by comparing your files to files ending in .new !!!')
console.log('If you do not fix these conflicts, the controller might fail to start or crash.')
console.log('Affected files: ')
let log = 'Update did not succeed because of conflicts. Please merge the following files manually:'
conflicts.forEach(name => {
console.log(name)
log += '\n' + name
})
console.log('_____________________________________')
try {
await fs.writeFile('update.log', log, { encoding: 'utf-8' })
} catch(e) {
console.log('Failed to write update log.')
console.log(e)
}
process.exit(2)
}
if (updatePerformed) {
console.log('Update successful.')
} else {
console.log('Update not necessary.')
}
process.exit()
}
20 changes: 15 additions & 5 deletions docker_run.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#!/bin/sh
# Do **NOT** run this script locally,
# it is meant to only be used in the provided docker environment.
# it is meant to only be used in the provided Docker environment.

# create and copy dedicated config
if find /app/server/GameData/Config -mindepth 1 -maxdepth 1 | read; then
echo 'Server config exists, skipping initial setup.'
rm dedicated_cfg.txt.bk
else
echo 'Setting up server...'
# cool and ugly xml replacement
# ugly xml replacement
xml ed -L -u "/dedicated/authorization_levels/level[name='SuperAdmin']/password" -v "$SUPER_ADMIN_PASSWORD" dedicated_cfg.txt.bk
xml ed -L -u "/dedicated/authorization_levels/level[name='SuperAdmin']/name" -v "$SUPER_ADMIN_NAME" dedicated_cfg.txt.bk
ADMIN_PASS=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c"${1:-32}";echo;)
Expand All @@ -32,13 +32,23 @@ else
echo 'Setting up tracks...'
mv /app/server/Tracksbk/* /app/server/GameData/Tracks/
fi
# copy over trakman directory
# update and copy over Trakman directory
if find /app/server/trakman -mindepth 1 -maxdepth 1 | read; then
echo 'Trakman exists, skipping initial setup.'
echo 'Trakman exists. Attempting update...'
cd trakman || exit
node Update.js /app/server/trakmanbk/.hashes.json
if [ $? -gt 0 ]; then
chown server:server update.log
echo 'Update not fully successful, please stop the container.'
sleep 1m # wait a minute for user to read the message, or to realise something's wrong
exit
fi
cd ..
rm -r trakmanbk
else
echo 'Setting up trakman...'
echo 'Setting up Trakman...'
mv /app/server/trakmanbk/* /app/server/trakman/
mv /app/server/trakmanbk/.hashes.json /app/server/trakman/
fi
# ugly creating of files to be able to chmod and remove them later
mkdir -p trakman/logs
Expand Down