Skip to content

Instantly share code, notes, and snippets.

@lselden
Last active November 6, 2023 21:01
Show Gist options
  • Save lselden/1e5c394a95828e42d07bc26da5e87070 to your computer and use it in GitHub Desktop.
Save lselden/1e5c394a95828e42d07bc26da5e87070 to your computer and use it in GitHub Desktop.
you don't need node-windows! Here's an example of running a vercel/pkg packaged node executable as a windows service by wrapping winsw
// DISCLAIMER! Use at own risk, no guarantees, etc. This sample code is adapted from existing code without code review/testing
import fs from 'fs';
import path_ from 'path';
import { promisify } from 'util';
import { execFile } from 'child_process';
export const isPkg = !!process.pkg;
// make pkg keep the executable
// this is done to force pkg to detect as asset (looks for "path.join", but typescript renames)
// of course, make sure winsw.exe actually exists in this path
const path = path_;
path.join(__dirname, './bin/winsw.exe');
// may or may not be necessary depending on version of winsw.exe
path.join(__dirname, './bin/winsw.config');
// pkg doesn't support the fs.promises.access, so using old style
async function exists(filepath: string) {
return new Promise(done => {
fs.access(filepath, fs.constants.R_OK, err => done(!err));
});
}
// pkg has issues with copyFile
async function copy(source: string, destination: string) {
if (!isPkg) {
return fs.promises.copyFile(source, destination);
}
const buf = fs.readFileSync(source);
fs.writeFileSync(destination, buf);
}
async function findWinSW() {
const testDirectories = [
process.cwd(),
path.dirname(process.execPath),
path.join(process.cwd(), 'bin'),
path.join(__dirname, 'bin'),
'.',
path.join('.', 'bin')
];
// let set by environment variable
const envPath = process.env["NODE_WINSW_PATH"];
if (envPath && await exists(envPath)) return envPath;
for (let base of testDirectories) {
const exePath = path.join(base, 'winsw.exe');
if (await exists(exePath)) return exePath;
}
throw new Error(`Cannot find winsw.exe. To specify manually set the NODE_WINSW_PATH environment variable, or download from https://github.com/winsw/winsw and put in ${process.cwd()}`);
}
export async function writeServiceWrapperToDisk(installPath = process.cwd(), opts = {id: 'service-executable-name', name: 'service name', desc: 'service description'}) {
// copy service exectuable (renamed winsw.exe) to path with sidcar config files next to them
const basePath = path.join(installPath, opts.id);
const wraperExecutable = `${basePath}.exe`;
// create winsw xml config
const serviceConfig = `<service>
<id>${opts.id}.exe</id>
<name>${opts.name}</name>
<description>${opts.desc}</description>
<executable>${process.execPath}</executable>
<logmode>rotate</logmode>
<logpath>${path.join(process.cwd(), `${basePath}.stdout`)}</logpath>
<argument>--sample-argument</argument>
<argument>argument-value</argument>
<stoptimeout>30sec</stoptimeout>
<env name="SAMPLE_ENV_KEY" value="SAMPLE_ENV_VALUE"/>
<workingdirectory>${process.cwd()}</workingdirectory>
</service>`;
// write config xml
await fs.promises.writeFile(`${basePath}.xml`, serviceConfig);
// find path to winsw (asset in pkg or on disk)
const winswPath = await findWinSW();
await copy(winswPath, wraperExecutable);
// copy sidecard config file if necessary
if (await exists(`${winswPath}.config`)) await copy(`${winswPath}.config`, `${basePath}.exe.config`);
// run installer
const promise = promisify(execFile)(wraperExecutable, ['install'], { cwd: installPath, shell: true });
const { stderr, stdout } = await promise;
const { child: { exitCode }} = promise;
console.log(stdout);
if (exitCode !== 0) {
console.warn('Install failed', stderr);
}
return { stdout, stderr, exitCode };
}
export async function uninstallService(installPath = process.cwd(), opts = {id: 'service-executable-name'}) {
const basePath = path.join(installPath, opts.id);
const wraperExecutable = `${basePath}.exe`;
// run uninstaller
const promise = promisify(execFile)(wraperExecutable, ['uninstall'], { cwd: installPath, shell: true });
const { stderr, stdout } = await promise;
const { child: { exitCode }} = promise;
console.log(stdout);
if (exitCode !== 0) {
console.warn('Uninstall failed', stderr);
}
// delete associated files - quick way to delete log files + sidecars
const listing = await fs.promises.readdir(installPath, { withFileTypes: true });
const files = listing
.filter(file => file.isFile() && file.name.startsWith(opts.id))
.map(file => path.join(installPath, file.name));
for (let file of files) {
await fs.promises.unlink(file)
.catch(err => console.warn(`Cannot delete file ${file}`, err));
}
return { stdout, stderr, exitCode };
}
export async function addServiceStopHook(onStop = () => { }) {
// add signal inturrupt handler, so process can stop when windows service's stop is called
process.on('SIGINT', async () => {
let returnCode = await Promise.resolve(onStop())
.catch(err => {
console.error(err);
return parseInt(err.code, 10) || 1;
});
// give a bit of extra time to make sure everything's cleaned up
await new Promise(done => setTimeout(done, 500));
process.exit(returnCode);
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment