diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ffca305 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,69 @@ +# Version control +.git +.gitignore + +# Node.js +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.npm + +# Development files +.dockerignore +Dockerfile* +docker-compose*.yml +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE and editor files +.vscode +.idea +*.swp +*.swo +*~ +.nova + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage + +# Documentation +docs/ +readme.md +*.md + +# Config and temporary files +config.js +*.socket +attic + +# Test files +test/ +tests/ +*.test.js +*.spec.js + +# Build artifacts +dist/ +build/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a63cc73 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:22-alpine +WORKDIR /usr/src/app +RUN apk add --no-cache python3 make g++ + +# Copy package.json and package-lock.json first to leverage caching +COPY package*.json ./ +RUN npm install + +# Copy the rest of the application files +COPY . . + +ENTRYPOINT ["node", "./bin/tileblaster.js"] +CMD [] \ No newline at end of file diff --git a/bin/tileblaster.js b/bin/tileblaster.js index cc63b6f..91aebef 100755 --- a/bin/tileblaster.js +++ b/bin/tileblaster.js @@ -19,6 +19,19 @@ if (require.main === module) { function fork(n){ worker = cluster.fork({ ...process.env, workerid: n }); debug.info("Started Worker #%d".bold.white, n); + + // Handle messages from workers + worker.on('message', function(message) { + if (typeof message === 'object' && message.type === 'fatal-error') { + debug.error("Worker #%d reported fatal error: %s".bold.red, n, message.message); + if (message.error === 'EADDRINUSE') { + debug.error("Address already in use - initiating complete shutdown".bold.red); + shutdown(); + return; + } + } + }); + worker.on('disconnect', function(){ debug.warn("Disconnect from Worker #%d".bold.white, n); if (shuttingdown) { // remove worker from pool diff --git a/lib/purge.js b/lib/purge.js index 0b4414b..bfec0ef 100644 --- a/lib/purge.js +++ b/lib/purge.js @@ -84,20 +84,31 @@ if (worker.isMainThread) { Promise.allSettled(caches.map(function(cache){ return new Promise(function(resolve,reject){ - let deletable = []; - - klaw(cache.dir).on("data", function(file){ - if (file.stats.mtimeMs > cache.expires) deletable.push(file.path); - }).on("end", function(){ - Promise.allSettled(deletable.map(function(file){ - return new Promise(function(resolve,reject){ - fs.unlink(file, function(err){ - if (err) return reject(err); - purged++; - resolve(); + // check if directory exists before trying to walk it + fs.access(cache.dir, fs.constants.F_OK, function(err){ + if (err) { + // directory doesn't exist, nothing to purge + return resolve(); + } + + let deletable = []; + + klaw(cache.dir).on("data", function(file){ + if (file.stats.mtimeMs > cache.expires) deletable.push(file.path); + }).on("end", function(){ + Promise.allSettled(deletable.map(function(file){ + return new Promise(function(resolve,reject){ + fs.unlink(file, function(err){ + if (err) return reject(err); + purged++; + resolve(); + }); }); - }); - })).then(resolve).catch(reject); + })).then(resolve).catch(reject); + }).on("error", function(err){ + // handle klaw errors gracefully + reject(err); + }); }); }); })).then(function(){ diff --git a/readme.md b/readme.md index 03e2333..7126f04 100644 --- a/readme.md +++ b/readme.md @@ -40,6 +40,10 @@ if you're allowed to do so. * `-v` `--verbose` - enable debug output * `-q` `--quiet` - disable debug output +## Docker usage +`docker build . -t tileblaster` +`docker run -v :/usr/src/app/config.js tileblaster -c ./config.js -v` + ## Configuration See [Configuration](docs/config.md) and [Examples](docs/examples.md) diff --git a/tileblaster.js b/tileblaster.js index 6b67271..e9bff39 100644 --- a/tileblaster.js +++ b/tileblaster.js @@ -434,10 +434,30 @@ tileblaster.prototype.listen = function(router){ router.call(self.router, ...arguments); }); + // Handle server errors (like EADDRINUSE) before they become unhandled + server.on('error', function(err) { + debug.error("Server error:", err); + if (err.code === 'EADDRINUSE') { + debug.error("Address already in use, starting shutdown process"); + // Notify main process about fatal error before shutting down + if (process.send) { + process.send({ type: 'fatal-error', error: 'EADDRINUSE', message: 'Address already in use' }); + } + self.shutdown(); + } + }); + if (listen.port) { server.listen(listen.port, listen.host, function(err){ - if (err) return debug.error("listen: ERROR binding port '%s:%d':", listen.host, listen.port, err); + if (err) { + debug.error("listen: ERROR binding port '%s:%d':", listen.host, listen.port, err); + if (err.code === 'EADDRINUSE') { + debug.error("Port %d is already in use, starting shutdown process", listen.port); + self.shutdown(); + } + return; + } debug.info("Listening on '%s:%d'", listen.host, listen.port); self.servers.push(server); }); @@ -459,7 +479,14 @@ tileblaster.prototype.listen = function(router){ if (err && err.code !== "ENOENT") return debug.error("Deleting socket '%s':", listen.socket, err); server.listen(listen.socket, function(err) { - if (err) return debug.error("Binding to socket '%s':", listen.socket, err); + if (err) { + debug.error("Binding to socket '%s':", listen.socket, err); + if (err.code === 'EADDRINUSE') { + debug.error("Socket %s is already in use, starting shutdown process", listen.socket); + self.shutdown(); + } + return; + } // store socket path server.socket = listen.socket;