Last active
November 6, 2023 21:01
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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