From bf9ed01adf414c4f0bf4ff7cc2be61bb80a85b11 Mon Sep 17 00:00:00 2001 From: shmck Date: Wed, 17 Nov 2021 20:23:18 -0800 Subject: [PATCH 1/5] fallback to file Signed-off-by: shmck --- src/environment.ts | 3 +++ src/services/node/index.ts | 10 +++++++++- src/services/storage/index.ts | 21 ++++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/environment.ts b/src/environment.ts index 59077d61..49475422 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -46,3 +46,6 @@ export const CONTENT_SECURITY_POLICY_EXEMPTIONS: string | null = // optional token for authorization/authentication of webhook calls export const WEBHOOK_TOKEN = process.env.CODEROAD_WEBHOOK_TOKEN || null + +// a path to write session state to a file. Useful for maintaining session across containers +export const SESSION_FILE_PATH = process.env.CODEROAD_SESSION_FILE_PATH || null diff --git a/src/services/node/index.ts b/src/services/node/index.ts index a90dd208..e4dc1f7d 100644 --- a/src/services/node/index.ts +++ b/src/services/node/index.ts @@ -7,6 +7,7 @@ import { WORKSPACE_ROOT } from '../../environment' const asyncExec = promisify(cpExec) const asyncRemoveFile = promisify(fs.unlink) const asyncReadFile = promisify(fs.readFile) +const asyncWriteFile = promisify(fs.writeFile) interface ExecParams { command: string @@ -27,5 +28,12 @@ export const removeFile = (...paths: string[]) => { } export const readFile = (...paths: string[]) => { - return asyncReadFile(join(...paths)) + return asyncReadFile(join(...paths), 'utf8') +} + +export const writeFile = (data: any, ...paths: string[]) => { + const filePath = join(...paths) + return asyncWriteFile(filePath, JSON.stringify(data)).catch((err) => { + console.error(`Failed to write to ${filePath}`) + }) } diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index 8a64cceb..aab047f8 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -1,4 +1,6 @@ import * as vscode from 'vscode' +import { readFile, writeFile } from '../node' +import { SESSION_FILE_PATH } from '../../environment' // NOTE: localStorage is not available on client // and must be stored in editor @@ -19,6 +21,18 @@ class Storage { const value: string | undefined = await this.storage.get(this.key) if (value) { return JSON.parse(value) + } else if (SESSION_FILE_PATH) { + // optionally read from file as a fallback to localstorage + const sessionFile = await readFile(SESSION_FILE_PATH) + try { + const session = JSON.parse(sessionFile) + if (session && session[this.key]) { + // TODO: validate session + return session[this.key] + } + } catch (err) { + console.error(`Failed to parse session file: ${SESSION_FILE_PATH}`) + } } return this.defaultValue } @@ -32,7 +46,12 @@ class Storage { ...current, ...value, }) - this.storage.update(this.key, next) + this.storage.update(this.key, next).then(() => { + // optionally write to file + if (SESSION_FILE_PATH) { + writeFile(this.storage, SESSION_FILE_PATH) + } + }) } public reset = () => { this.set(this.defaultValue) From 1e81199435704be332049008a3e9f0580283a706 Mon Sep 17 00:00:00 2001 From: shmck Date: Sat, 20 Nov 2021 20:44:38 -0800 Subject: [PATCH 2/5] handle session keys in file Signed-off-by: shmck --- src/services/node/index.ts | 22 +++++++++++++------- src/services/storage/index.ts | 39 +++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/services/node/index.ts b/src/services/node/index.ts index e4dc1f7d..92504100 100644 --- a/src/services/node/index.ts +++ b/src/services/node/index.ts @@ -14,26 +14,34 @@ interface ExecParams { dir?: string } +// correct paths to be from workspace root rather than extension folder +const getWorkspacePath = (...paths: string[]) => { + return join(WORKSPACE_ROOT, ...paths) +} + export const exec = (params: ExecParams): Promise<{ stdout: string; stderr: string }> | never => { const cwd = join(WORKSPACE_ROOT, params.dir || '') return asyncExec(params.command, { cwd }) } export const exists = (...paths: string[]): boolean | never => { - return fs.existsSync(join(WORKSPACE_ROOT, ...paths)) + return fs.existsSync(getWorkspacePath(...paths)) } export const removeFile = (...paths: string[]) => { - return asyncRemoveFile(join(WORKSPACE_ROOT, ...paths)) + return asyncRemoveFile(getWorkspacePath(...paths)) } -export const readFile = (...paths: string[]) => { - return asyncReadFile(join(...paths), 'utf8') +export const readFile = (...paths: string[]): Promise => { + const filePath = getWorkspacePath(...paths) + return asyncReadFile(getWorkspacePath(...paths), 'utf8').catch((err) => { + console.warn(`Failed to read from ${filePath}`) + }) } -export const writeFile = (data: any, ...paths: string[]) => { - const filePath = join(...paths) +export const writeFile = (data: any, ...paths: string[]): Promise => { + const filePath = getWorkspacePath(...paths) return asyncWriteFile(filePath, JSON.stringify(data)).catch((err) => { - console.error(`Failed to write to ${filePath}`) + console.warn(`Failed to write to ${filePath}`) }) } diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index aab047f8..bd44a6b2 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -22,16 +22,25 @@ class Storage { if (value) { return JSON.parse(value) } else if (SESSION_FILE_PATH) { - // optionally read from file as a fallback to localstorage - const sessionFile = await readFile(SESSION_FILE_PATH) try { + // optionally read from file as a fallback to local storage + const sessionFile = await readFile(SESSION_FILE_PATH) + if (!sessionFile) { + throw new Error('No session file found') + } const session = JSON.parse(sessionFile) - if (session && session[this.key]) { - // TODO: validate session - return session[this.key] + + if (session) { + const keys = Object.keys(session) + // validate session + if (keys.length) { + // should only be one + this.key = keys[0] + return session[this.key] + } } } catch (err) { - console.error(`Failed to parse session file: ${SESSION_FILE_PATH}`) + console.warn(`Failed to read or parse session file: ${SESSION_FILE_PATH}`) } } return this.defaultValue @@ -39,6 +48,7 @@ class Storage { public set = (value: T): void => { const stringValue = JSON.stringify(value) this.storage.update(this.key, stringValue) + this.writeToSessionFile(stringValue) } public update = async (value: T): Promise => { const current = await this.get() @@ -46,12 +56,19 @@ class Storage { ...current, ...value, }) - this.storage.update(this.key, next).then(() => { - // optionally write to file - if (SESSION_FILE_PATH) { - writeFile(this.storage, SESSION_FILE_PATH) + await this.storage.update(this.key, next) + + this.writeToSessionFile(next) + } + public writeToSessionFile(data: string) { + // optionally write to file + if (SESSION_FILE_PATH) { + try { + writeFile({ [this.key]: data }, SESSION_FILE_PATH) + } catch (err: any) { + console.warn(`Failed to write coderoad session to path: ${SESSION_FILE_PATH}`) } - }) + } } public reset = () => { this.set(this.defaultValue) From 63c80671b03241742e3d45e7fdb9597cb2ebf01b Mon Sep 17 00:00:00 2001 From: shmck Date: Sat, 20 Nov 2021 21:09:08 -0800 Subject: [PATCH 3/5] change to use storage path Signed-off-by: shmck --- src/environment.ts | 2 +- src/services/context/state/Position.ts | 1 + src/services/context/state/Tutorial.ts | 1 + src/services/node/index.ts | 4 +-- src/services/storage/index.ts | 40 ++++++++++++++++---------- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/environment.ts b/src/environment.ts index 49475422..2e74aa15 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -48,4 +48,4 @@ export const CONTENT_SECURITY_POLICY_EXEMPTIONS: string | null = export const WEBHOOK_TOKEN = process.env.CODEROAD_WEBHOOK_TOKEN || null // a path to write session state to a file. Useful for maintaining session across containers -export const SESSION_FILE_PATH = process.env.CODEROAD_SESSION_FILE_PATH || null +export const SESSION_STORAGE_PATH = process.env.CODEROAD_STORAGE_PATH || null diff --git a/src/services/context/state/Position.ts b/src/services/context/state/Position.ts index 50c649f0..fe90d2da 100644 --- a/src/services/context/state/Position.ts +++ b/src/services/context/state/Position.ts @@ -19,6 +19,7 @@ class Position { setTutorial(workspaceState: vscode.Memento, tutorial: TT.Tutorial): void { this.storage = new Storage({ key: `coderoad:position:${tutorial.id}:${tutorial.version}`, + filePath: 'coderoad_position', storage: workspaceState, defaultValue, }) diff --git a/src/services/context/state/Tutorial.ts b/src/services/context/state/Tutorial.ts index 1195e7bf..40ecc48c 100644 --- a/src/services/context/state/Tutorial.ts +++ b/src/services/context/state/Tutorial.ts @@ -9,6 +9,7 @@ class Tutorial { constructor(workspaceState: vscode.Memento) { this.storage = new Storage({ key: 'coderoad:currentTutorial', + filePath: 'coderoad_tutorial', storage: workspaceState, defaultValue: null, }) diff --git a/src/services/node/index.ts b/src/services/node/index.ts index 92504100..53b6f991 100644 --- a/src/services/node/index.ts +++ b/src/services/node/index.ts @@ -35,13 +35,13 @@ export const removeFile = (...paths: string[]) => { export const readFile = (...paths: string[]): Promise => { const filePath = getWorkspacePath(...paths) return asyncReadFile(getWorkspacePath(...paths), 'utf8').catch((err) => { - console.warn(`Failed to read from ${filePath}`) + console.warn(`Failed to read from ${filePath}: ${err.message}`) }) } export const writeFile = (data: any, ...paths: string[]): Promise => { const filePath = getWorkspacePath(...paths) return asyncWriteFile(filePath, JSON.stringify(data)).catch((err) => { - console.warn(`Failed to write to ${filePath}`) + console.warn(`Failed to write to ${filePath}: ${err.message}`) }) } diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index bd44a6b2..db7ebe34 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode' import { readFile, writeFile } from '../node' -import { SESSION_FILE_PATH } from '../../environment' +import { SESSION_STORAGE_PATH } from '../../environment' // NOTE: localStorage is not available on client // and must be stored in editor @@ -10,37 +10,47 @@ import { SESSION_FILE_PATH } from '../../environment' // forcing it to be passed in through activation and down to other tools class Storage { private key: string + private filePath: string private storage: vscode.Memento private defaultValue: T - constructor({ key, storage, defaultValue }: { key: string; storage: vscode.Memento; defaultValue: T }) { + constructor({ + key, + filePath, + storage, + defaultValue, + }: { + key: string + filePath: string + storage: vscode.Memento + defaultValue: T + }) { this.storage = storage this.key = key + this.filePath = filePath this.defaultValue = defaultValue } public get = async (): Promise => { const value: string | undefined = await this.storage.get(this.key) if (value) { return JSON.parse(value) - } else if (SESSION_FILE_PATH) { + } else if (SESSION_STORAGE_PATH) { try { // optionally read from file as a fallback to local storage - const sessionFile = await readFile(SESSION_FILE_PATH) + const sessionFile = await readFile(SESSION_STORAGE_PATH, `${this.filePath}.json`) if (!sessionFile) { throw new Error('No session file found') } - const session = JSON.parse(sessionFile) + const data: T = JSON.parse(sessionFile) - if (session) { - const keys = Object.keys(session) + if (data) { // validate session + const keys = Object.keys(data) if (keys.length) { - // should only be one - this.key = keys[0] - return session[this.key] + return data } } } catch (err) { - console.warn(`Failed to read or parse session file: ${SESSION_FILE_PATH}`) + console.warn(`Failed to read or parse session file: ${SESSION_STORAGE_PATH}/${this.filePath}.json`) } } return this.defaultValue @@ -61,12 +71,12 @@ class Storage { this.writeToSessionFile(next) } public writeToSessionFile(data: string) { - // optionally write to file - if (SESSION_FILE_PATH) { + // optionally write state to file, useful when state cannot be controlled across containers + if (SESSION_STORAGE_PATH) { try { - writeFile({ [this.key]: data }, SESSION_FILE_PATH) + writeFile(data, SESSION_STORAGE_PATH, `${this.filePath}.json`) } catch (err: any) { - console.warn(`Failed to write coderoad session to path: ${SESSION_FILE_PATH}`) + console.warn(`Failed to write coderoad session to path: ${SESSION_STORAGE_PATH}/${this.filePath}.json`) } } } From 8582bf957b3210d8fc833b382972dd5d3d1602c4 Mon Sep 17 00:00:00 2001 From: shmck Date: Sat, 20 Nov 2021 21:33:09 -0800 Subject: [PATCH 4/5] remove additional stringification Signed-off-by: shmck --- src/actions/onStartup.ts | 4 ++-- src/services/node/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/actions/onStartup.ts b/src/actions/onStartup.ts index 09cc97a3..98235536 100644 --- a/src/actions/onStartup.ts +++ b/src/actions/onStartup.ts @@ -35,8 +35,8 @@ const onStartup = async (context: Context): Promise => { // NEW: no stored tutorial, must start new tutorial if (!tutorial || !tutorial.id) { - if (!!TUTORIAL_URL) { - // NEW_FROM_URL + if (TUTORIAL_URL) { + // if a tutorial URL is added, launch on startup try { const tutorialRes = await fetch(TUTORIAL_URL) const tutorial = await tutorialRes.json() diff --git a/src/services/node/index.ts b/src/services/node/index.ts index 53b6f991..78bfac20 100644 --- a/src/services/node/index.ts +++ b/src/services/node/index.ts @@ -41,7 +41,7 @@ export const readFile = (...paths: string[]): Promise => { export const writeFile = (data: any, ...paths: string[]): Promise => { const filePath = getWorkspacePath(...paths) - return asyncWriteFile(filePath, JSON.stringify(data)).catch((err) => { + return asyncWriteFile(filePath, data).catch((err) => { console.warn(`Failed to write to ${filePath}: ${err.message}`) }) } From ba4e1ac0326ba8b4ec55c4c2bcea39ce4b5fe60b Mon Sep 17 00:00:00 2001 From: shmck Date: Sat, 20 Nov 2021 21:35:53 -0800 Subject: [PATCH 5/5] add storage path key to docs Signed-off-by: shmck --- docs/docs/env-vars.md | 2 ++ src/environment.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/docs/env-vars.md b/docs/docs/env-vars.md index 58561f6c..ac04c7f8 100644 --- a/docs/docs/env-vars.md +++ b/docs/docs/env-vars.md @@ -20,6 +20,8 @@ CodeRoad has a number of configurations: - `CODEROAD_WEBHOOK_TOKEN` - an optional token for authenticating/authorizing webhook endpoints. Passed to the webhook endpoint in a `CodeRoad-User-Token` header. +- `CODEROAD_SESSION_STORAGE_PATH` - the path to a directory for writing session storage to files. Helps preserves state across containers. Example: `../tmp`. + ## How to Use Variables ### Local diff --git a/src/environment.ts b/src/environment.ts index 2e74aa15..8073609b 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -48,4 +48,4 @@ export const CONTENT_SECURITY_POLICY_EXEMPTIONS: string | null = export const WEBHOOK_TOKEN = process.env.CODEROAD_WEBHOOK_TOKEN || null // a path to write session state to a file. Useful for maintaining session across containers -export const SESSION_STORAGE_PATH = process.env.CODEROAD_STORAGE_PATH || null +export const SESSION_STORAGE_PATH = process.env.CODEROAD_SESSION_STORAGE_PATH || null