Skip to content

Commit

Permalink
Develop (#3)
Browse files Browse the repository at this point in the history
* set up tests

* checkpt; axios throws a regression error (#3219)

* Lower axios version (see #1)

* finish startup routine

* remove unused dependencies

* setup npm publish pipeline

Co-authored-by: Stanislaw Hüll <stanislaw.huell@js-soft.com>
  • Loading branch information
slavistan and slavistan committed Nov 29, 2022
1 parent 20c587c commit cbc02c3
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 38 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
on:
push:
branches:
- main
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.12.1
- run: npm install
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
19 changes: 0 additions & 19 deletions .github/workflows/release-win10-x64.yml

This file was deleted.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@
"main": "dist/main.js",
"types": "dist/main.d.ts",
"scripts": {
"test": "npm run test:build && npx mocha --timeout 3000 './dist-test/tests/**/*.test.js'",
"test": "npm run test:clean && npm run test:build && npx mocha --timeout 3000 './dist-test/tests/**/*.test.js'",
"test:build": "npx tsc -p ./tests/tsconfig.json",
"test:debug": "npm run test:build && NODE_OPTIONS=--inspect-brk npx mocha --timeout 9999999 './dist-test/tests/**/*.test.js'",
"test:debug": "npm run test:clean && npm run test:build && NODE_OPTIONS=--inspect-brk npx mocha --timeout 9999999 './dist-test/tests/**/*.test.js'",
"test:clean": "rm -rf ./dist-test",
"build": "npx tsc -p ./tsconfig.json",
"start": "node ./dist/main.js",
"clean": "rm -rf ./dist"
},
"dependencies": {
"@types/express": "^4.17.14",
"@types/node": "^18.11.9",
"axios": "~1.1.3",
"express": "^4.18.2",
"neverthrow": "^5.1.0",
"typescript": "^4.9.3"
},
"prettier": {
Expand Down
35 changes: 35 additions & 0 deletions src/dss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { AxiosResponse } from "axios"
import { Utility } from "./utility"

export namespace Dss {
/*
* Checks whether the DSS API is accessible at the specified ip/hostname
* and port by querying the DSS's default browser API.
*
* If 'waitSeconds' is set, this request is repeated until the set time has
* passed or a reply is received from the server.
*/
export async function isOnline(
baseUrl: string,
options?: {
waitSeconds?: number
}
): Promise<boolean> {
const waitSeconds = options?.waitSeconds ? options.waitSeconds : 0
const start = new Date().getTime() // returns unix seconds

const config = {
url: baseUrl,
method: "GET",
timeout: 3000
}
do {
const httpReqRes = await Utility.httpReq(config)
if (httpReqRes.isOk()) {
return true
}
Utility.sleepms(1000)
} while ((new Date().getTime() - start) / 1000 < waitSeconds)
return false
}
}
53 changes: 44 additions & 9 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
import express from "express";
import { Settings } from "./settings"
import { Dss } from "./dss"
import express from "express"
import http from "http"
import https from "https"

const app = express();
const port = process.env.LOCAL_MODULE_PORT ? process.env.LOCAL_MODULE_PORT : 2048;
async function main() {
/* Parse application settings. */
const settingsRes = Settings.parseApplicationSettings()
if (settingsRes.isErr()) {
console.error(settingsRes.error.message)
process.exit(1)
}
const settings = settingsRes.value

app.get("/", (_req, res) => {
res.send(`Local module v${process.env.npm_package_version}`);
});
/* Wait for DSS startup to fininsh. */
const wait = 360
const isOnline = await Dss.isOnline(settings.dssBaseUrl, { waitSeconds: wait })
if (isOnline) {
console.log("DSS responded. Starting HTTP server.")
} else {
console.error(`Could not reach DSS after ${wait} seconds. Abort.`)
process.exit(1)
}

app.listen(port, () => {
console.log(`Listening on port ${port}.`);
});
/* Start http server. */
const app = express()
app.get("/", (_req, res) => {
res.send(`Local module v${process.env.npm_package_version}`)
})

let protocol = "http"
if (settings.localModuleUseHttps) {
protocol += "s"
}
const initCallback = () => {
console.log(`Listening on ${protocol}://${settings.localModuleIp}:${settings.localModulePort}.`)
}
if (settings.localModuleUseHttps) {
// NOTE: This code path is currently inactive.
https.createServer(app).listen(settings.localModulePort, settings.localModuleIp, initCallback)
} else {
http.createServer(app).listen(settings.localModulePort, settings.localModuleIp, initCallback)
}
}

main()
115 changes: 115 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import fs from "fs"
import url from "url"
import { ok, err, Result } from "neverthrow"
import { Utility } from "./utility"

export namespace Settings {
export namespace Errors {
export class InvalidSettings extends Error {
public constructor(message: string) {
super(message)
}
}

export class MissingEnvvar extends InvalidSettings {
public envvar: string
public constructor(envvar: string, messageAppendix?: string) {
let message = `Mandatory environment variable '${envvar}' is unset.`
if (messageAppendix !== undefined) {
message += ` ${messageAppendix}`
}
super(message)
this.envvar = envvar
}
}

export class InvalidEnvvarValue extends InvalidSettings {
public envvar: string
public value: string
public constructor(envvar: string, value: string, messageAppendix?: string) {
let message = `Mandatory environment variable '${envvar}' has invalid value '${value}'.`
if (messageAppendix !== undefined) {
message += ` ${messageAppendix}`
}
super(message)
this.envvar = envvar
this.value = value
}
}
}

/* Export names of envvars for testing purposes.. */
export const localModulePortEnvvar = "WP07_LOCAL_MODULE_PORT"
export const dssBaseUrlEnvvar = "WP07_DSS_BASE_URL"

export interface IApplicationSettings {
/* Local module configuration. Parsed from the 'WP07_LOCAL_MODULE_PORT'
* envvar. */
localModuleUseHttps: boolean
localModuleIp: string // hostname or ip
localModulePort: number

/* Base url of the DSS API. Parsed from the 'WP07_DSS_BASE_URL' envvar. */
dssBaseUrl: string // always has a trailing slash
}

export function parseApplicationSettings(env = process.env): Result<IApplicationSettings, Errors.MissingEnvvar | Errors.InvalidEnvvarValue | Error> {
/* Merge environment with .env file, if provided. Existing envvars are
* not overwritten by the contents of the .env file. */
if (fs.existsSync(".env")) {
const parseRes = Utility.parseKeyValueFile(".env")
if (parseRes.isErr()) {
return err(parseRes.error)
}
env = { ...parseRes.value, ...env }
}

/* Validate local module settings. */
if (env[localModulePortEnvvar] == undefined) {
return err(new Errors.MissingEnvvar(localModulePortEnvvar))
}
const localModulePort = Number(env[localModulePortEnvvar])
if (!Utility.isValidPort(localModulePort)) {
return err(new Errors.InvalidEnvvarValue(localModulePortEnvvar, env[localModulePortEnvvar]))
}

/* Destructure DSS base url and validate its fields. We assert that
* the protocol is http/https and no additional fields outside of
* protocol, host and path are specified by the url.
*
* NOTE: The protocol parsed from a url by the urllib always
* contains a trailing colon. */
if (env[dssBaseUrlEnvvar] == undefined) {
return err(new Errors.MissingEnvvar(dssBaseUrlEnvvar))
}

try {
const urlStruct = new url.URL(env[dssBaseUrlEnvvar] as string)
if (!["http:", "https:"].includes(urlStruct.protocol)) {
return err(new Errors.InvalidEnvvarValue(dssBaseUrlEnvvar, env[dssBaseUrlEnvvar], `Only 'http' and 'https' are supported.`))
} else if (urlStruct.port !== "" && !Utility.isValidPort(Number(urlStruct.port))) {
/* If the default port for http or https is used, the .port
* field is empty. We thus have to process this special case. */
return err(new Errors.InvalidEnvvarValue(dssBaseUrlEnvvar, env[dssBaseUrlEnvvar], `Invalid port: '${urlStruct.port}'.`))
} else if (urlStruct.username !== "" || urlStruct.password !== "" || urlStruct.search !== "" || urlStruct.hash !== "") {
throw new Error()
}
} catch (error: unknown) {
return err(new Errors.InvalidEnvvarValue(dssBaseUrlEnvvar, env[dssBaseUrlEnvvar]))
}

/* Append a trailing slash to the base url for consistency. */
let dssBaseUrl = env[dssBaseUrlEnvvar]
if (dssBaseUrl[dssBaseUrl.length - 1] !== "/") {
dssBaseUrl += "/"
}

const result: IApplicationSettings = {
localModuleUseHttps: false,
localModuleIp: "localhost",
localModulePort: localModulePort,
dssBaseUrl: env[dssBaseUrlEnvvar]
}
return ok(result)
}
}
71 changes: 71 additions & 0 deletions src/utility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import fs from "fs"
import util from "util"
import { ok, err, Result } from "neverthrow"
import { AxiosRequestConfig, AxiosResponse, AxiosError } from "axios"
import axios from "axios"

export namespace Utility {
/*
* Neverthrow axios wrapper.
*/
export async function httpReq(config: AxiosRequestConfig): Promise<Result<AxiosResponse, AxiosError | Error>> {
try {
const response = await axios(config)
return ok(response)
} catch (error: unknown) {
if (error instanceof AxiosError || error instanceof Error) {
return err(error)
}
return err(new Error("An unhandled error occurred."))
}
}

export async function sleepms(ms: number) {
return util.promisify(setTimeout)(ms)
}

/*
* Checks whether 'port' is a valid port, i.e. an integer in (0, 65535].
*/
export function isValidPort(port: number) {
if (isNaN(port) || port <= 0 || 65536 <= port || Math.floor(port) !== port) {
return false
}
return true
}

/*
* Parses a file of utf-8 encoded, single-line key=value pairs. Lines
* starting with a literal '#' character are ignored, as well as lines not
* containing a literal '='. The first literal '=' character is used to
* split key from value.
*
* Both linux ('\n') and windows ('\r\n') EOL characters are supported.
*
* This serves as a simple 'dotenv' replacement. 'dotenv' necessarily
* overrides process.env, whereas for testing purposes it is preferable to
* pass around explicit objects. Merging the parsed kv-pairs with
* process.env can be done in a separate, trivial step.
*/
export function parseKeyValueFile(path: string): Result<Record<string, string>, Error> {
let lines: string[]
try {
lines = fs.readFileSync(path, "utf-8").split(/\r?\n/)
} catch (error: unknown) {
return err(new Error(`Error reading file '${path}'`))
}

const result: Record<string, string> = {}
for (const line of lines) {
const n = line.search("=")
if (line.length === 0 || line[0] === "#" || n === -1 || n === 0) {
continue
}
const key = line.slice(0, n)
const value = line.slice(n + 1)
result[key] = value
}

return ok(result)
}
}
8 changes: 0 additions & 8 deletions tests/dummy.test.ts

This file was deleted.

0 comments on commit cbc02c3

Please sign in to comment.