Compare commits

..

4 Commits

Author SHA1 Message Date
Florent Beauchamp
91f79be44a show debug once per cleanVm 2022-05-12 15:35:11 +02:00
Florent Beauchamp
7d094f6706 fix: remove debug of vhd children 2022-05-12 15:31:46 +02:00
Florent Beauchamp
b34c43db81 fix scope 2022-05-12 15:28:56 +02:00
Florent Beauchamp
42dce7d10c feat: add more debug in getUsedChildChainOrDelete and don't delete vhd 2022-05-12 15:04:15 +02:00
1047 changed files with 19950 additions and 62803 deletions

View File

@@ -1 +0,0 @@
{ "extends": ["@commitlint/config-conventional"] }

View File

@@ -28,10 +28,8 @@ module.exports = {
},
},
{
files: ['*.{integ,spec,test}.{,c,m}js'],
files: ['*.spec.{,c,m}js'],
rules: {
'n/no-unpublished-require': 'off',
'n/no-unpublished-import': 'off',
'n/no-unsupported-features/node-builtins': [
'error',
{

View File

@@ -6,10 +6,7 @@ labels: 'status: triaging :triangular_flag_on_post:, type: bug :bug:'
assignees: ''
---
1. ⚠️ **If you don't follow this template, the issue will be closed**.
2. ⚠️ **If your issue can't be easily reproduced, please report it [on the forum first](https://xcp-ng.org/forum/category/12/xen-orchestra)**.
Are you using XOA or XO from the sources?
**XOA or XO from the sources?**
If XOA:
@@ -18,7 +15,6 @@ If XOA:
If XO from the sources:
- Provide **your commit number**. If it's older than a week, we won't investigate
- Don't forget to [read this first](https://xen-orchestra.com/docs/community.html)
- As well as follow [this guide](https://xen-orchestra.com/docs/community.html#report-a-bug)
@@ -42,6 +38,8 @@ If applicable, add screenshots to help explain your problem.
**Environment (please provide the following information):**
- Node: [e.g. 16.12.1]
- xo-server: [e.g. 5.82.3]
- xo-web: [e.g. 5.87.0]
- hypervisor: [e.g. XCP-ng 8.2.0]
**Additional context**

View File

@@ -4,6 +4,7 @@ about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**

View File

@@ -1,32 +0,0 @@
name: Continous Integration
on: push
jobs:
CI:
runs-on: ubuntu-latest
steps:
# https://github.com/actions/checkout
- uses: actions/checkout@v3
- name: Install packages
run: |
sudo apt-get update
sudo apt-get install -y curl qemu-utils python3-vmdkstream git libxml2-utils libfuse2 nbdkit
- name: Cache Turbo
# https://github.com/actions/cache
uses: actions/cache@v3
with:
path: '**/node_modules/.cache/turbo'
key: ${{ runner.os }}-turbo-cache
- name: Setup Node environment
# https://github.com/actions/setup-node
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install project dependencies
run: yarn
- name: Build the project
run: yarn build
- name: Lint tests
run: yarn test-lint
- name: Integration tests
run: sudo yarn test-integration

13
.github/workflows/push.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: CI
on: [push]
jobs:
build:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: satackey/action-docker-layer-caching@v0.0.11
# Ignore the failure of a step and avoid terminating the job.
continue-on-error: true
- run: docker-compose -f docker/docker-compose.dev.yml build
- run: docker-compose -f docker/docker-compose.dev.yml up

3
.gitignore vendored
View File

@@ -10,6 +10,8 @@
/packages/*/dist/
/packages/*/node_modules/
/packages/vhd-cli/src/commands/index.js
/packages/xen-api/examples/node_modules/
/packages/xen-api/plot.dat
@@ -34,4 +36,3 @@ yarn-error.log.*
# code coverage
.nyc_output/
coverage/
.turbo/

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Only check commit message if commit on master or first commit on another
# branch to avoid bothering fix commits after reviews
#
# FIXME: does not properly run with git commit --amend
if [ "$(git rev-parse --abbrev-ref HEAD)" = master ] || [ "$(git rev-list --count master..)" -eq 0 ]
then
npx --no -- commitlint --edit "$1"
fi

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

View File

@@ -1,11 +1,8 @@
'use strict'
module.exports = {
arrowParens: 'avoid',
jsxSingleQuote: true,
semi: false,
singleQuote: true,
trailingComma: 'es5',
// 2020-11-24: Requested by nraynaud and approved by the rest of the team
//

View File

@@ -14,7 +14,7 @@ Returns a promise wich rejects as soon as a call to `iteratee` throws or a promi
`opts` is an object that can contains the following options:
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `10`. The value `0` means no concurrency limit.
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `1`
- `signal`: an abort signal to stop the iteration
- `stopOnError`: wether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`

View File

@@ -10,8 +10,8 @@
Installation of the [npm package](https://npmjs.org/package/@vates/async-each):
```sh
npm install --save @vates/async-each
```
> npm install --save @vates/async-each
```
## Usage
@@ -32,7 +32,7 @@ Returns a promise wich rejects as soon as a call to `iteratee` throws or a promi
`opts` is an object that can contains the following options:
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `10`. The value `0` means no concurrency limit.
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `1`
- `signal`: an abort signal to stop the iteration
- `stopOnError`: wether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`

View File

@@ -9,16 +9,7 @@ class AggregateError extends Error {
}
}
/**
* @template Item
* @param {Iterable<Item>} iterable
* @param {(item: Item, index: number, iterable: Iterable<Item>) => Promise<void>} iteratee
* @returns {Promise<void>}
*/
exports.asyncEach = function asyncEach(iterable, iteratee, { concurrency = 10, signal, stopOnError = true } = {}) {
if (concurrency === 0) {
concurrency = Infinity
}
exports.asyncEach = function asyncEach(iterable, iteratee, { concurrency = 1, signal, stopOnError = true } = {}) {
return new Promise((resolve, reject) => {
const it = (iterable[Symbol.iterator] || iterable[Symbol.asyncIterator]).call(iterable)
const errors = []

View File

@@ -1,8 +1,6 @@
'use strict'
const { describe, it, beforeEach } = require('test')
const assert = require('assert').strict
const { spy } = require('sinon')
/* eslint-env jest */
const { asyncEach } = require('./')
@@ -36,18 +34,12 @@ describe('asyncEach', () => {
})
it('works', async () => {
const iteratee = spy(async () => {})
const iteratee = jest.fn(async () => {})
await asyncEach.call(thisArg, iterable, iteratee, { concurrency: 1 })
await asyncEach.call(thisArg, iterable, iteratee)
assert.deepStrictEqual(
iteratee.thisValues,
Array.from(values, () => thisArg)
)
assert.deepStrictEqual(
iteratee.args,
Array.from(values, (value, index) => [value, index, iterable])
)
expect(iteratee.mock.instances).toEqual(Array.from(values, () => thisArg))
expect(iteratee.mock.calls).toEqual(Array.from(values, (value, index) => [value, index, iterable]))
})
;[1, 2, 4].forEach(concurrency => {
it('respects a concurrency of ' + concurrency, async () => {
@@ -57,7 +49,7 @@ describe('asyncEach', () => {
values,
async () => {
++running
assert.deepStrictEqual(running <= concurrency, true)
expect(running).toBeLessThanOrEqual(concurrency)
await randomDelay()
--running
},
@@ -67,52 +59,40 @@ describe('asyncEach', () => {
})
it('stops on first error when stopOnError is true', async () => {
const tracker = new assert.CallTracker()
const error = new Error()
const iteratee = tracker.calls((_, i) => {
const iteratee = jest.fn((_, i) => {
if (i === 1) {
throw error
}
}, 2)
assert.deepStrictEqual(
await rejectionOf(asyncEach(iterable, iteratee, { concurrency: 1, stopOnError: true })),
error
)
})
tracker.verify()
expect(await rejectionOf(asyncEach(iterable, iteratee, { stopOnError: true }))).toBe(error)
expect(iteratee).toHaveBeenCalledTimes(2)
})
it('rejects AggregateError when stopOnError is false', async () => {
const errors = []
const iteratee = spy(() => {
const iteratee = jest.fn(() => {
const error = new Error()
errors.push(error)
throw error
})
const error = await rejectionOf(asyncEach(iterable, iteratee, { stopOnError: false }))
assert.deepStrictEqual(error.errors, errors)
assert.deepStrictEqual(
iteratee.args,
Array.from(values, (value, index) => [value, index, iterable])
)
expect(error.errors).toEqual(errors)
expect(iteratee.mock.calls).toEqual(Array.from(values, (value, index) => [value, index, iterable]))
})
it('can be interrupted with an AbortSignal', async () => {
const tracker = new assert.CallTracker()
const ac = new AbortController()
const iteratee = tracker.calls((_, i) => {
const iteratee = jest.fn((_, i) => {
if (i === 1) {
ac.abort()
}
}, 2)
await assert.rejects(asyncEach(iterable, iteratee, { concurrency: 1, signal: ac.signal }), {
message: 'asyncEach aborted',
})
tracker.verify()
await expect(asyncEach(iterable, iteratee, { signal: ac.signal })).rejects.toThrow('asyncEach aborted')
expect(iteratee).toHaveBeenCalledTimes(2)
})
})
)

View File

@@ -24,17 +24,11 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "1.0.0",
"version": "0.1.0",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
},
"devDependencies": {
"sinon": "^15.0.1",
"tap": "^16.3.0",
"test": "^3.2.1"
"postversion": "npm publish --access public"
}
}

View File

@@ -10,8 +10,8 @@
Installation of the [npm package](https://npmjs.org/package/@vates/cached-dns.lookup):
```sh
npm install --save @vates/cached-dns.lookup
```
> npm install --save @vates/cached-dns.lookup
```
## Usage

View File

@@ -10,8 +10,8 @@
Installation of the [npm package](https://npmjs.org/package/@vates/coalesce-calls):
```sh
npm install --save @vates/coalesce-calls
```
> npm install --save @vates/coalesce-calls
```
## Usage

View File

@@ -1,7 +1,6 @@
'use strict'
const { describe, it } = require('test')
const assert = require('assert')
/* eslint-env jest */
const { coalesceCalls } = require('./')
@@ -24,13 +23,13 @@ describe('coalesceCalls', () => {
const promise2 = fn(defer2.promise)
defer1.resolve('foo')
assert.strictEqual(await promise1, 'foo')
assert.strictEqual(await promise2, 'foo')
expect(await promise1).toBe('foo')
expect(await promise2).toBe('foo')
const defer3 = pDefer()
const promise3 = fn(defer3.promise)
defer3.resolve('bar')
assert.strictEqual(await promise3, 'bar')
expect(await promise3).toBe('bar')
})
})

View File

@@ -30,10 +30,6 @@
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
},
"devDependencies": {
"test": "^3.2.1"
"postversion": "npm publish --access public"
}
}

View File

@@ -10,8 +10,8 @@
Installation of the [npm package](https://npmjs.org/package/@vates/compose):
```sh
npm install --save @vates/compose
```
> npm install --save @vates/compose
```
## Usage

View File

@@ -1,7 +1,6 @@
'use strict'
const { describe, it } = require('test')
const assert = require('node:assert').strict
/* eslint-env jest */
const { compose } = require('./')
@@ -10,42 +9,43 @@ const mul3 = x => x * 3
describe('compose()', () => {
it('throws when no functions is passed', () => {
assert.throws(() => compose(), TypeError)
assert.throws(() => compose([]), TypeError)
expect(() => compose()).toThrow(TypeError)
expect(() => compose([])).toThrow(TypeError)
})
it('applies from left to right', () => {
assert.strictEqual(compose(add2, mul3)(5), 21)
expect(compose(add2, mul3)(5)).toBe(21)
})
it('accepts functions in an array', () => {
assert.strictEqual(compose([add2, mul3])(5), 21)
expect(compose([add2, mul3])(5)).toBe(21)
})
it('can apply from right to left', () => {
assert.strictEqual(compose({ right: true }, add2, mul3)(5), 17)
expect(compose({ right: true }, add2, mul3)(5)).toBe(17)
})
it('accepts options with functions in an array', () => {
assert.strictEqual(compose({ right: true }, [add2, mul3])(5), 17)
expect(compose({ right: true }, [add2, mul3])(5)).toBe(17)
})
it('can compose async functions', async () => {
assert.strictEqual(
expect(
await compose(
{ async: true },
async x => x + 2,
async x => x * 3
)(5),
21
)
)(5)
).toBe(21)
})
it('forwards all args to first function', () => {
expect.assertions(1)
const expectedArgs = [Math.random(), Math.random()]
compose(
(...args) => {
assert.deepEqual(args, expectedArgs)
expect(args).toEqual(expectedArgs)
},
// add a second function to avoid the one function special case
Function.prototype
@@ -53,13 +53,15 @@ describe('compose()', () => {
})
it('forwards context to all functions', () => {
expect.assertions(2)
const expectedThis = {}
compose(
function () {
assert.strictEqual(this, expectedThis)
expect(this).toBe(expectedThis)
},
function () {
assert.strictEqual(this, expectedThis)
expect(this).toBe(expectedThis)
}
).call(expectedThis)
})

View File

@@ -19,10 +19,6 @@
"node": ">=7.6"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
},
"devDependencies": {
"test": "^3.2.1"
"postversion": "npm publish --access public"
}
}

View File

@@ -10,8 +10,8 @@
Installation of the [npm package](https://npmjs.org/package/@vates/decorate-with):
```sh
npm install --save @vates/decorate-with
```
> npm install --save @vates/decorate-with
```
## Usage

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('assert')
const { describe, it } = require('test')
const { describe, it } = require('tap').mocha
const { decorateClass, decorateWith, decorateMethodsWith, perInstance } = require('./')

View File

@@ -26,9 +26,9 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"test": "tap"
},
"devDependencies": {
"test": "^3.2.1"
"tap": "^16.0.1"
}
}

View File

@@ -1,32 +0,0 @@
```js
import diff from '@vates/diff'
diff('foo bar baz', 'Foo qux')
// → [ 0, 'F', 4, 'qux', 7, '' ]
//
// Differences of the second string from the first one:
// - at position 0, it contains `F`
// - at position 4, it contains `qux`
// - at position 7, it ends
diff('Foo qux', 'foo bar baz')
// → [ 0, 'f', 4, 'bar', 7, ' baz' ]
//
// Differences of the second string from the first one:
// - at position 0, it contains f`
// - at position 4, it contains `bar`
// - at position 7, it contains `baz`
// works with all collections that supports
// - `.length`
// - `collection[index]`
// - `.slice(start, end)`
//
// which includes:
// - arrays
// - strings
// - `Buffer`
// - `TypedArray`
diff([0, 1, 2], [3, 4])
// → [ 0, [ 3, 4 ], 2, [] ]
```

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,65 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/diff
[![Package Version](https://badgen.net/npm/v/@vates/diff)](https://npmjs.org/package/@vates/diff) ![License](https://badgen.net/npm/license/@vates/diff) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/diff)](https://bundlephobia.com/result?p=@vates/diff) [![Node compatibility](https://badgen.net/npm/node/@vates/diff)](https://npmjs.org/package/@vates/diff)
> Computes differences between two arrays, buffers or strings
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/diff):
```sh
npm install --save @vates/diff
```
## Usage
```js
import diff from '@vates/diff'
diff('foo bar baz', 'Foo qux')
// → [ 0, 'F', 4, 'qux', 7, '' ]
//
// Differences of the second string from the first one:
// - at position 0, it contains `F`
// - at position 4, it contains `qux`
// - at position 7, it ends
diff('Foo qux', 'foo bar baz')
// → [ 0, 'f', 4, 'bar', 7, ' baz' ]
//
// Differences of the second string from the first one:
// - at position 0, it contains f`
// - at position 4, it contains `bar`
// - at position 7, it contains `baz`
// works with all collections that supports
// - `.length`
// - `collection[index]`
// - `.slice(start, end)`
//
// which includes:
// - arrays
// - strings
// - `Buffer`
// - `TypedArray`
diff([0, 1, 2], [3, 4])
// → [ 0, [ 3, 4 ], 2, [] ]
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,37 +0,0 @@
'use strict'
/**
* Compare two data arrays, buffers or strings and invoke the provided callback function for each difference.
*
* @template {Array|Buffer|string} T
* @param {Array|Buffer|string} data1 - The first data array or buffer to compare.
* @param {T} data2 - The second data array or buffer to compare.
* @param {(index: number, diff: T) => void} [cb] - The callback function to invoke for each difference. If not provided, an array of differences will be returned.
* @returns {Array<number|T>|undefined} - An array of differences if no callback is provided, otherwise undefined.
*/
module.exports = function diff(data1, data2, cb) {
let result
if (cb === undefined) {
result = []
cb = result.push.bind(result)
}
const n1 = data1.length
const n2 = data2.length
const n = Math.min(n1, n2)
for (let i = 0; i < n; ++i) {
if (data1[i] !== data2[i]) {
let j = i + 1
while (j < n && data1[j] !== data2[j]) {
++j
}
cb(i, data2.slice(i, j))
i = j
}
}
if (n1 !== n2) {
cb(n, n1 < n2 ? data2.slice(n) : data2.slice(0, 0))
}
return result
}

View File

@@ -1,51 +0,0 @@
'use strict'
const assert = require('node:assert/strict')
const test = require('test')
const diff = require('./index.js')
test('data of equal length', function () {
const data1 = 'foo bar baz'
const data2 = 'baz bar foo'
assert.deepEqual(diff(data1, data2), [0, 'baz', 8, 'foo'])
})
test('data1 is longer', function () {
const data1 = 'foo bar'
const data2 = 'foo'
assert.deepEqual(diff(data1, data2), [3, ''])
})
test('data2 is longer', function () {
const data1 = 'foo'
const data2 = 'foo bar'
assert.deepEqual(diff(data1, data2), [3, ' bar'])
})
test('with arrays', function () {
const data1 = 'foo bar baz'.split('')
const data2 = 'baz bar foo'.split('')
assert.deepEqual(diff(data1, data2), [0, 'baz'.split(''), 8, 'foo'.split('')])
})
test('with buffers', function () {
const data1 = Buffer.from('foo bar baz')
const data2 = Buffer.from('baz bar foo')
assert.deepEqual(diff(data1, data2), [0, Buffer.from('baz'), 8, Buffer.from('foo')])
})
test('cb param', function () {
const data1 = 'foo bar baz'
const data2 = 'baz bar foo'
const calls = []
const cb = (...args) => calls.push(args)
diff(data1, data2, cb)
assert.deepEqual(calls, [
[0, 'baz'],
[8, 'foo'],
])
})

View File

@@ -1,36 +0,0 @@
{
"private": false,
"name": "@vates/diff",
"description": "Computes differences between two arrays, buffers or strings",
"keywords": [
"array",
"binary",
"buffer",
"diff",
"differences",
"string"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/diff",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/diff",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.0",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
},
"devDependencies": {
"test": "^3.3.0"
}
}

View File

@@ -10,8 +10,8 @@
Installation of the [npm package](https://npmjs.org/package/@vates/disposable):
```sh
npm install --save @vates/disposable
```
> npm install --save @vates/disposable
```
## Usage

View File

@@ -1,17 +1,16 @@
'use strict'
const { describe, it } = require('test')
const { useFakeTimers, spy, assert } = require('sinon')
/* eslint-env jest */
const { createDebounceResource } = require('./debounceResource')
const clock = useFakeTimers()
jest.useFakeTimers()
describe('debounceResource()', () => {
it('calls the resource disposer after 10 seconds', async () => {
const debounceResource = createDebounceResource()
const delay = 10e3
const dispose = spy()
const dispose = jest.fn()
const resource = await debounceResource(
Promise.resolve({
@@ -23,10 +22,10 @@ describe('debounceResource()', () => {
resource.dispose()
assert.notCalled(dispose)
expect(dispose).not.toBeCalled()
clock.tick(delay)
jest.advanceTimersByTime(delay)
assert.called(dispose)
expect(dispose).toBeCalled()
})
})

View File

@@ -1,14 +1,13 @@
'use strict'
const { describe, it } = require('test')
const { spy, assert } = require('sinon')
/* eslint-env jest */
const { deduped } = require('./deduped')
describe('deduped()', () => {
it('calls the resource function only once', async () => {
const value = {}
const getResource = spy(async () => ({
const getResource = jest.fn(async () => ({
value,
dispose: Function.prototype,
}))
@@ -18,13 +17,13 @@ describe('deduped()', () => {
const { value: v1 } = await dedupedGetResource()
const { value: v2 } = await dedupedGetResource()
assert.calledOnce(getResource)
assert.match(v1, value)
assert.match(v2, value)
expect(getResource).toHaveBeenCalledTimes(1)
expect(v1).toBe(value)
expect(v2).toBe(value)
})
it('only disposes the source disposable when its all copies dispose', async () => {
const dispose = spy()
const dispose = jest.fn()
const getResource = async () => ({
value: '',
dispose,
@@ -37,35 +36,35 @@ describe('deduped()', () => {
d1()
assert.notCalled(dispose)
expect(dispose).not.toHaveBeenCalled()
d2()
assert.calledOnce(dispose)
expect(dispose).toHaveBeenCalledTimes(1)
})
it('works with sync factory', () => {
const value = {}
const dispose = spy()
const dispose = jest.fn()
const dedupedGetResource = deduped(() => ({ value, dispose }))
const d1 = dedupedGetResource()
assert.match(d1.value, value)
expect(d1.value).toBe(value)
const d2 = dedupedGetResource()
assert.match(d2.value, value)
expect(d2.value).toBe(value)
d1.dispose()
assert.notCalled(dispose)
expect(dispose).not.toHaveBeenCalled()
d2.dispose()
assert.calledOnce(dispose)
expect(dispose).toHaveBeenCalledTimes(1)
})
it('no race condition on dispose before async acquisition', async () => {
const dispose = spy()
const dispose = jest.fn()
const dedupedGetResource = deduped(async () => ({ value: 42, dispose }))
const d1 = await dedupedGetResource()
@@ -74,6 +73,6 @@ describe('deduped()', () => {
d1.dispose()
assert.notCalled(dispose)
expect(dispose).not.toHaveBeenCalled()
})
})

View File

@@ -14,22 +14,17 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.4",
"version": "0.1.1",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"postversion": "npm publish --access public"
},
"dependencies": {
"@vates/multi-key-map": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/log": "^0.3.0",
"ensure-array": "^1.0.0"
},
"devDependencies": {
"sinon": "^15.0.1",
"test": "^3.2.1"
}
}

View File

@@ -8,8 +8,8 @@
Installation of the [npm package](https://npmjs.org/package/@vates/event-listeners-manager):
```sh
npm install --save @vates/event-listeners-manager
```
> npm install --save @vates/event-listeners-manager
```
## Usage

View File

@@ -9,7 +9,7 @@ exports.EventListenersManager = class EventListenersManager {
}
add(type, listener) {
let listeners = this._listeners.get(type)
let listeners = this._listeners[type]
if (listeners === undefined) {
listeners = new Set()
this._listeners.set(type, listeners)

View File

@@ -1,67 +0,0 @@
'use strict'
const t = require('tap')
const { EventEmitter } = require('events')
const { EventListenersManager } = require('./')
const noop = Function.prototype
// function spy (impl = Function.prototype) {
// function spy() {
// spy.calls.push([Array.from(arguments), this])
// }
// spy.calls = []
// return spy
// }
function assertListeners(t, event, listeners) {
t.strictSame(t.context.ee.listeners(event), listeners)
}
t.beforeEach(function (t) {
t.context.ee = new EventEmitter()
t.context.em = new EventListenersManager(t.context.ee)
})
t.test('.add adds a listener', function (t) {
t.context.em.add('foo', noop)
assertListeners(t, 'foo', [noop])
t.end()
})
t.test('.add does not add a duplicate listener', function (t) {
t.context.em.add('foo', noop).add('foo', noop)
assertListeners(t, 'foo', [noop])
t.end()
})
t.test('.remove removes a listener', function (t) {
t.context.em.add('foo', noop).remove('foo', noop)
assertListeners(t, 'foo', [])
t.end()
})
t.test('.removeAll removes all listeners of a given type', function (t) {
t.context.em.add('foo', noop).add('bar', noop).removeAll('foo')
assertListeners(t, 'foo', [])
assertListeners(t, 'bar', [noop])
t.end()
})
t.test('.removeAll removes all listeners', function (t) {
t.context.em.add('foo', noop).add('bar', noop).removeAll()
assertListeners(t, 'foo', [])
assertListeners(t, 'bar', [])
t.end()
})

View File

@@ -35,12 +35,8 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "1.0.1",
"version": "1.0.0",
"scripts": {
"postversion": "npm publish --access public",
"test": "tap --branches=72"
},
"devDependencies": {
"tap": "^16.2.0"
"postversion": "npm publish --access public"
}
}

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,64 +0,0 @@
import LRU from 'lru-cache'
import Fuse from 'fuse-native'
import { VhdSynthetic } from 'vhd-lib'
import { Disposable, fromCallback } from 'promise-toolbox'
// build a s stat object from https://github.com/fuse-friends/fuse-native/blob/master/test/fixtures/stat.js
const stat = st => ({
mtime: st.mtime || new Date(),
atime: st.atime || new Date(),
ctime: st.ctime || new Date(),
size: st.size !== undefined ? st.size : 0,
mode: st.mode === 'dir' ? 16877 : st.mode === 'file' ? 33188 : st.mode === 'link' ? 41453 : st.mode,
uid: st.uid !== undefined ? st.uid : process.getuid(),
gid: st.gid !== undefined ? st.gid : process.getgid(),
})
export const mount = Disposable.factory(async function* mount(handler, diskPath, mountDir) {
const vhd = yield VhdSynthetic.fromVhdChain(handler, diskPath)
const cache = new LRU({
max: 16, // each cached block is 2MB in size
})
await vhd.readBlockAllocationTable()
const fuse = new Fuse(mountDir, {
async readdir(path, cb) {
if (path === '/') {
return cb(null, ['vhd0'])
}
cb(Fuse.ENOENT)
},
async getattr(path, cb) {
if (path === '/') {
return cb(
null,
stat({
mode: 'dir',
size: 4096,
})
)
}
if (path === '/vhd0') {
return cb(
null,
stat({
mode: 'file',
size: vhd.footer.currentSize,
})
)
}
cb(Fuse.ENOENT)
},
read(path, fd, buf, len, pos, cb) {
if (path === '/vhd0') {
return vhd.readRawData(pos, len, cache, buf).then(cb)
}
throw new Error(`read file ${path} not exists`)
},
})
return new Disposable(
() => fromCallback(() => fuse.unmount()),
fromCallback(() => fuse.mount())
)
})

View File

@@ -1,30 +0,0 @@
{
"name": "@vates/fuse-vhd",
"version": "2.0.0",
"license": "ISC",
"private": false,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/fuse-vhd",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/fuse-vhd",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"engines": {
"node": ">=14"
},
"main": "./index.mjs",
"dependencies": {
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.5.0"
},
"scripts": {
"postversion": "npm publish --access public"
}
}

View File

@@ -10,8 +10,8 @@
Installation of the [npm package](https://npmjs.org/package/@vates/multi-key-map):
```sh
npm install --save @vates/multi-key-map
```
> npm install --save @vates/multi-key-map
```
## Usage

View File

@@ -1,7 +1,6 @@
'use strict'
const { describe, it } = require('test')
const assert = require('node:assert')
/* eslint-env jest */
const { MultiKeyMap } = require('./')
@@ -29,9 +28,9 @@ describe('MultiKeyMap', () => {
keys.forEach((key, i) => {
// copy the key to make sure the array itself is not the key
assert.strictEqual(map.get(key.slice()), values[i])
expect(map.get(key.slice())).toBe(values[i])
map.delete(key.slice())
assert.strictEqual(map.get(key.slice()), undefined)
expect(map.get(key.slice())).toBe(undefined)
})
})
})

View File

@@ -23,10 +23,6 @@
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
},
"devDependencies": {
"test": "^3.2.1"
"postversion": "npm publish --access public"
}
}

View File

@@ -1,16 +0,0 @@
### `new NdbClient({address, exportname, secure = true, port = 10809})`
create a new nbd client
```js
import NbdClient from '@vates/nbd-client'
const client = new NbdClient({
address: 'MY_NBD_HOST',
exportname: 'MY_SECRET_EXPORT',
cert: 'Server certificate', // optional, will use encrypted link if provided
})
await client.connect()
const block = await client.readBlock(blockIndex, BlockSize)
await client.disconnect()
```

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,47 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/nbd-client
[![Package Version](https://badgen.net/npm/v/@vates/nbd-client)](https://npmjs.org/package/@vates/nbd-client) ![License](https://badgen.net/npm/license/@vates/nbd-client) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/nbd-client)](https://bundlephobia.com/result?p=@vates/nbd-client) [![Node compatibility](https://badgen.net/npm/node/@vates/nbd-client)](https://npmjs.org/package/@vates/nbd-client)
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/nbd-client):
```sh
npm install --save @vates/nbd-client
```
## Usage
### `new NdbClient({address, exportname, secure = true, port = 10809})`
create a new nbd client
```js
import NbdClient from '@vates/nbd-client'
const client = new NbdClient({
address: 'MY_NBD_HOST',
exportname: 'MY_SECRET_EXPORT',
cert: 'Server certificate', // optional, will use encrypted link if provided
})
await client.connect()
const block = await client.readBlock(blockIndex, BlockSize)
await client.disconnect()
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,32 +0,0 @@
import NbdClient from "./client.mjs";
async function bench(){
const client = new NbdClient({
address:'localhost',
port: 9000,
exportname: 'bench_export'
})
await client.connect()
console.log('connected', client.exportSize)
for(let chunk_size=16*1024; chunk_size < 16*1024*1024; chunk_size *=2){
let i=0
const start = + new Date()
for await(const block of client.readBlocks(chunk_size) ){
i++
if((i*chunk_size) % (16*1024*1024) ===0){
process.stdout.write('.')
}
if(i*chunk_size > 1024*1024*1024) break
}
console.log(chunk_size,Math.round( (i*chunk_size/1024/1024*1000)/ (new Date() - start)))
}
await client.disconnect()
}
bench()

View File

@@ -1,359 +0,0 @@
import assert from 'node:assert'
import { Socket } from 'node:net'
import { connect } from 'node:tls'
import { fromCallback, pRetry, pDelay, pTimeout } from 'promise-toolbox'
import { readChunkStrict } from '@vates/read-chunk'
import { createLogger } from '@xen-orchestra/log'
import {
INIT_PASSWD,
NBD_CMD_READ,
NBD_DEFAULT_BLOCK_SIZE,
NBD_DEFAULT_PORT,
NBD_FLAG_FIXED_NEWSTYLE,
NBD_FLAG_HAS_FLAGS,
NBD_OPT_EXPORT_NAME,
NBD_OPT_REPLY_MAGIC,
NBD_OPT_STARTTLS,
NBD_REPLY_ACK,
NBD_REPLY_MAGIC,
NBD_REQUEST_MAGIC,
OPTS_MAGIC,
NBD_CMD_DISC,
} from './constants.mjs'
const { warn } = createLogger('vates:nbd-client')
// documentation is here : https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
export default class NbdClient {
#serverAddress
#serverCert
#serverPort
#serverSocket
#exportName
#exportSize
#waitBeforeReconnect
#readAhead
#readBlockRetries
#reconnectRetry
#connectTimeout
// AFAIK, there is no guaranty the server answers in the same order as the queries
// so we handle a backlog of command waiting for response and handle concurrency manually
#waitingForResponse // there is already a listenner waiting for a response
#nextCommandQueryId = BigInt(0)
#commandQueryBacklog // map of command waiting for an response queryId => { size/*in byte*/, resolve, reject}
#connected = false
#reconnectingPromise
constructor(
{ address, port = NBD_DEFAULT_PORT, exportname, cert },
{ connectTimeout = 6e4, waitBeforeReconnect = 1e3, readAhead = 10, readBlockRetries = 5, reconnectRetry = 5 } = {}
) {
this.#serverAddress = address
this.#serverPort = port
this.#exportName = exportname
this.#serverCert = cert
this.#waitBeforeReconnect = waitBeforeReconnect
this.#readAhead = readAhead
this.#readBlockRetries = readBlockRetries
this.#reconnectRetry = reconnectRetry
this.#connectTimeout = connectTimeout
}
get exportSize() {
return this.#exportSize
}
async #tlsConnect() {
return new Promise((resolve, reject) => {
this.#serverSocket = connect({
socket: this.#serverSocket,
rejectUnauthorized: false,
cert: this.#serverCert
})
this.#serverSocket.once('error', reject)
this.#serverSocket.once('secureConnect', () => {
this.#serverSocket.removeListener('error', reject)
resolve()
})
})
}
// mandatory , at least to start the handshake
async #unsecureConnect() {
this.#serverSocket = new Socket()
return new Promise((resolve, reject) => {
this.#serverSocket.connect({
port:this.#serverPort,
host: this.#serverAddress,
// @todo should test the onRead to limit buffer copy
})
this.#serverSocket.once('error', reject)
this.#serverSocket.once('connect', () => {
this.#serverSocket.removeListener('error', reject)
resolve()
})
})
}
async #connect() {
// first we connect to the server without tls, and then we upgrade the connection
// to tls during the handshake
await this.#unsecureConnect()
await this.#handshake()
this.#connected = true
// reset internal state if we reconnected a nbd client
this.#commandQueryBacklog = new Map()
this.#waitingForResponse = false
}
async connect() {
return pTimeout.call(this.#connect(), this.#connectTimeout)
}
async disconnect() {
if (!this.#connected) {
return
}
const buffer = Buffer.alloc(28)
buffer.writeInt32BE(NBD_REQUEST_MAGIC, 0) // it is a nbd request
buffer.writeInt16BE(0, 4) // no command flags for a disconnect
buffer.writeInt16BE(NBD_CMD_DISC, 6) // we want to disconnect from nbd server
await this.#write(buffer)
await this.#serverSocket.destroy()
this.#serverSocket = undefined
this.#connected = false
}
#clearReconnectPromise = () => {
this.#reconnectingPromise = undefined
}
async #reconnect() {
await this.disconnect().catch(() => {})
await pDelay(this.#waitBeforeReconnect) // need to let the xapi clean things on its side
await this.connect()
}
async reconnect() {
// we need to ensure reconnections do not occur in parallel
if (this.#reconnectingPromise === undefined) {
this.#reconnectingPromise = pRetry(() => this.#reconnect(), {
tries: this.#reconnectRetry,
})
this.#reconnectingPromise.then(this.#clearReconnectPromise, this.#clearReconnectPromise)
}
return this.#reconnectingPromise
}
// we can use individual read/write from the socket here since there is no concurrency
async #sendOption(option, buffer = Buffer.alloc(0)) {
await this.#write(OPTS_MAGIC)
await this.#writeInt32(option)
await this.#writeInt32(buffer.length)
await this.#write(buffer)
assert.strictEqual(await this.#readInt64(), NBD_OPT_REPLY_MAGIC) // magic number everywhere
assert.strictEqual(await this.#readInt32(), option) // the option passed
assert.strictEqual(await this.#readInt32(), NBD_REPLY_ACK) // ACK
const length = await this.#readInt32()
assert.strictEqual(length, 0) // length
}
// we can use individual read/write from the socket here since there is only one handshake at once, no concurrency
async #handshake() {
assert((await this.#read(8)).equals(INIT_PASSWD))
assert((await this.#read(8)).equals(OPTS_MAGIC))
const flagsBuffer = await this.#read(2)
const flags = flagsBuffer.readInt16BE(0)
assert.strictEqual(flags & NBD_FLAG_FIXED_NEWSTYLE, NBD_FLAG_FIXED_NEWSTYLE) // only FIXED_NEWSTYLE one is supported from the server options
await this.#writeInt32(NBD_FLAG_FIXED_NEWSTYLE) // client also support NBD_FLAG_C_FIXED_NEWSTYLE
if (this.#serverCert !== undefined) {
// upgrade socket to TLS if needed
await this.#sendOption(NBD_OPT_STARTTLS)
await this.#tlsConnect()
}
// send export name we want to access.
// it's implictly closing the negociation phase.
await this.#write(OPTS_MAGIC)
await this.#writeInt32(NBD_OPT_EXPORT_NAME)
const exportNameBuffer = Buffer.from(this.#exportName)
await this.#writeInt32(exportNameBuffer.length)
await this.#write(exportNameBuffer)
// 8 (export size ) + 2 (flags) + 124 zero = 134
// must read all to ensure nothing stays in the buffer
const answer = await this.#read(134)
this.#exportSize = answer.readBigUInt64BE(0)
const transmissionFlags = answer.readInt16BE(8)
assert.strictEqual(transmissionFlags & NBD_FLAG_HAS_FLAGS, NBD_FLAG_HAS_FLAGS, 'NBD_FLAG_HAS_FLAGS') // must always be 1 by the norm
// note : xapi server always send NBD_FLAG_READ_ONLY (3) as a flag
}
#read(length) {
return readChunkStrict(this.#serverSocket, length)
}
#write(buffer) {
return fromCallback.call(this.#serverSocket, 'write', buffer)
}
async #readInt32() {
const buffer = await this.#read(4)
return buffer.readInt32BE(0)
}
async #readInt64() {
const buffer = await this.#read(8)
return buffer.readBigUInt64BE(0)
}
#writeInt32(int) {
const buffer = Buffer.alloc(4)
buffer.writeInt32BE(int)
return this.#write(buffer)
}
// when one read fail ,stop everything
async #rejectAll(error) {
this.#commandQueryBacklog.forEach(({ reject }) => {
reject(error)
})
}
async #readBlockResponse() {
// ensure at most one read occur in parallel
if (this.#waitingForResponse) {
return
}
try {
this.#waitingForResponse = true
const buffer = await this.#read(4+4+8)
const magic = buffer.readUInt32BE()
if (magic !== NBD_REPLY_MAGIC) {
throw new Error(`magic number for block answer is wrong : ${magic} ${NBD_REPLY_MAGIC}`)
}
const error = buffer.readUInt32BE(4)
if (error !== 0) {
// @todo use error code from constants.mjs
throw new Error(`GOT ERROR CODE : ${error}`)
}
const blockQueryId = buffer.readBigUInt64BE(8)
const query = this.#commandQueryBacklog.get(blockQueryId)
if (!query) {
throw new Error(` no query associated with id ${blockQueryId}`)
}
this.#commandQueryBacklog.delete(blockQueryId)
const data = await this.#read(query.size)
query.resolve(data)
this.#waitingForResponse = false
if (this.#commandQueryBacklog.size > 0) {
// it doesn't throw directly but will throw all relevant promise on failure
this.#readBlockResponse()
}
} catch (error) {
// reject all the promises
// we don't need to call readBlockResponse on failure
// since we will empty the backlog
await this.#rejectAll(error)
}
}
async readBlock(index, size = NBD_DEFAULT_BLOCK_SIZE) {
// we don't want to add anything in backlog while reconnecting
if (this.#reconnectingPromise) {
await this.#reconnectingPromise
}
const queryId = this.#nextCommandQueryId
this.#nextCommandQueryId++
// create and send command at once to ensure there is no concurrency issue
const buffer = Buffer.alloc(28)
buffer.writeInt32BE(NBD_REQUEST_MAGIC, 0) // it is a nbd request
buffer.writeInt16BE(0, 4) // no command flags for a simple block read
buffer.writeInt16BE(NBD_CMD_READ, 6) // we want to read a data block
buffer.writeBigUInt64BE(queryId, 8)
// byte offset in the raw disk
buffer.writeBigUInt64BE(BigInt(index) * BigInt(size), 16)
buffer.writeInt32BE(size, 24)
return new Promise((resolve, reject) => {
function decoratedReject(error) {
error.index = index
error.size = size
reject(error)
}
// this will handle one block response, but it can be another block
// since server does not guaranty to handle query in order
this.#commandQueryBacklog.set(queryId, {
size,
resolve,
reject: decoratedReject,
})
// really send the command to the server
this.#write(buffer).catch(decoratedReject)
// #readBlockResponse never throws directly
// but if it fails it will reject all the promises in the backlog
this.#readBlockResponse()
})
}
async *readBlocks(indexGenerator = 2*1024*1024) {
// default : read all blocks
if (typeof indexGenerator === 'number') {
const exportSize = this.#exportSize
const chunkSize = indexGenerator
indexGenerator = function* () {
const nbBlocks = Math.ceil(Number(exportSize / BigInt(chunkSize)))
for (let index = 0; BigInt(index) < nbBlocks; index++) {
yield { index, size: chunkSize }
}
}
}
const readAhead = []
const readAheadMaxLength = this.#readAhead
const makeReadBlockPromise = (index, size) => {
const promise = pRetry(() => this.readBlock(index, size), {
tries: this.#readBlockRetries,
onRetry: async err => {
console.error(err)
warn('will retry reading block ', index, err)
await this.reconnect()
},
})
// error is handled during unshift
promise.catch(() => {})
return promise
}
// read all blocks, but try to keep readAheadMaxLength promise waiting ahead
for (const { index, size } of indexGenerator()) {
// stack readAheadMaxLength promises before starting to handle the results
if (readAhead.length === readAheadMaxLength) {
// any error will stop reading blocks
yield readAhead.shift()
}
readAhead.push(makeReadBlockPromise(index, size))
}
while (readAhead.length > 0) {
yield readAhead.shift()
}
}
}

View File

@@ -1,81 +0,0 @@
// https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
export const INIT_PASSWD = Buffer.from('NBDMAGIC') // "NBDMAGIC" ensure we're connected to a nbd server
export const OPTS_MAGIC = Buffer.from('IHAVEOPT') // "IHAVEOPT" start an option block
export const NBD_OPT_REPLY_MAGIC = 1100100111001001n // magic received during negociation
export const NBD_OPT_EXPORT_NAME = 1
export const NBD_OPT_ABORT = 2
export const NBD_OPT_LIST = 3
export const NBD_OPT_STARTTLS = 5
export const NBD_OPT_INFO = 6
export const NBD_OPT_GO = 7
export const NBD_OPT_STRUCTURED_REPLY = 8
export const NBD_OPT_LIST_META_CONTEXT = 9
export const NBD_OPT_SET_META_CONTEXT = 10
export const NBD_OPT_EXTENDED_HEADERS = 11
export const NBD_REP_ACK =1
export const NBD_REP_SERVER = 2
export const NBD_REP_INFO = 3
export const NBD_REP_META_CONTEXT = 4
export const NBD_REP_ERR_UNSUP = 0x80000001 // 2^32+1
export const NBD_REP_ERR_POLICY = 0x80000002
export const NBD_REP_ERR_INVALID = 0x80000003
export const NBD_REP_ERR_PLATFORM = 0x80000004
export const NBD_REP_ERR_TLS_REQD = 0x80000005
export const NBD_REP_ERR_UNKNOWN = 0x80000006
export const NBD_REP_ERR_SHUTDOWN = 0x80000007
export const NBD_REP_ERR_BLOCK_SIZE_REQD = 0x80000008
export const NBD_REP_ERR_TOO_BIG = 0x80000009
export const NBD_REP_ERR_EXT_HEADER_REQD = 0x8000000a
export const NBD_INFO_EXPORT = 0
export const NBD_INFO_NAME = 1
export const NBD_INFO_DESCRIPTION = 2
export const NBD_INFO_BLOCK_SIZE = 3
export const NBD_FLAG_HAS_FLAGS = 1 << 0
export const NBD_FLAG_READ_ONLY = 1 << 1
export const NBD_FLAG_SEND_FLUSH = 1 << 2
export const NBD_FLAG_SEND_FUA = 1 << 3
export const NBD_FLAG_ROTATIONAL = 1 << 4
export const NBD_FLAG_SEND_TRIM = 1 << 5
export const NBD_FLAG_SEND_WRITE_ZEROES = 1 << 6
export const NBD_FLAG_SEND_DF = 1 << 7
export const NBD_FLAG_CAN_MULTI_CONN = 1 << 8
export const NBD_FLAG_FIXED_NEWSTYLE = 1 << 0
export const NBD_CMD_FLAG_FUA = 1 << 0
export const NBD_CMD_FLAG_NO_HOLE = 1 << 1
export const NBD_CMD_FLAG_DF = 1 << 2
export const NBD_CMD_FLAG_REQ_ONE = 1 << 3
export const NBD_CMD_FLAG_FAST_ZERO = 1 << 4
export const NBD_CMD_READ = 0
export const NBD_CMD_WRITE = 1
export const NBD_CMD_DISC = 2
export const NBD_CMD_FLUSH = 3
export const NBD_CMD_TRIM = 4
export const NBD_CMD_CACHE = 5
export const NBD_CMD_WRITE_ZEROES = 6
export const NBD_CMD_BLOCK_STATUS = 7
export const NBD_CMD_RESIZE = 8
export const NBD_REQUEST_MAGIC = 0x25609513 // magic number to create a new NBD request to send to the server
export const NBD_REPLY_MAGIC = 0x67446698 // magic number received from the server when reading response to a nbd request
export const NBD_REPLY_ACK = 1
export const NBD_SIMPLE_REPLY_MAGIC = 0x67446698
export const NBD_STRUCTURED_REPLY_MAGIC = 0x668e33ef
export const NBD_REPLY_TYPE_NONE = 0
export const NBD_REPLY_TYPE_OFFSET_DATA = 1
export const NBD_REPLY_TYPE_OFFSET_HOLE = 2
export const NBD_REPLY_TYPE_BLOCK_STATUS = 5
export const NBD_REPLY_TYPE_ERROR = 1 << 15 +1
export const NBD_REPLY_TYPE_ERROR_OFFSET = 1 << 15 +2
export const NBD_DEFAULT_PORT = 10809
export const NBD_DEFAULT_BLOCK_SIZE = 64 * 1024

View File

@@ -1,37 +0,0 @@
{
"private": false,
"name": "@vates/nbd-client",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/nbd-client",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/nbd-client",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "2.0.0",
"engines": {
"node": ">=14.0"
},
"main": "./index.mjs",
"dependencies": {
"@vates/async-each": "^1.0.0",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"promise-toolbox": "^0.21.0",
"xen-api": "^1.3.4"
},
"devDependencies": {
"tap": "^16.3.0",
"tmp": "^0.2.1"
},
"scripts": {
"postversion": "npm publish --access public",
"test-integration": "tap --lines 97 --functions 95 --branches 74 --statements 97 tests/*.integ.mjs"
}
}

View File

@@ -1,292 +0,0 @@
import assert, { deepEqual, strictEqual, notStrictEqual } from 'node:assert'
import { createServer } from 'node:net'
import { fromCallback } from 'promise-toolbox'
import { readChunkStrict } from '@vates/read-chunk'
import {
INIT_PASSWD,
NBD_CMD_READ,
NBD_DEFAULT_PORT,
NBD_FLAG_FIXED_NEWSTYLE,
NBD_FLAG_HAS_FLAGS,
NBD_OPT_EXPORT_NAME,
NBD_OPT_REPLY_MAGIC,
NBD_REPLY_ACK,
NBD_REQUEST_MAGIC,
OPTS_MAGIC,
NBD_CMD_DISC,
NBD_REP_ERR_UNSUP,
NBD_CMD_WRITE,
NBD_OPT_GO,
NBD_OPT_INFO,
NBD_INFO_EXPORT,
NBD_REP_INFO,
NBD_SIMPLE_REPLY_MAGIC,
NBD_REP_ERR_UNKNOWN,
} from './constants.mjs'
import { PassThrough } from 'node:stream'
export default class NbdServer {
#server
#clients = new Map()
constructor(port = NBD_DEFAULT_PORT) {
this.#server = createServer()
this.#server.listen(port)
this.#server.on('connection', client => this.#handleNewConnection(client))
}
// will wait for a client to connect and upload the file to this server
downloadStream(key) {
strictEqual(this.#clients.has(key), false)
const stream = new PassThrough()
const offset = BigInt(0)
this.#clients.set(key, { length: BigInt(2 * 1024 * 1024 * 1024 * 1024), stream, offset, key })
return stream
}
// will wait for a client to connect and downlaod this stream
uploadStream(key, source, length) {
strictEqual(this.#clients.has(key), false)
notStrictEqual(length, undefined)
const offset = BigInt(0)
this.#clients.set(key, { length: BigInt(length), stream: source, offset, key })
}
#read(socket, length) {
return readChunkStrict(socket, length)
}
async #readInt32(socket) {
const buffer = await this.#read(socket, 4)
return buffer.readUInt32BE()
}
#write(socket, buffer) {
return fromCallback.call(socket, 'write', buffer)
}
async #writeInt16(socket, int16) {
const buffer = Buffer.alloc(2)
buffer.writeUInt16BE(int16)
return this.#write(socket, buffer)
}
async #writeInt32(socket, int32) {
const buffer = Buffer.alloc(4)
buffer.writeUInt32BE(int32)
return this.#write(socket, buffer)
}
async #writeInt64(socket, int64) {
const buffer = Buffer.alloc(8)
buffer.writeBigUInt64BE(int64)
return this.#write(socket, buffer)
}
async #openExport(key) {
if (!this.#clients.has(key)) {
// export does not exists
const err = new Error('Export not found ')
err.code = 'ENOTFOUND'
throw err
}
const { length } = this.#clients.get(key)
return length
}
async #sendOptionResponse(socket, option, response, data = Buffer.alloc(0)) {
await this.#writeInt64(socket, NBD_OPT_REPLY_MAGIC)
await this.#writeInt32(socket, option)
await this.#writeInt32(socket, response)
await this.#writeInt32(socket, data.length)
await this.#write(socket, data)
}
/**
*
* @param {Socket} socket
* @returns true if server is waiting for more options
*/
async #readOption(socket) {
console.log('wait for option')
const magic = await this.#read(socket, 8)
console.log(magic.toString('ascii'), magic.length, OPTS_MAGIC.toString('ascii'))
deepEqual(magic, OPTS_MAGIC)
const option = await this.#readInt32(socket)
const length = await this.#readInt32(socket)
console.log({ option, length })
const data = length > 0 ? await this.#read(socket, length) : undefined
switch (option) {
case NBD_OPT_EXPORT_NAME: {
const exportNameLength = data.readInt32BE()
const key = data.slice(4, exportNameLength + 4).toString()
let exportSize
try {
exportSize = await this.#openExport(key)
} catch (err) {
if (err.code === 'ENOTFOUND') {
this.#sendOptionResponse(socket, option, NBD_REP_ERR_UNKNOWN)
return false
}
throw err
}
socket.key = key
await this.#writeInt64(socket, exportSize)
await this.#writeInt16(socket, NBD_FLAG_HAS_FLAGS /* transmission flag */)
await this.#write(socket, Buffer.alloc(124) /* padding */)
return false
}
/*
case NBD_OPT_STARTTLS:
console.log('starttls')
// @todo not working
return true
*/
case NBD_OPT_GO:
case NBD_OPT_INFO: {
const exportNameLength = data.readInt32BE()
const key = data.slice(4, exportNameLength + 4).toString()
let exportSize
try {
exportSize = await this.#openExport(key)
} catch (err) {
if (err.code === 'ENOTFOUND') {
this.#sendOptionResponse(socket, option, NBD_REP_ERR_UNKNOWN)
// @todo should disconnect
return false
}
throw err
}
socket.key = key
await this.#writeInt64(socket, NBD_OPT_REPLY_MAGIC)
await this.#writeInt32(socket, option)
await this.#writeInt32(socket, NBD_REP_INFO)
await this.#writeInt32(socket, 12)
// the export info
await this.#writeInt16(socket, NBD_INFO_EXPORT)
await this.#writeInt64(socket, exportSize)
await this.#writeInt16(socket, NBD_FLAG_HAS_FLAGS /* transmission flag */)
// an ACK at the end of the infos
await this.#sendOptionResponse(socket, option, NBD_REPLY_ACK) // no additionnal data
return option === NBD_OPT_INFO // we stays in option phase is option is INFO
}
default:
// not supported
console.log('not supported', option, length, data?.toString())
await this.#sendOptionResponse(socket, option, NBD_REP_ERR_UNSUP) // no additionnal data
// wait for next option
return true
}
}
async #readCommand(socket) {
const key = socket.key
// this socket has an export key
notStrictEqual(key, undefined)
// this export key is still valid
strictEqual(this.#clients.has(key), true)
const client = this.#clients.get(key)
const buffer = await this.#read(socket, 28)
const magic = buffer.readInt32BE(0)
strictEqual(magic, NBD_REQUEST_MAGIC)
/* const commandFlags = */ buffer.readInt16BE(4)
const command = buffer.readInt16BE(6)
const cookie = buffer.readBigUInt64BE(8)
const offset = buffer.readBigUInt64BE(16)
const length = buffer.readInt32BE(24)
switch (command) {
case NBD_CMD_DISC:
console.log('gotdisconnect', client.offset)
await client.stream?.destroy()
// @todo : disconnect
return false
case NBD_CMD_READ: {
/** simple replies */
// read length byte from offset in export
// the client is writing in contiguous mode
assert.strictEqual(offset, client.offset)
client.offset += BigInt(length)
const data = await readChunkStrict(client.stream, length)
const reply = Buffer.alloc(16)
reply.writeInt32BE(NBD_SIMPLE_REPLY_MAGIC)
reply.writeInt32BE(0, 4) // no error
reply.writeBigInt64BE(cookie, 8)
await this.#write(socket, reply)
await this.#write(socket, data)
/* if we implement non stream read, we can handle read in parallel
const reply = Buffer.alloc(16+length)
reply.writeInt32BE(NBD_SIMPLE_REPLY_MAGIC)
reply.writeInt32BE(0,4)// no error
reply.writeBigInt64BE(cookie,8)
// read length byte from offset in export directly in the given buffer
// may do multiple read in parallel on the same export
size += length
socket.fd.read(reply, 16, length, Number(offset))
.then(()=>{
return this.#write(socket, reply)
})
.catch(err => console.error('NBD_CMD_READ',err)) */
return true
}
case NBD_CMD_WRITE: {
// the client is writing in contiguous mode
assert.strictEqual(offset, client.offset)
const data = await this.#read(socket, length)
client.offset += BigInt(length)
await new Promise((resolve, reject) => {
if (!client.stream.write(data, 0, length, Number(offset))) {
client.stream.once('drain', err => (err ? reject(err) : resolve()))
} else {
process.nextTick(resolve)
}
})
const reply = Buffer.alloc(16)
reply.writeInt32BE(NBD_SIMPLE_REPLY_MAGIC)
reply.writeInt32BE(0, 4) // no error
reply.writeBigInt64BE(cookie, 8)
await this.#write(socket, reply)
return true
}
default:
console.log('GOT unsupported command ', command)
// fail to handle
return true
}
}
async #handleNewConnection(socket) {
const remoteAddress = socket.remoteAddress + ':' + socket.remotePort
console.log('new client connection from %s', remoteAddress)
socket.on('close', () => {
console.log('client ', remoteAddress, 'is done')
})
socket.on('error', error => {
throw error
})
// handshake
await this.#write(socket, INIT_PASSWD)
await this.#write(socket, OPTS_MAGIC)
// send flags , the bare minimum
await this.#writeInt16(socket, NBD_FLAG_FIXED_NEWSTYLE)
const clientFlag = await this.#readInt32(socket)
assert.strictEqual(clientFlag & NBD_FLAG_FIXED_NEWSTYLE, NBD_FLAG_FIXED_NEWSTYLE) // only FIXED_NEWSTYLE one is supported from the server options
// read client response flags
let waitingForOptions = true
while (waitingForOptions) {
waitingForOptions = await this.#readOption(socket)
}
let waitingForCommand = true
while (waitingForCommand) {
waitingForCommand = await this.#readCommand(socket)
}
}
#handleClientData(client, data) {}
}

View File

@@ -1,182 +0,0 @@
Public Key Info:
Public Key Algorithm: RSA
Key Security Level: High (3072 bits)
modulus:
00:be:92:be:df:de:0a:ab:38:fc:1a:c0:1a:58:4d:86
b8:1f:25:10:7d:19:05:17:bf:02:3d:e9:ef:f8:c0:04
5d:6f:98:de:5c:dd:c3:0f:e2:61:61:e4:b5:9c:42:ac
3e:af:fd:30:10:e1:54:32:66:75:f6:80:90:85:05:a0
6a:14:a2:6f:a7:2e:f0:f3:52:94:2a:f2:34:fc:0d:b4
fb:28:5d:1c:11:5c:59:6e:63:34:ba:b3:fd:73:b1:48
35:00:84:53:da:6a:9b:84:ab:64:b1:a1:2b:3a:d1:5a
d7:13:7c:12:2a:4e:72:e9:96:d6:30:74:c5:71:05:14
4b:2d:01:94:23:67:4e:37:3c:1e:c1:a0:bc:34:04:25
21:11:fb:4b:6b:53:74:8f:90:93:57:af:7f:3b:78:d6
a4:87:fe:7d:ed:20:11:8b:70:54:67:b8:c9:f5:c0:6b
de:4e:e7:a5:79:ff:f7:ad:cf:10:57:f5:51:70:7b:54
68:28:9e:b9:c2:10:7b:ab:aa:11:47:9f:ec:e6:2f:09
44:4a:88:5b:dd:8c:10:b4:c4:03:25:06:d9:e0:9f:a0
0d:cf:94:4b:3b:fa:a5:17:2c:e4:67:c4:17:6a:ab:d8
c8:7a:16:41:b9:91:b7:9c:ae:8c:94:be:26:61:51:71
c1:a6:39:39:97:75:28:a9:0e:21:ea:f0:bd:71:4a:8c
e1:f8:1d:a9:22:2f:10:a8:1b:e5:a4:9a:fd:0f:fa:c6
20:bc:96:99:79:c6:ba:a4:1f:3e:d4:91:c5:af:bb:71
0a:5a:ef:69:9c:64:69:ce:5a:fe:3f:c2:24:f4:26:d4
3d:ab:ab:9a:f0:f6:f1:b1:64:a9:f4:e2:34:6a:ab:2e
95:47:b9:07:5a:39:c6:95:9c:a9:e8:ed:71:dd:c1:21
16:c8:2d:4c:2c:af:06:9d:c6:fa:fe:c5:2a:6c:b4:c3
d5:96:fc:5e:fd:ec:1c:30:b4:9d:cb:29:ef:a8:50:1c
21:
public exponent:
01:00:01:
private exponent:
25:37:c5:7d:35:01:02:65:73:9e:c9:cb:9b:59:30:a9
3e:b3:df:5f:7f:06:66:97:d0:19:45:59:af:4b:d8:ce
62:a0:09:35:3b:bd:ff:99:27:89:95:bf:fe:0f:6b:52
26:ce:9c:97:7f:5a:11:29:bf:79:ef:ab:c9:be:ca:90
4d:0d:58:1e:df:65:01:30:2c:6d:a2:b5:c4:4f:ec:fb
6b:eb:9b:32:ac:c5:6e:70:83:78:be:f4:0d:a7:1e:c1
f3:22:e4:b9:70:3e:85:0f:6f:ef:dc:d8:f3:78:b5:73
f1:83:36:8c:fa:9b:28:91:63:ad:3c:f0:de:5c:ae:94
eb:ea:36:03:20:06:bf:74:c7:50:eb:52:36:1a:65:21
eb:40:17:7f:93:61:dd:33:d0:02:bc:ec:6d:31:f1:41
5a:a9:d1:f0:00:66:4c:c4:18:47:d5:67:e3:cd:bb:83
44:07:ab:62:83:21:dc:d8:e6:89:37:08:bb:9d:ea:62
c2:5d:ce:85:c2:dc:48:27:0c:a4:23:61:b7:30:e7:26
44:dc:1e:5c:2e:16:35:2b:2e:a6:e6:a4:ce:1f:9b:e9
fe:96:fa:49:1d:fb:2a:df:bc:bf:46:da:52:f8:37:8a
84:ab:e4:73:e6:46:56:b5:b4:3d:e1:63:eb:02:8e:d7
67:96:c4:dc:28:6d:6b:b6:0c:a3:0b:db:87:29:ad:f9
ec:73:b6:55:a3:40:32:13:84:c7:2f:33:74:04:dc:42
00:11:9c:fb:fc:62:35:b3:82:c3:3c:28:80:e8:09:a8
97:c7:c1:2e:3d:27:fa:4f:9b:fc:c2:34:58:41:5c:a1
e2:70:2e:2f:82:ad:bd:bd:8e:dd:23:12:25:de:89:70
60:75:48:90:80:ac:55:74:51:6f:49:9e:7f:63:41:8b
3c:b1:f5:c3:6b:4b:5a:50:a6:4d:38:e8:82:c2:04:c8
30:fd:06:9b:c1:04:27:b6:63:3a:5e:f5:4d:00:c3:d1
prime1:
00:f6:00:2e:7d:89:61:24:16:5e:87:ca:18:6c:03:b8
b4:33:df:4a:a7:7f:db:ed:39:15:41:12:61:4f:4e:b4
de:ab:29:d9:0c:6c:01:7e:53:2e:ee:e7:5f:a2:e4:6d
c6:4b:07:4e:d8:a3:ae:45:06:97:bd:18:a3:e9:dd:29
54:64:6d:f0:af:08:95:ae:ae:3e:71:63:76:2a:a1:18
c4:b1:fc:bc:3d:42:15:74:b3:c5:38:1f:5d:92:f1:b2
c6:3f:10:fe:35:1a:c6:b1:ce:70:38:ff:08:5c:de:61
79:c7:50:91:22:4d:e9:c8:18:49:e2:5c:91:84:86:e2
4d:0f:6e:9b:0d:81:df:aa:f3:59:75:56:e9:33:18:dd
ab:39:da:e2:25:01:05:a1:6e:23:59:15:2c:89:35:c7
ae:9c:c7:ea:88:9a:1a:f3:48:07:11:82:59:79:8c:62
53:06:37:30:14:b3:82:b1:50:fc:ae:b8:f7:1c:57:44
7d:
prime2:
00:c6:51:cc:dc:88:2e:cf:98:90:10:19:e0:d3:a4:d1
3f:dc:b0:29:d3:bb:26:ee:eb:00:17:17:d1:d1:bb:9b
34:b1:4e:af:b5:6c:1c:54:53:b4:bb:55:da:f7:78:cd
38:b4:2e:3a:8c:63:80:3b:64:9c:b4:2b:cd:dd:50:0b
05:d2:00:7a:df:8e:c3:e6:29:e0:9c:d8:40:b7:11:09
f4:38:df:f6:ed:93:1e:18:d4:93:fa:8d:ee:82:9c:0f
c1:88:26:84:9d:4f:ae:8a:17:d5:55:54:4c:c6:0a:ac
4d:ec:33:51:68:0f:4b:92:2e:04:57:fe:15:f5:00:46
5c:8e:ad:09:2c:e7:df:d5:36:7a:4e:bd:da:21:22:d7
58:b4:72:93:94:af:34:cc:e2:b8:d0:4f:0b:5d:97:08
12:19:17:34:c5:15:49:00:48:56:13:b8:45:4e:3b:f8
bc:d5:ab:d9:6d:c2:4a:cc:01:1a:53:4d:46:50:49:3b
75:
coefficient:
63:67:50:29:10:6a:85:a3:dc:51:90:20:76:86:8c:83
8e:d5:ff:aa:75:fd:b5:f8:31:b0:96:6c:18:1d:5b:ed
a4:2e:47:8d:9c:c2:1e:2c:a8:6d:4b:10:a5:c2:53:46
8a:9a:84:91:d7:fc:f5:cc:03:ce:b9:3d:5c:01:d2:27
99:7b:79:89:4f:a1:12:e3:05:5d:ee:10:f6:8c:e6:ce
5e:da:32:56:6d:6f:eb:32:b4:75:7b:94:49:d8:2d:9e
4d:19:59:2e:e4:0b:bc:95:df:df:65:67:a1:dd:c6:2b
99:f4:76:e8:9f:fa:57:1d:ca:f9:58:a9:ce:9b:30:5c
42:8a:ba:05:e7:e2:15:45:25:bc:e9:68:c1:8b:1a:37
cc:e1:aa:45:2e:94:f5:81:47:1e:64:7f:c0:c1:b7:a8
21:58:18:a9:a0:ed:e0:27:75:bf:65:81:6b:e4:1d:5a
b7:7e:df:d8:28:c6:36:21:19:c8:6e:da:ca:9e:da:84
exp1:
00:ba:d7:fe:77:a9:0d:98:2c:49:56:57:c0:5e:e2:20
ba:f6:1f:26:03:bc:d0:5d:08:9b:45:16:61:c4:ab:e2
22:b1:dc:92:17:a6:3d:28:26:a4:22:1e:a8:7b:ff:86
05:33:5d:74:9c:85:0d:cb:2d:ab:b8:9b:6b:7c:28:57
c8:da:92:ca:59:17:6b:21:07:05:34:78:37:fb:3e:ea
a2:13:12:04:23:7e:fa:ee:ed:cf:e0:c5:a9:fb:ff:0a
2b:1b:21:9c:02:d7:b8:8c:ba:60:70:59:fc:8f:14:f4
f2:5a:d9:ad:b2:61:7d:2c:56:8e:5f:98:b1:89:f8:2d
10:1c:a5:84:ad:28:b4:aa:92:34:a3:34:04:e1:a3:84
52:16:1a:52:e3:8a:38:2d:99:8a:cd:91:90:87:12:ca
fc:ab:e6:08:14:03:00:6f:41:88:e4:da:9d:7c:fd:8c
7c:c4:de:cb:ed:1d:3f:29:d0:7a:6b:76:df:71:ae:32
bd:
exp2:
4a:e9:d3:6c:ea:b4:64:0e:c9:3c:8b:c9:f5:a8:a8:b2
6a:f6:d0:95:fe:78:32:7f:ea:c4:ce:66:9f:c7:32:55
b1:34:7c:03:18:17:8b:73:23:2e:30:bc:4a:07:03:de
8b:91:7a:e4:55:21:b7:4d:c6:33:f8:e8:06:d5:99:94
55:43:81:26:b9:93:1e:7a:6b:32:54:2d:fd:f9:1d:bd
77:4e:82:c4:33:72:87:06:a5:ef:5b:75:e1:38:7a:6b
2c:b7:00:19:3c:64:3e:1d:ca:a4:34:f7:db:47:64:d6
fa:86:58:15:ea:d1:2d:22:dc:d9:30:4d:b3:02:ab:91
83:03:b2:17:98:6f:60:e6:f7:44:8f:4a:ba:81:a2:bf
0b:4a:cc:9c:b9:a2:44:52:d0:65:3f:b6:97:5f:d9:d8
9c:49:bb:d1:46:bd:10:b2:42:71:a8:85:e5:8b:99:e6
1b:00:93:5d:76:ab:32:6c:a8:39:17:53:9c:38:4d:91
Public Key PIN:
pin-sha256:ISh/UeFjUG5Gwrpx6hMUGQPvg9wOKjOkHmRbs4YjZqs=
Public Key ID:
sha256:21287f51e163506e46c2ba71ea13141903ef83dc0e2a33a41e645bb3862366ab
sha1:1a48455111ac45fb5807c5cdb7b20b896c52f0b6
-----BEGIN RSA PRIVATE KEY-----
MIIG4wIBAAKCAYEAvpK+394Kqzj8GsAaWE2GuB8lEH0ZBRe/Aj3p7/jABF1vmN5c
3cMP4mFh5LWcQqw+r/0wEOFUMmZ19oCQhQWgahSib6cu8PNSlCryNPwNtPsoXRwR
XFluYzS6s/1zsUg1AIRT2mqbhKtksaErOtFa1xN8EipOcumW1jB0xXEFFEstAZQj
Z043PB7BoLw0BCUhEftLa1N0j5CTV69/O3jWpIf+fe0gEYtwVGe4yfXAa95O56V5
//etzxBX9VFwe1RoKJ65whB7q6oRR5/s5i8JREqIW92MELTEAyUG2eCfoA3PlEs7
+qUXLORnxBdqq9jIehZBuZG3nK6MlL4mYVFxwaY5OZd1KKkOIerwvXFKjOH4Haki
LxCoG+Wkmv0P+sYgvJaZeca6pB8+1JHFr7txClrvaZxkac5a/j/CJPQm1D2rq5rw
9vGxZKn04jRqqy6VR7kHWjnGlZyp6O1x3cEhFsgtTCyvBp3G+v7FKmy0w9WW/F79
7BwwtJ3LKe+oUBwhAgMBAAECggGAJTfFfTUBAmVznsnLm1kwqT6z319/BmaX0BlF
Wa9L2M5ioAk1O73/mSeJlb/+D2tSJs6cl39aESm/ee+ryb7KkE0NWB7fZQEwLG2i
tcRP7Ptr65syrMVucIN4vvQNpx7B8yLkuXA+hQ9v79zY83i1c/GDNoz6myiRY608
8N5crpTr6jYDIAa/dMdQ61I2GmUh60AXf5Nh3TPQArzsbTHxQVqp0fAAZkzEGEfV
Z+PNu4NEB6tigyHc2OaJNwi7nepiwl3OhcLcSCcMpCNhtzDnJkTcHlwuFjUrLqbm
pM4fm+n+lvpJHfsq37y/RtpS+DeKhKvkc+ZGVrW0PeFj6wKO12eWxNwobWu2DKML
24cprfnsc7ZVo0AyE4THLzN0BNxCABGc+/xiNbOCwzwogOgJqJfHwS49J/pPm/zC
NFhBXKHicC4vgq29vY7dIxIl3olwYHVIkICsVXRRb0mef2NBizyx9cNrS1pQpk04
6ILCBMgw/QabwQQntmM6XvVNAMPRAoHBAPYALn2JYSQWXofKGGwDuLQz30qnf9vt
ORVBEmFPTrTeqynZDGwBflMu7udfouRtxksHTtijrkUGl70Yo+ndKVRkbfCvCJWu
rj5xY3YqoRjEsfy8PUIVdLPFOB9dkvGyxj8Q/jUaxrHOcDj/CFzeYXnHUJEiTenI
GEniXJGEhuJND26bDYHfqvNZdVbpMxjdqzna4iUBBaFuI1kVLIk1x66cx+qImhrz
SAcRgll5jGJTBjcwFLOCsVD8rrj3HFdEfQKBwQDGUczciC7PmJAQGeDTpNE/3LAp
07sm7usAFxfR0bubNLFOr7VsHFRTtLtV2vd4zTi0LjqMY4A7ZJy0K83dUAsF0gB6
347D5ingnNhAtxEJ9Djf9u2THhjUk/qN7oKcD8GIJoSdT66KF9VVVEzGCqxN7DNR
aA9Lki4EV/4V9QBGXI6tCSzn39U2ek692iEi11i0cpOUrzTM4rjQTwtdlwgSGRc0
xRVJAEhWE7hFTjv4vNWr2W3CSswBGlNNRlBJO3UCgcEAutf+d6kNmCxJVlfAXuIg
uvYfJgO80F0Im0UWYcSr4iKx3JIXpj0oJqQiHqh7/4YFM110nIUNyy2ruJtrfChX
yNqSylkXayEHBTR4N/s+6qITEgQjfvru7c/gxan7/worGyGcAte4jLpgcFn8jxT0
8lrZrbJhfSxWjl+YsYn4LRAcpYStKLSqkjSjNATho4RSFhpS44o4LZmKzZGQhxLK
/KvmCBQDAG9BiOTanXz9jHzE3svtHT8p0Hprdt9xrjK9AoHASunTbOq0ZA7JPIvJ
9aiosmr20JX+eDJ/6sTOZp/HMlWxNHwDGBeLcyMuMLxKBwPei5F65FUht03GM/jo
BtWZlFVDgSa5kx56azJULf35Hb13ToLEM3KHBqXvW3XhOHprLLcAGTxkPh3KpDT3
20dk1vqGWBXq0S0i3NkwTbMCq5GDA7IXmG9g5vdEj0q6gaK/C0rMnLmiRFLQZT+2
l1/Z2JxJu9FGvRCyQnGoheWLmeYbAJNddqsybKg5F1OcOE2RAoHAY2dQKRBqhaPc
UZAgdoaMg47V/6p1/bX4MbCWbBgdW+2kLkeNnMIeLKhtSxClwlNGipqEkdf89cwD
zrk9XAHSJ5l7eYlPoRLjBV3uEPaM5s5e2jJWbW/rMrR1e5RJ2C2eTRlZLuQLvJXf
32Vnod3GK5n0duif+lcdyvlYqc6bMFxCiroF5+IVRSW86WjBixo3zOGqRS6U9YFH
HmR/wMG3qCFYGKmg7eAndb9lgWvkHVq3ft/YKMY2IRnIbtrKntqE
-----END RSA PRIVATE KEY-----

View File

@@ -1,168 +0,0 @@
import NbdClient from '../client.mjs'
import { spawn, exec } from 'node:child_process'
import fs from 'node:fs/promises'
import { test } from 'tap'
import tmp from 'tmp'
import { pFromCallback } from 'promise-toolbox'
import { Socket } from 'node:net'
import { NBD_DEFAULT_PORT } from '../constants.mjs'
import assert from 'node:assert'
const FILE_SIZE = 10 * 1024 * 1024
async function createTempFile(size) {
const tmpPath = await pFromCallback(cb => tmp.file(cb))
const data = Buffer.alloc(size, 0)
for (let i = 0; i < size; i += 4) {
data.writeUInt32BE(i, i)
}
await fs.writeFile(tmpPath, data)
return tmpPath
}
async function spawnNbdKit(path) {
let tries = 5
// wait for server to be ready
const nbdServer = spawn(
'nbdkit',
[
'file',
path,
'--newstyle', //
'--exit-with-parent',
'--read-only',
'--export-name=MY_SECRET_EXPORT',
'--tls=on',
'--tls-certificates=./tests/',
// '--tls-verify-peer',
// '--verbose',
'--exit-with-parent',
],
{
stdio: ['inherit', 'inherit', 'inherit'],
}
)
nbdServer.on('error', err => {
console.error(err)
})
do {
try {
const socket = new Socket()
await new Promise((resolve, reject) => {
socket.connect(NBD_DEFAULT_PORT, 'localhost')
socket.once('error', reject)
socket.once('connect', resolve)
})
socket.destroy()
break
} catch (err) {
tries--
if (tries <= 0) {
throw err
} else {
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
} while (true)
return nbdServer
}
async function killNbdKit() {
return new Promise((resolve, reject) =>
exec('pkill -9 -f -o nbdkit', err => {
err ? reject(err) : resolve()
})
)
}
test('it works with unsecured network', async tap => {
const path = await createTempFile(FILE_SIZE)
let nbdServer = await spawnNbdKit(path)
const client = new NbdClient(
{
address: '127.0.0.1',
exportname: 'MY_SECRET_EXPORT',
cert: `-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUeHpQ0IeD6BmP2zgsv3LV3J4BI/EwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA1MTcxMzU1MzBaFw0yNDA1
MTYxMzU1MzBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQC/8wLopj/iZY6ijmpvgCJsl+zY0hQZQcIoaCs0H75u
8PPSzHedtOLURAkJeMmIS40UY/eIvHh7yZolevaSJLNT2Iolscvc2W9NCF4N1V6y
zs4pDzP+YPF7Q8ldNaQIX0bAk4PfaMSM+pLh67u+uI40732AfQqD01BNCTD/uHRB
lKnQuqQpe9UM9UzRRVejpu1r19D4dJruAm6y2SJVTeT4a1sSJixl6I1YPmt80FJh
gq9O2KRGbXp1xIjemWgW99MHg63pTgxEiULwdJOGgmqGRDzgZKJS5UUpxe/ViEO4
59I18vIkgibaRYhENgmnP3lIzTOLlUe07tbSML5RGBbBAgMBAAGjUzBRMB0GA1Ud
DgQWBBR/8+zYoL0H0LdWfULHg1LynFdSbzAfBgNVHSMEGDAWgBR/8+zYoL0H0LdW
fULHg1LynFdSbzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBD
OF5bTmbDEGoZ6OuQaI0vyya/T4FeaoWmh22gLeL6dEEmUVGJ1NyMTOvG9GiGJ8OM
QhD1uHJei45/bXOYIDGey2+LwLWye7T4vtRFhf8amYh0ReyP/NV4/JoR/U3pTSH6
tns7GZ4YWdwUhvOOlm17EQKVO/hP3t9mp74gcjdL4bCe5MYSheKuNACAakC1OR0U
ZakJMP9ijvQuq8spfCzrK+NbHKNHR9tEgQw+ax/t1Au4dGVtFbcoxqCrx2kTl0RP
CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
57ABhfYpMlZ4aHjuN1bL
-----END CERTIFICATE-----
`,
},
{
readAhead: 2,
}
)
await client.connect()
tap.equal(client.exportSize, BigInt(FILE_SIZE))
const CHUNK_SIZE = 1024 * 1024 // non default size
const indexes = []
for (let i = 0; i < FILE_SIZE / CHUNK_SIZE; i++) {
indexes.push(i)
}
const nbdIterator = client.readBlocks(function* () {
for (const index of indexes) {
yield { index, size: CHUNK_SIZE }
}
})
let i = 0
for await (const block of nbdIterator) {
let blockOk = true
let firstFail
for (let j = 0; j < CHUNK_SIZE; j += 4) {
const wanted = i * CHUNK_SIZE + j
const found = block.readUInt32BE(j)
blockOk = blockOk && found === wanted
if (!blockOk && firstFail === undefined) {
firstFail = j
}
}
tap.ok(blockOk, `check block ${i} content`)
i++
// flaky server is flaky
if (i % 7 === 0) {
// kill the older nbdkit process
await killNbdKit()
nbdServer = await spawnNbdKit(path)
}
}
// we can reuse the conneciton to read other blocks
// default iterator
const nbdIteratorWithDefaultBlockIterator = client.readBlocks()
let nb = 0
for await (const block of nbdIteratorWithDefaultBlockIterator) {
nb++
tap.equal(block.length, 2 * 1024 * 1024)
}
tap.equal(nb, 5)
assert.rejects(() => client.readBlock(100, CHUNK_SIZE))
await client.disconnect()
// double disconnection shouldn't pose any problem
await client.disconnect()
nbdServer.kill()
await fs.unlink(path)
})

View File

@@ -1,21 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUeHpQ0IeD6BmP2zgsv3LV3J4BI/EwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA1MTcxMzU1MzBaFw0yNDA1
MTYxMzU1MzBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQC/8wLopj/iZY6ijmpvgCJsl+zY0hQZQcIoaCs0H75u
8PPSzHedtOLURAkJeMmIS40UY/eIvHh7yZolevaSJLNT2Iolscvc2W9NCF4N1V6y
zs4pDzP+YPF7Q8ldNaQIX0bAk4PfaMSM+pLh67u+uI40732AfQqD01BNCTD/uHRB
lKnQuqQpe9UM9UzRRVejpu1r19D4dJruAm6y2SJVTeT4a1sSJixl6I1YPmt80FJh
gq9O2KRGbXp1xIjemWgW99MHg63pTgxEiULwdJOGgmqGRDzgZKJS5UUpxe/ViEO4
59I18vIkgibaRYhENgmnP3lIzTOLlUe07tbSML5RGBbBAgMBAAGjUzBRMB0GA1Ud
DgQWBBR/8+zYoL0H0LdWfULHg1LynFdSbzAfBgNVHSMEGDAWgBR/8+zYoL0H0LdW
fULHg1LynFdSbzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBD
OF5bTmbDEGoZ6OuQaI0vyya/T4FeaoWmh22gLeL6dEEmUVGJ1NyMTOvG9GiGJ8OM
QhD1uHJei45/bXOYIDGey2+LwLWye7T4vtRFhf8amYh0ReyP/NV4/JoR/U3pTSH6
tns7GZ4YWdwUhvOOlm17EQKVO/hP3t9mp74gcjdL4bCe5MYSheKuNACAakC1OR0U
ZakJMP9ijvQuq8spfCzrK+NbHKNHR9tEgQw+ax/t1Au4dGVtFbcoxqCrx2kTl0RP
CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
57ABhfYpMlZ4aHjuN1bL
-----END CERTIFICATE-----

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/8wLopj/iZY6i
jmpvgCJsl+zY0hQZQcIoaCs0H75u8PPSzHedtOLURAkJeMmIS40UY/eIvHh7yZol
evaSJLNT2Iolscvc2W9NCF4N1V6yzs4pDzP+YPF7Q8ldNaQIX0bAk4PfaMSM+pLh
67u+uI40732AfQqD01BNCTD/uHRBlKnQuqQpe9UM9UzRRVejpu1r19D4dJruAm6y
2SJVTeT4a1sSJixl6I1YPmt80FJhgq9O2KRGbXp1xIjemWgW99MHg63pTgxEiULw
dJOGgmqGRDzgZKJS5UUpxe/ViEO459I18vIkgibaRYhENgmnP3lIzTOLlUe07tbS
ML5RGBbBAgMBAAECggEATLYiafcTHfgnZmjTOad0WoDnC4n9tVBV948WARlUooLS
duL3RQRHCLz9/ZaTuFA1XDpNcYyc/B/IZoU7aJGZR3+JSmJBjowpUphu+klVNNG4
i6lDRrzYlUI0hfdLjHsDTDBIKi91KcB0lix/VkvsrVQvDHwsiR2ZAIiVWAWQFKrR
5O3DhSTHbqyq47uR58rWr4Zf3zvZaUl841AS1yELzCiZqz7AenvyWphim0c0XA5d
I63CEShntHnEAA9OMcP8+BNf/3AmqB4welY+m8elB3aJNH+j7DKq/AWqaM5nl2PC
cS6qgpxwOyTxEOyj1xhwK5ZMRR3heW3NfutIxSOPlwKBgQDB9ZkrBeeGVtCISO7C
eCANzSLpeVrahTvaCSQLdPHsLRLDUc+5mxdpi3CaRlzYs3S1OWdAtyWX9mBryltF
qDPhCNjFDyHok4D3wLEWdS9oUVwEKUM8fOPW3tXLLiMM7p4862Qo7LqnqHzPqsnz
22iZo5yjcc7aLJ+VmFrbAowwOwKBgQD9WNCvczTd7Ymn7zEvdiAyNoS0OZ0orwEJ
zGaxtjqVguGklNfrb/UB+eKNGE80+YnMiSaFc9IQPetLntZdV0L7kWYdCI8kGDNA
DbVRCOp+z8DwAojlrb/zsYu23anQozT3WeHxVU66lNuyEQvSW2tJa8gN1htrD7uY
5KLibYrBMwKBgEM0iiHyJcrSgeb2/mO7o7+keJhVSDm3OInP6QFfQAQJihrLWiKB
rpcPjbCm+LzNUX8JqNEvpIMHB1nR/9Ye9frfSdzd5W3kzicKSVHywL5wkmWOtpFa
5Mcq5wFDtzlf5MxO86GKhRJauwRptRgdyhySKFApuva1x4XaCIEiXNjJAoGBAN82
t3c+HCBEv3o05rMYcrmLC1T3Rh6oQlPtwbVmByvfywsFEVCgrc/16MPD3VWhXuXV
GRmPuE8THxLbead30M5xhvShq+xzXgRbj5s8Lc9ZIHbW5OLoOS1vCtgtaQcoJOyi
Rs4pCVqe+QpktnO6lEZ2Libys+maTQEiwNibBxu9AoGAUG1V5aKMoXa7pmGeuFR6
ES+1NDiCt6yDq9BsLZ+e2uqvWTkvTGLLwvH6xf9a0pnnILd0AUTKAAaoUdZS6++E
cGob7fxMwEE+UETp0QBgLtfjtExMOFwr2avw8PV4CYEUkPUAm2OFB2Twh+d/PNfr
FAxF1rN47SBPNbFI8N4TFsg=
-----END PRIVATE KEY-----

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 reedog117
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,127 +0,0 @@
forked from https://github.com/reedog117/node-vsphere-soap
# node-vsphere-soap
[![Join the chat at https://gitter.im/reedog117/node-vsphere-soap](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/reedog117/node-vsphere-soap?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
This is a Node.js module to connect to VMware vCenter servers and/or ESXi hosts and perform operations using the [vSphere Web Services API]. If you're feeling really adventurous, you can use this module to port vSphere operations from other languages (such as the Perl, Python, and Go libraries that exist) and have fully native Node.js code controlling your VMware virtual infrastructure!
This is very much in alpha.
## Authors
- Patrick C - [@reedog117]
## Version
0.0.2-5
## Installation
```sh
$ npm install node-vsphere-soap --save
```
## Sample Code
### To connect to a vCenter server:
var nvs = require('node-vsphere-soap');
var vc = new nvs.Client(host, user, password, sslVerify);
vc.once('ready', function() {
// perform work here
});
vc.once('error', function(err) {
// handle error here
});
#### Arguments
- host = hostname or IP of vCenter/ESX/ESXi server
- user = username
- password = password
- sslVerify = true|false - set to false if you have self-signed/unverified certificates
#### Events
- ready = emits when session authenticated with server
- error = emits when there's an error
- _err_ contains the error
#### Client instance variables
- serviceContent - ServiceContent object retrieved by RetrieveServiceContent API call
- userName - username of authenticated user
- fullName - full name of authenticated user
### To run a command:
var vcCmd = vc.runCommand( commandToRun, arguments );
vcCmd.once('result', function( result, raw, soapHeader) {
// handle results
});
vcCmd.once('error', function( err) {
// handle errors
});
#### Arguments
- commandToRun = Method from the vSphere API
- arguments = JSON document containing arguments to send
#### Events
- result = emits when session authenticated with server
- _result_ contains the JSON-formatted result from the server
- _raw_ contains the raw SOAP XML response from the server
- _soapHeader_ contains any soapHeaders from the server
- error = emits when there's an error
- _err_ contains the error
Make sure you check out tests/vsphere-soap.test.js for examples on how to create commands to run
## Development
node-vsphere-soap uses a number of open source projects to work properly:
- [node.js] - evented I/O for the backend
- [node-soap] - SOAP client for Node.js
- [soap-cookie] - cookie authentication for the node-soap module
- [lodash] - for quickly manipulating JSON
- [lab] - testing engine
- [code] - assertion engine used with lab
Want to contribute? Great!
### Todo's
- Write More Tests
- Create Travis CI test harness with a fake vCenter Instance
- Add Code Comments
### Testing
I have been testing on a Mac with node v0.10.36 and both ESXi and vCenter 5.5.
To edit tests, edit the file **test/vsphere-soap.test.js**
To point the module at your own vCenter/ESXi host, edit **config-test.stub.js** and save it as **config-test.js**
To run test scripts:
```sh
$ npm test
```
## License
MIT
[vSphere Web Services API]: http://pubs.vmware.com/vsphere-55/topic/com.vmware.wssdk.apiref.doc/right-pane.html
[node-soap]: https://github.com/vpulim/node-soap
[node.js]: http://nodejs.org/
[soap-cookie]: https://github.com/shanestillwell/soap-cookie
[code]: https://github.com/hapijs/code
[lab]: https://github.com/hapijs/lab
[lodash]: https://lodash.com/
[@reedog117]: http://www.twitter.com/reedog117

View File

@@ -1,230 +0,0 @@
/*
node-vsphere-soap
client.js
This file creates the Client class
- when the class is instantiated, a connection will be made to the ESXi/vCenter server to verify that the creds are good
- upon a bad login, the connnection will be terminated
*/
import { EventEmitter } from 'events'
import axios from 'axios'
import https from 'node:https'
import util from 'util'
import soap from 'soap'
import Cookie from 'soap-cookie' // required for session persistence
// Client class
// inherits from EventEmitter
// possible events: connect, error, ready
export function Client(vCenterHostname, username, password, sslVerify) {
this.status = 'disconnected'
this.reconnectCount = 0
sslVerify = typeof sslVerify !== 'undefined' ? sslVerify : false
EventEmitter.call(this)
// sslVerify argument handling
if (sslVerify) {
this.clientopts = {}
} else {
this.clientopts = {
request: axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
}),
}
}
this.connectionInfo = {
host: vCenterHostname,
user: username,
password,
sslVerify,
}
this._loginArgs = {
userName: this.connectionInfo.user,
password: this.connectionInfo.password,
}
this._vcUrl = 'https://' + this.connectionInfo.host + '/sdk/vimService.wsdl'
// connect to the vCenter / ESXi host
this.on('connect', this._connect)
this.emit('connect')
// close session
this.on('close', this._close)
return this
}
util.inherits(Client, EventEmitter)
Client.prototype.runCommand = function (command, args) {
const self = this
let cmdargs
if (!args || args === null) {
cmdargs = {}
} else {
cmdargs = args
}
const emitter = new EventEmitter()
// check if client has successfully connected
if (self.status === 'ready' || self.status === 'connecting') {
self.client.VimService.VimPort[command](cmdargs, function (err, result, raw, soapHeader) {
if (err) {
_soapErrorHandler(self, emitter, command, cmdargs, err)
}
if (command === 'Logout') {
self.status = 'disconnected'
process.removeAllListeners('beforeExit')
}
emitter.emit('result', result, raw, soapHeader)
})
} else {
// if connection not ready or connecting, reconnect to instance
if (self.status === 'disconnected') {
self.emit('connect')
}
self.once('ready', function () {
self.client.VimService.VimPort[command](cmdargs, function (err, result, raw, soapHeader) {
if (err) {
_soapErrorHandler(self, emitter, command, cmdargs, err)
}
if (command === 'Logout') {
self.status = 'disconnected'
process.removeAllListeners('beforeExit')
}
emitter.emit('result', result, raw, soapHeader)
})
})
}
return emitter
}
Client.prototype.close = function () {
const self = this
self.emit('close')
}
Client.prototype._connect = function () {
const self = this
if (self.status !== 'disconnected') {
return
}
self.status = 'connecting'
soap.createClient(
self._vcUrl,
self.clientopts,
function (err, client) {
if (err) {
self.emit('error', err)
throw err
}
self.client = client // save client for later use
self
.runCommand('RetrieveServiceContent', { _this: 'ServiceInstance' })
.once('result', function (result, raw, soapHeader) {
if (!result.returnval) {
self.status = 'disconnected'
self.emit('error', raw)
return
}
self.serviceContent = result.returnval
self.sessionManager = result.returnval.sessionManager
const loginArgs = { _this: self.sessionManager, ...self._loginArgs }
self
.runCommand('Login', loginArgs)
.once('result', function (result, raw, soapHeader) {
self.authCookie = new Cookie(client.lastResponseHeaders)
self.client.setSecurity(self.authCookie) // needed since vSphere SOAP WS uses cookies
self.userName = result.returnval.userName
self.fullName = result.returnval.fullName
self.reconnectCount = 0
self.status = 'ready'
self.emit('ready')
process.once('beforeExit', self._close)
})
.once('error', function (err) {
self.status = 'disconnected'
self.emit('error', err)
})
})
.once('error', function (err) {
self.status = 'disconnected'
self.emit('error', err)
})
},
self._vcUrl
)
}
Client.prototype._close = function () {
const self = this
if (self.status === 'ready') {
self
.runCommand('Logout', { _this: self.sessionManager })
.once('result', function () {
self.status = 'disconnected'
})
.once('error', function () {
/* don't care of error during disconnection */
self.status = 'disconnected'
})
} else {
self.status = 'disconnected'
}
}
function _soapErrorHandler(self, emitter, command, args, err) {
err = err || { body: 'general error' }
if (err.body.match(/session is not authenticated/)) {
self.status = 'disconnected'
process.removeAllListeners('beforeExit')
if (self.reconnectCount < 10) {
self.reconnectCount += 1
self
.runCommand(command, args)
.once('result', function (result, raw, soapHeader) {
emitter.emit('result', result, raw, soapHeader)
})
.once('error', function (err) {
emitter.emit('error', err.body)
throw err
})
} else {
emitter.emit('error', err.body)
throw err
}
} else {
emitter.emit('error', err.body)
throw err
}
}
// end

View File

@@ -1,38 +0,0 @@
{
"name": "@vates/node-vsphere-soap",
"version": "2.0.0",
"description": "interface to vSphere SOAP/WSDL from node for interfacing with vCenter or ESXi, forked from node-vsphere-soap",
"main": "lib/client.mjs",
"author": "reedog117",
"repository": {
"directory": "@vates/node-vsphere-soap",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"axios": "^1.4.0",
"soap": "^1.0.0",
"soap-cookie": "^0.10.1"
},
"devDependencies": {
"test": "^3.3.0"
},
"keywords": [
"vsphere",
"vcenter",
"api",
"soap",
"wsdl"
],
"preferGlobal": false,
"license": "MIT",
"private": false,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/node-vsphere-soap",
"engines": {
"node": ">=14"
},
"scripts": {
"postversion": "npm publish --access public"
}
}

View File

@@ -1,11 +0,0 @@
// place your own credentials here for a vCenter or ESXi server
// this information will be used for connecting to a vCenter instance
// for module testing
// name the file config-test.js
export const vCenterTestCreds = {
vCenterIP: 'vcsa',
vCenterUser: 'vcuser',
vCenterPassword: 'vcpw',
vCenter: true,
}

View File

@@ -1,138 +0,0 @@
/*
vsphere-soap.test.js
tests for the vCenterConnectionInstance class
*/
import assert from 'assert'
import { describe, it } from 'test'
import * as vc from '../lib/client.mjs'
// eslint-disable-next-line n/no-missing-import
import { vCenterTestCreds as TestCreds } from '../config-test.mjs'
const VItest = new vc.Client(TestCreds.vCenterIP, TestCreds.vCenterUser, TestCreds.vCenterPassword, false)
describe('Client object initialization:', function () {
it('provides a successful login', { timeout: 5000 }, function (t, done) {
VItest.once('ready', function () {
assert.notEqual(VItest.userName, null)
assert.notEqual(VItest.fullName, null)
assert.notEqual(VItest.serviceContent, null)
done()
}).once('error', function (err) {
console.error(err)
// this should fail if there's a problem
assert.notEqual(VItest.userName, null)
assert.notEqual(VItest.fullName, null)
assert.notEqual(VItest.serviceContent, null)
done()
})
})
})
describe('Client reconnection test:', function () {
it('can successfully reconnect', { timeout: 5000 }, function (t, done) {
VItest.runCommand('Logout', { _this: VItest.serviceContent.sessionManager })
.once('result', function (result) {
// now we're logged out, so let's try running a command to test automatic re-login
VItest.runCommand('CurrentTime', { _this: 'ServiceInstance' })
.once('result', function (result) {
assert(result.returnval instanceof Date)
done()
})
.once('error', function (err) {
console.error(err)
})
})
.once('error', function (err) {
console.error(err)
})
})
})
// these tests don't work yet
describe('Client tests - query commands:', function () {
it('retrieves current time', { timeout: 5000 }, function (t, done) {
VItest.runCommand('CurrentTime', { _this: 'ServiceInstance' }).once('result', function (result) {
assert(result.returnval instanceof Date)
done()
})
})
it('retrieves current time 2 (check for event clobbering)', { timeout: 5000 }, function (t, done) {
VItest.runCommand('CurrentTime', { _this: 'ServiceInstance' }).once('result', function (result) {
assert(result.returnval instanceof Date)
done()
})
})
it('can obtain the names of all Virtual Machines in the inventory', { timeout: 20000 }, function (t, done) {
// get property collector
const propertyCollector = VItest.serviceContent.propertyCollector
// get view manager
const viewManager = VItest.serviceContent.viewManager
// get root folder
const rootFolder = VItest.serviceContent.rootFolder
let containerView, objectSpec, traversalSpec, propertySpec, propertyFilterSpec
// this is the equivalent to
VItest.runCommand('CreateContainerView', {
_this: viewManager,
container: rootFolder,
type: ['VirtualMachine'],
recursive: true,
}).once('result', function (result) {
// build all the data structures needed to query all the vm names
containerView = result.returnval
objectSpec = {
attributes: { 'xsi:type': 'ObjectSpec' }, // setting attributes xsi:type is important or else the server may mis-recognize types!
obj: containerView,
skip: true,
}
traversalSpec = {
attributes: { 'xsi:type': 'TraversalSpec' },
name: 'traverseEntities',
type: 'ContainerView',
path: 'view',
skip: false,
}
objectSpec = { ...objectSpec, selectSet: [traversalSpec] }
propertySpec = {
attributes: { 'xsi:type': 'PropertySpec' },
type: 'VirtualMachine',
pathSet: ['name'],
}
propertyFilterSpec = {
attributes: { 'xsi:type': 'PropertyFilterSpec' },
propSet: [propertySpec],
objectSet: [objectSpec],
}
// TODO: research why it fails if propSet is declared after objectSet
VItest.runCommand('RetrievePropertiesEx', {
_this: propertyCollector,
specSet: [propertyFilterSpec],
options: { attributes: { type: 'RetrieveOptions' } },
})
.once('result', function (result, raw) {
assert.notEqual(result.returnval.objects, null)
if (Array.isArray(result.returnval.objects)) {
assert.strictEqual(result.returnval.objects[0].obj.attributes.type, 'VirtualMachine')
} else {
assert.strictEqual(result.returnval.objects.obj.attributes.type, 'VirtualMachine')
}
done()
})
.once('error', function (err) {
console.error('\n\nlast request : ' + VItest.client.lastRequest, err)
})
})
})
})

View File

@@ -1,130 +0,0 @@
### Usual workflow
> This section presents how this library should be used to implement a classic two factor authentification.
#### Setup
```js
import { generateSecret, generateTotp } from '@vates/otp'
import QrCode from 'qrcode'
// Generates a secret that will be shared by both the service and the user:
const secret = generateSecret()
// Stores the secret in the service:
await currentUser.saveOtpSecret(secret)
// Generates an URI to present to the user
const uri = generateTotpUri({ secret })
// Generates the QR code from the URI to make it easily importable in Authy or Google Authenticator
const qr = await QrCode.toDataURL(uri)
```
#### Authentication
```js
import { verifyTotp } from '@vates/otp'
// Verifies a `token` entered by the user against a `secret` generated during setup.
if (await verifyTotp(token, { secret })) {
console.log('authenticated!')
}
```
### API
#### Secret
```js
import { generateSecret } from '@vates/otp'
const secret = generateSecret()
// 'OJOKA65RY5FQQ2RYWVKD5Y3YG5CSHGYH'
```
#### HOTP
> This is likely not what you want to use, see TOTP below instead.
```js
import { generateHotp, generateHotpUri, verifyHotp } from '@vates/otp'
// a sequence number, see HOTP specification
const counter = 0
// generate a token
//
// optional params:
// - digits
const token = await generateHotp({ counter, secret })
// '239988'
// verify a token
//
// optional params:
// - digits
const isValid = await verifyHotp(token, { counter, secret })
// true
// generate a URI than can be displayed as a QR code to be used with Authy or Google Authenticator
//
// optional params:
// - digits
const uri = generateHotpUri({ counter, label: 'account name', issuer: 'my app', secret })
// 'otpauth://hotp/my%20app:account%20name?counter=0&issuer=my%20app&secret=OJOKA65RY5FQQ2RYWVKD5Y3YG5CSHGYH'
```
Optional params and their default values:
- `digits = 6`: length of the token, avoid using it because not compatible with Google Authenticator
#### TOTP
```js
import { generateTotp, generateTotpUri, verifyTotp } from '@vates/otp'
// generate a token
//
// optional params:
// - digits
// - period
// - timestamp
const token = await generateTotp({ secret })
// '632869'
// verify a token
//
// optional params:
// - digits
// - period
// - timestamp
// - window
const isValid = await verifyTotp(token, { secret })
// true
// generate a URI than can be displayed as a QR code to be used with Authy or Google Authenticator
//
// optional params:
// - digits
// - period
const uri = generateTotpUri({ label: 'account name', issuer: 'my app', secret })
// 'otpauth://totp/my%20app:account%20name?issuer=my%20app&secret=OJOKA65RY5FQQ2RYWVKD5Y3YG5CSHGYH'
```
Optional params and their default values:
- `digits = 6`: length of the token, avoid using it because not compatible with Google Authenticator
- `period = 30`: number of seconds a token is valid
- `timestamp = Date.now() / 1e3`: Unix timestamp, in seconds, when this token will be valid, default to now
- `window = 1`: number of periods before and after `timestamp` for which the token is considered valid
#### Verification from URI
```js
import { verifyFromUri } from '@vates/otp'
// Verify the token using all the information contained in the URI
const isValid = await verifyFromUri(token, uri)
// true
```

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,163 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/otp
[![Package Version](https://badgen.net/npm/v/@vates/otp)](https://npmjs.org/package/@vates/otp) ![License](https://badgen.net/npm/license/@vates/otp) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/otp)](https://bundlephobia.com/result?p=@vates/otp) [![Node compatibility](https://badgen.net/npm/node/@vates/otp)](https://npmjs.org/package/@vates/otp)
> Minimal HTOP/TOTP implementation
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/otp):
```sh
npm install --save @vates/otp
```
## Usage
### Usual workflow
> This section presents how this library should be used to implement a classic two factor authentification.
#### Setup
```js
import { generateSecret, generateTotp } from '@vates/otp'
import QrCode from 'qrcode'
// Generates a secret that will be shared by both the service and the user:
const secret = generateSecret()
// Stores the secret in the service:
await currentUser.saveOtpSecret(secret)
// Generates an URI to present to the user
const uri = generateTotpUri({ secret })
// Generates the QR code from the URI to make it easily importable in Authy or Google Authenticator
const qr = await QrCode.toDataURL(uri)
```
#### Authentication
```js
import { verifyTotp } from '@vates/otp'
// Verifies a `token` entered by the user against a `secret` generated during setup.
if (await verifyTotp(token, { secret })) {
console.log('authenticated!')
}
```
### API
#### Secret
```js
import { generateSecret } from '@vates/otp'
const secret = generateSecret()
// 'OJOKA65RY5FQQ2RYWVKD5Y3YG5CSHGYH'
```
#### HOTP
> This is likely not what you want to use, see TOTP below instead.
```js
import { generateHotp, generateHotpUri, verifyHotp } from '@vates/otp'
// a sequence number, see HOTP specification
const counter = 0
// generate a token
//
// optional params:
// - digits
const token = await generateHotp({ counter, secret })
// '239988'
// verify a token
//
// optional params:
// - digits
const isValid = await verifyHotp(token, { counter, secret })
// true
// generate a URI than can be displayed as a QR code to be used with Authy or Google Authenticator
//
// optional params:
// - digits
const uri = generateHotpUri({ counter, label: 'account name', issuer: 'my app', secret })
// 'otpauth://hotp/my%20app:account%20name?counter=0&issuer=my%20app&secret=OJOKA65RY5FQQ2RYWVKD5Y3YG5CSHGYH'
```
Optional params and their default values:
- `digits = 6`: length of the token, avoid using it because not compatible with Google Authenticator
#### TOTP
```js
import { generateTotp, generateTotpUri, verifyTotp } from '@vates/otp'
// generate a token
//
// optional params:
// - digits
// - period
// - timestamp
const token = await generateTotp({ secret })
// '632869'
// verify a token
//
// optional params:
// - digits
// - period
// - timestamp
// - window
const isValid = await verifyTotp(token, { secret })
// true
// generate a URI than can be displayed as a QR code to be used with Authy or Google Authenticator
//
// optional params:
// - digits
// - period
const uri = generateTotpUri({ label: 'account name', issuer: 'my app', secret })
// 'otpauth://totp/my%20app:account%20name?issuer=my%20app&secret=OJOKA65RY5FQQ2RYWVKD5Y3YG5CSHGYH'
```
Optional params and their default values:
- `digits = 6`: length of the token, avoid using it because not compatible with Google Authenticator
- `period = 30`: number of seconds a token is valid
- `timestamp = Date.now() / 1e3`: Unix timestamp, in seconds, when this token will be valid, default to now
- `window = 1`: number of periods before and after `timestamp` for which the token is considered valid
#### Verification from URI
```js
import { verifyFromUri } from '@vates/otp'
// Verify the token using all the information contained in the URI
const isValid = await verifyFromUri(token, uri)
// true
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,111 +0,0 @@
import { base32 } from 'rfc4648'
import { webcrypto } from 'node:crypto'
const { subtle } = webcrypto
function assert(name, value) {
if (!value) {
throw new TypeError('invalid value for param ' + name)
}
}
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
function generateUri(protocol, label, params) {
assert('label', typeof label === 'string')
assert('secret', typeof params.secret === 'string')
let path = encodeURIComponent(label)
const { issuer } = params
if (issuer !== undefined) {
path = encodeURIComponent(issuer) + ':' + path
}
const query = Object.entries(params)
.filter(_ => _[1] !== undefined)
.map(([key, value]) => key + '=' + encodeURIComponent(value))
.join('&')
return `otpauth://${protocol}/${path}?${query}`
}
export function generateSecret() {
// https://www.rfc-editor.org/rfc/rfc4226 recommends 160 bits (i.e. 20 bytes)
const data = new Uint8Array(20)
webcrypto.getRandomValues(data)
return base32.stringify(data, { pad: false })
}
const DIGITS = 6
// https://www.rfc-editor.org/rfc/rfc4226
export async function generateHotp({ counter, digits = DIGITS, secret }) {
const data = new Uint8Array(8)
new DataView(data.buffer).setBigInt64(0, BigInt(counter), false)
const key = await subtle.importKey(
'raw',
base32.parse(secret, { loose: true }),
{ name: 'HMAC', hash: 'SHA-1' },
false,
['sign', 'verify']
)
const digest = new DataView(await subtle.sign('HMAC', key, data))
const offset = digest.getUint8(digest.byteLength - 1) & 0xf
const p = digest.getUint32(offset) & 0x7f_ff_ff_ff
return String(p % Math.pow(10, digits)).padStart(digits, '0')
}
export function generateHotpUri({ counter, digits, issuer, label, secret }) {
assert('counter', typeof counter === 'number')
return generateUri('hotp', label, { counter, digits, issuer, secret })
}
export async function verifyHotp(token, opts) {
return token === (await generateHotp(opts))
}
function totpCounter(period = 30, timestamp = Math.floor(Date.now() / 1e3)) {
return Math.floor(timestamp / period)
}
// https://www.rfc-editor.org/rfc/rfc6238.html
export async function generateTotp({ period, timestamp, ...opts }) {
opts.counter = totpCounter(period, timestamp)
return await generateHotp(opts)
}
export function generateTotpUri({ digits, issuer, label, period, secret }) {
return generateUri('totp', label, { digits, issuer, period, secret })
}
export async function verifyTotp(token, { period, timestamp, window = 1, ...opts }) {
const counter = totpCounter(period, timestamp)
const end = counter + window
opts.counter = counter - window
while (opts.counter <= end) {
if (token === (await generateHotp(opts))) {
return true
}
opts.counter += 1
}
return false
}
export async function verifyFromUri(token, uri) {
const url = new URL(uri)
assert('protocol', url.protocol === 'otpauth:')
const { host } = url
const opts = Object.fromEntries(url.searchParams.entries())
if (host === 'hotp') {
return await verifyHotp(token, opts)
}
if (host === 'totp') {
return await verifyTotp(token, opts)
}
assert('host', false)
}

View File

@@ -1,112 +0,0 @@
import { strict as assert } from 'node:assert'
import { describe, it } from 'tap/mocha'
import {
generateHotp,
generateHotpUri,
generateSecret,
generateTotp,
generateTotpUri,
verifyHotp,
verifyTotp,
} from './index.mjs'
describe('generateSecret', function () {
it('generates a string of 32 chars', async function () {
const secret = generateSecret()
assert.equal(typeof secret, 'string')
assert.equal(secret.length, 32)
})
it('generates a different secret at each call', async function () {
assert.notEqual(generateSecret(), generateSecret())
})
})
describe('HOTP', function () {
it('generate and verify valid tokens', async function () {
for (const [token, opts] of Object.entries({
382752: {
counter: -3088,
secret: 'PJYFSZ3JNVXVQMZXOB2EQYJSKB2HE6TB',
},
163376: {
counter: 30598,
secret: 'GBUDQZ3UKZZGIMRLNVYXA33GMFMEGQKN',
},
})) {
assert.equal(await generateHotp(opts), token)
assert(await verifyHotp(token, opts))
}
})
describe('generateHotpUri', function () {
const opts = {
counter: 59732,
label: 'the label',
secret: 'OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
}
Object.entries({
'without optional params': [
opts,
'otpauth://hotp/the%20label?counter=59732&secret=OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
],
'with issuer': [
{ ...opts, issuer: 'the issuer' },
'otpauth://hotp/the%20issuer:the%20label?counter=59732&issuer=the%20issuer&secret=OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
],
'with digits': [
{ ...opts, digits: 7 },
'otpauth://hotp/the%20label?counter=59732&digits=7&secret=OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
],
}).forEach(([title, [opts, uri]]) => {
it(title, async function () {
assert.strictEqual(generateHotpUri(opts), uri)
})
})
})
})
describe('TOTP', function () {
Object.entries({
'033702': {
secret: 'PJYFSZ3JNVXVQMZXOB2EQYJSKB2HE6TB',
timestamp: 1665416296,
period: 30,
},
107250: {
secret: 'GBUDQZ3UKZZGIMRLNVYXA33GMFMEGQKN',
timestamp: 1665416674,
period: 60,
},
}).forEach(([token, opts]) => {
it('works', async function () {
assert.equal(await generateTotp(opts), token)
assert(await verifyTotp(token, opts))
})
})
describe('generateHotpUri', function () {
const opts = {
label: 'the label',
secret: 'OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
}
Object.entries({
'without optional params': [opts, 'otpauth://totp/the%20label?secret=OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX'],
'with issuer': [
{ ...opts, issuer: 'the issuer' },
'otpauth://totp/the%20issuer:the%20label?issuer=the%20issuer&secret=OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
],
'with digits': [
{ ...opts, digits: 7 },
'otpauth://totp/the%20label?digits=7&secret=OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
],
}).forEach(([title, [opts, uri]]) => {
it(title, async function () {
assert.strictEqual(generateTotpUri(opts), uri)
})
})
})
})

View File

@@ -1,39 +0,0 @@
{
"private": false,
"name": "@vates/otp",
"description": "Minimal HTOP/TOTP implementation",
"keywords": [
"2fa",
"authenticator",
"hotp",
"otp",
"totp"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/otp",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"main": "index.mjs",
"repository": {
"directory": "@vates/otp",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "1.0.0",
"engines": {
"node": ">=15"
},
"dependencies": {
"rfc4648": "^1.5.2"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "tap"
},
"devDependencies": {
"tap": "^16.3.0"
}
}

View File

@@ -10,8 +10,8 @@
Installation of the [npm package](https://npmjs.org/package/@vates/parse-duration):
```sh
npm install --save @vates/parse-duration
```
> npm install --save @vates/parse-duration
```
## Usage

View File

@@ -1,7 +1,7 @@
`undefined` predicates are ignored and `undefined` is returned if all predicates are `undefined`, this permits the most efficient composition:
```js
const compositePredicate = not(every(undefined, some(not(predicate2), undefined)))
const compositePredicate = every(undefined, some(predicate2, undefined))
// ends up as
@@ -36,21 +36,6 @@ isBetween3And10(10)
// → false
```
### `not(predicate)`
> Returns a predicate that returns the negation of the predicate.
```js
const isEven = n => n % 2 === 0
const isOdd = not(isEven)
isOdd(1)
// true
isOdd(2)
// false
```
### `some(predicates)`
> Returns a predicate that returns `true` iff some predicate returns `true`.

View File

@@ -10,8 +10,8 @@
Installation of the [npm package](https://npmjs.org/package/@vates/predicates):
```sh
npm install --save @vates/predicates
```
> npm install --save @vates/predicates
```
## Usage
@@ -19,7 +19,7 @@ npm install --save @vates/predicates
`undefined` predicates are ignored and `undefined` is returned if all predicates are `undefined`, this permits the most efficient composition:
```js
const compositePredicate = not(every(undefined, some(not(predicate2), undefined)))
const compositePredicate = every(undefined, some(predicate2, undefined))
// ends up as
@@ -54,21 +54,6 @@ isBetween3And10(10)
// → false
```
### `not(predicate)`
> Returns a predicate that returns the negation of the predicate.
```js
const isEven = n => n % 2 === 0
const isOdd = not(isEven)
isOdd(1)
// true
isOdd(2)
// false
```
### `some(predicates)`
> Returns a predicate that returns `true` iff some predicate returns `true`.

View File

@@ -51,22 +51,6 @@ exports.every = function every() {
}
}
const notPredicateTag = {}
exports.not = function not(predicate) {
if (isDefinedPredicate(predicate)) {
if (predicate.tag === notPredicateTag) {
return predicate.predicate
}
function notPredicate() {
return !predicate.apply(this, arguments)
}
notPredicate.predicate = predicate
notPredicate.tag = notPredicateTag
return notPredicate
}
}
exports.some = function some() {
const predicates = handleArgs.apply(this, arguments)
const n = predicates.length

View File

@@ -3,14 +3,20 @@
const assert = require('assert/strict')
const { describe, it } = require('tap').mocha
const { every, not, some } = require('./')
const { every, some } = require('./')
const T = () => true
const F = () => false
const testArgHandling = fn => {
it('returns undefined if predicate is undefined', () => {
const testArgsHandling = fn => {
it('returns undefined if all predicates are undefined', () => {
assert.equal(fn(undefined), undefined)
assert.equal(fn([undefined]), undefined)
})
it('returns the predicate if only a single one is passed', () => {
assert.equal(fn(undefined, T), T)
assert.equal(fn([undefined, T]), T)
})
it('throws if it receives a non-predicate', () => {
@@ -18,15 +24,6 @@ const testArgHandling = fn => {
error.value = 3
assert.throws(() => fn(3), error)
})
}
const testArgsHandling = fn => {
testArgHandling(fn)
it('returns the predicate if only a single one is passed', () => {
assert.equal(fn(undefined, T), T)
assert.equal(fn([undefined, T]), T)
})
it('forwards this and arguments to predicates', () => {
const thisArg = 'qux'
@@ -39,21 +36,17 @@ const testArgsHandling = fn => {
})
}
const runTests = (fn, acceptMultiple, truthTable) =>
const runTests = (fn, truthTable) =>
it('works', () => {
truthTable.forEach(([result, ...predicates]) => {
if (acceptMultiple) {
assert.equal(fn(predicates)(), result)
} else {
assert.equal(predicates.length, 1)
}
assert.equal(fn(...predicates)(), result)
assert.equal(fn(predicates)(), result)
})
})
describe('every', () => {
testArgsHandling(every)
runTests(every, true, [
runTests(every, [
[true, T, T],
[false, T, F],
[false, F, T],
@@ -61,22 +54,9 @@ describe('every', () => {
])
})
describe('not', () => {
testArgHandling(not)
it('returns the original predicate if negated twice', () => {
assert.equal(not(not(T)), T)
})
runTests(not, false, [
[true, F],
[false, T],
])
})
describe('some', () => {
testArgsHandling(some)
runTests(some, true, [
runTests(some, [
[true, T, T],
[true, T, F],
[true, F, T],

View File

@@ -26,7 +26,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "1.1.0",
"version": "1.0.0",
"engines": {
"node": ">=6"
},

View File

@@ -1,9 +1,6 @@
### `readChunk(stream, [size])`
- returns the next available chunk of data
- like `stream.read()`, a number of bytes can be specified
- returns with less data than expected if stream has ended
- returns `null` if the stream has ended and no data has been read
- returns `null` if the stream has ended
```js
import { readChunk } from '@vates/read-chunk'
@@ -14,35 +11,3 @@ import { readChunk } from '@vates/read-chunk'
}
})()
```
### `readChunkStrict(stream, [size])`
Similar behavior to `readChunk` but throws if the stream ended before the requested data could be read.
```js
import { readChunkStrict } from '@vates/read-chunk'
const chunk = await readChunkStrict(stream, 1024)
```
### `skip(stream, size)`
Skips a given number of bytes from a stream.
Returns the number of bytes actually skipped, which may be less than the requested size if the stream has ended.
```js
import { skip } from '@vates/read-chunk'
const bytesSkipped = await skip(stream, 2 * 1024 * 1024 * 1024)
```
### `skipStrict(stream, size)`
Skips a given number of bytes from a stream and throws if the stream ended before enough stream has been skipped.
```js
import { skipStrict } from '@vates/read-chunk'
await skipStrict(stream, 2 * 1024 * 1024 * 1024)
```

View File

@@ -10,18 +10,15 @@
Installation of the [npm package](https://npmjs.org/package/@vates/read-chunk):
```sh
npm install --save @vates/read-chunk
```
> npm install --save @vates/read-chunk
```
## Usage
### `readChunk(stream, [size])`
- returns the next available chunk of data
- like `stream.read()`, a number of bytes can be specified
- returns with less data than expected if stream has ended
- returns `null` if the stream has ended and no data has been read
- returns `null` if the stream has ended
```js
import { readChunk } from '@vates/read-chunk'
@@ -33,38 +30,6 @@ import { readChunk } from '@vates/read-chunk'
})()
```
### `readChunkStrict(stream, [size])`
Similar behavior to `readChunk` but throws if the stream ended before the requested data could be read.
```js
import { readChunkStrict } from '@vates/read-chunk'
const chunk = await readChunkStrict(stream, 1024)
```
### `skip(stream, size)`
Skips a given number of bytes from a stream.
Returns the number of bytes actually skipped, which may be less than the requested size if the stream has ended.
```js
import { skip } from '@vates/read-chunk'
const bytesSkipped = await skip(stream, 2 * 1024 * 1024 * 1024)
```
### `skipStrict(stream, size)`
Skips a given number of bytes from a stream and throws if the stream ended before enough stream has been skipped.
```js
import { skipStrict } from '@vates/read-chunk'
await skipStrict(stream, 2 * 1024 * 1024 * 1024)
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on

View File

@@ -1,37 +1,9 @@
'use strict'
const assert = require('assert')
const isUtf8 = require('isutf8')
/**
* Read a chunk of data from a stream.
*
* The returned promise is rejected if there is an error while reading the stream.
*
* For streams in object mode, the returned promise resolves to a single object read from the stream.
*
* For streams in binary mode, the returned promise resolves to a Buffer or a string if an encoding has been specified using the `stream.setEncoding()` method.
*
* If `size` bytes are not available to be read, `null` will be returned *unless* the stream has ended, in which case all of the data remaining will be returned.
*
* @param {Readable} stream - A readable stream to read from.
* @param {number} [size] - The number of bytes to read for binary streams (ignored for object streams).
* @returns {Promise<Buffer|string|unknown|null>} - A Promise that resolves to the read chunk if available, or null if end of stream is reached.
*/
const readChunk = (stream, size) =>
stream.errored != null
? Promise.reject(stream.errored)
: stream.closed || stream.readableEnded
? Promise.resolve(null)
size === 0
? Promise.resolve(Buffer.alloc(0))
: new Promise((resolve, reject) => {
if (size !== undefined) {
assert(size > 0)
// per Node documentation:
// > The size argument must be less than or equal to 1 GiB.
assert(size < 1073741824)
}
function onEnd() {
resolve(null)
removeListeners()
@@ -58,110 +30,3 @@ const readChunk = (stream, size) =>
onReadable()
})
exports.readChunk = readChunk
/**
* Read a chunk of data from a stream.
*
* The returned promise is rejected if there is an error while reading the stream.
*
* For streams in object mode, the returned promise resolves to a single object read from the stream.
*
* For streams in binary mode, the returned promise resolves to a Buffer or a string if an encoding has been specified using the `stream.setEncoding()` method.
*
* If `size` bytes are not available to be read, the returned promise is rejected.
*
* @param {Readable} stream - A readable stream to read from.
* @param {number} [size] - The number of bytes to read for binary streams (ignored for object streams).
* @returns {Promise<Buffer|string|unknown>} - A Promise that resolves to the read chunk.
*/
exports.readChunkStrict = async function readChunkStrict(stream, size) {
const chunk = await readChunk(stream, size)
if (chunk === null) {
throw new Error('stream has ended without data')
}
if (size !== undefined && chunk.length !== size) {
const error = new Error(`stream has ended with not enough data (actual: ${chunk.length}, expected: ${size})`)
// Buffer.isUtf8 is too recent for now
// @todo : replace external package by Buffer.isUtf8 when the supported version of node reach 18
if (chunk.length < 1024 && isUtf8(chunk)) {
error.text = chunk.toString('utf8')
}
Object.defineProperties(error, {
chunk: {
value: chunk,
},
})
throw error
}
return chunk
}
/**
* Skips a given number of bytes from a readable stream.
*
* @param {Readable} stream - A readable stream to skip bytes from.
* @param {number} size - The number of bytes to skip.
* @returns {Promise<number>} A Promise that resolves to the number of bytes actually skipped. If the end of the stream is reached before all bytes are skipped, the Promise resolves to the number of bytes that were skipped before the end of the stream was reached. The Promise is rejected if there is an error while reading from the stream.
*/
async function skip(stream, size) {
return stream.errored != null
? Promise.reject(stream.errored)
: size === 0 || stream.closed || stream.readableEnded
? Promise.resolve(0)
: new Promise((resolve, reject) => {
let left = size
function onEnd() {
resolve(size - left)
removeListeners()
}
function onError(error) {
reject(error)
removeListeners()
}
function onReadable() {
const data = stream.read()
left -= data === null ? 0 : data.length
if (left > 0) {
// continue to read
} else {
// if more than wanted has been read, push back the rest
if (left < 0) {
stream.unshift(data.slice(left))
}
resolve(size)
removeListeners()
}
}
function removeListeners() {
stream.removeListener('end', onEnd)
stream.removeListener('error', onError)
stream.removeListener('readable', onReadable)
}
stream.on('end', onEnd)
stream.on('error', onError)
stream.on('readable', onReadable)
onReadable()
})
}
exports.skip = skip
/**
* Skips a given number of bytes from a stream.
*
* @param {Readable} stream - A readable stream to skip bytes from.
* @param {number} size - The number of bytes to skip.
* @returns {Promise<void>} - A Promise that resolves when the exact number of bytes have been skipped. The Promise is rejected if there is an error while reading from the stream or the stream ends before the exact number of bytes have been skipped.
*/
exports.skipStrict = async function skipStrict(stream, size) {
const bytesSkipped = await skip(stream, size)
if (bytesSkipped !== size) {
const error = new Error(`stream has ended with not enough data (actual: ${bytesSkipped}, expected: ${size})`)
error.bytesSkipped = bytesSkipped
throw error
}
}

View File

@@ -0,0 +1,45 @@
'use strict'
/* eslint-env jest */
const { Readable } = require('stream')
const { readChunk } = require('./')
const makeStream = it => Readable.from(it, { objectMode: false })
makeStream.obj = Readable.from
describe('readChunk', () => {
it('returns null if stream is empty', async () => {
expect(await readChunk(makeStream([]))).toBe(null)
})
describe('with binary stream', () => {
it('returns the first chunk of data', async () => {
expect(await readChunk(makeStream(['foo', 'bar']))).toEqual(Buffer.from('foo'))
})
it('returns a chunk of the specified size (smaller than first)', async () => {
expect(await readChunk(makeStream(['foo', 'bar']), 2)).toEqual(Buffer.from('fo'))
})
it('returns a chunk of the specified size (larger than first)', async () => {
expect(await readChunk(makeStream(['foo', 'bar']), 4)).toEqual(Buffer.from('foob'))
})
it('returns less data if stream ends', async () => {
expect(await readChunk(makeStream(['foo', 'bar']), 10)).toEqual(Buffer.from('foobar'))
})
it('returns an empty buffer if the specified size is 0', async () => {
expect(await readChunk(makeStream(['foo', 'bar']), 0)).toEqual(Buffer.alloc(0))
})
})
describe('with object stream', () => {
it('returns the first chunk of data verbatim', async () => {
const chunks = [{}, {}]
expect(await readChunk(makeStream.obj(chunks))).toBe(chunks[0])
})
})
})

View File

@@ -1,187 +0,0 @@
'use strict'
const { describe, it } = require('test')
const assert = require('node:assert').strict
const { Readable } = require('stream')
const { readChunk, readChunkStrict, skip, skipStrict } = require('./')
const makeStream = it => Readable.from(it, { objectMode: false })
makeStream.obj = Readable.from
const rejectionOf = promise =>
promise.then(
value => {
throw value
},
error => error
)
const makeErrorTests = fn => {
it('rejects if the stream errors', async () => {
const error = new Error()
const stream = makeStream([])
const pError = rejectionOf(fn(stream, 10))
stream.destroy(error)
assert.strict(await pError, error)
})
// only supported for Node >= 18
if (process.versions.node.split('.')[0] >= 18) {
it('rejects if the stream has already errored', async () => {
const error = new Error()
const stream = makeStream([])
await new Promise(resolve => {
stream.once('error', resolve).destroy(error)
})
assert.strict(await rejectionOf(fn(stream, 10)), error)
})
}
}
describe('readChunk', () => {
it('rejects if size is less than or equal to 0', async () => {
const error = await rejectionOf(readChunk(makeStream([]), 0))
assert.strictEqual(error.code, 'ERR_ASSERTION')
})
it('rejects if size is greater than or equal to 1 GiB', async () => {
const error = await rejectionOf(readChunk(makeStream([]), 1024 * 1024 * 1024))
assert.strictEqual(error.code, 'ERR_ASSERTION')
})
makeErrorTests(readChunk)
it('returns null if stream is empty', async () => {
assert.strictEqual(await readChunk(makeStream([])), null)
})
it('returns null if the stream is already ended', async () => {
const stream = await makeStream([])
await readChunk(stream)
assert.strictEqual(await readChunk(stream), null)
})
describe('with binary stream', () => {
it('returns the first chunk of data', async () => {
assert.deepEqual(await readChunk(makeStream(['foo', 'bar'])), Buffer.from('foo'))
})
it('returns a chunk of the specified size (smaller than first)', async () => {
assert.deepEqual(await readChunk(makeStream(['foo', 'bar']), 2), Buffer.from('fo'))
})
it('returns a chunk of the specified size (larger than first)', async () => {
assert.deepEqual(await readChunk(makeStream(['foo', 'bar']), 4), Buffer.from('foob'))
})
it('returns less data if stream ends', async () => {
assert.deepEqual(await readChunk(makeStream(['foo', 'bar']), 10), Buffer.from('foobar'))
})
})
describe('with object stream', () => {
it('returns the first chunk of data verbatim', async () => {
const chunks = [{}, {}]
assert.strictEqual(await readChunk(makeStream.obj(chunks)), chunks[0])
})
})
})
describe('readChunkStrict', function () {
it('throws if stream is empty', async () => {
const error = await rejectionOf(readChunkStrict(makeStream([])))
assert(error instanceof Error)
assert.strictEqual(error.message, 'stream has ended without data')
assert.strictEqual(error.chunk, undefined)
})
it('throws if stream ends with not enough data, utf8', async () => {
const error = await rejectionOf(readChunkStrict(makeStream(['foo', 'bar']), 10))
assert(error instanceof Error)
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 6, expected: 10)')
assert.strictEqual(error.text, 'foobar')
assert.deepEqual(error.chunk, Buffer.from('foobar'))
})
it('throws if stream ends with not enough data, non utf8 ', async () => {
const source = [Buffer.alloc(10, 128), Buffer.alloc(10, 128)]
const error = await rejectionOf(readChunkStrict(makeStream(source), 30))
assert(error instanceof Error)
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 20, expected: 30)')
assert.strictEqual(error.text, undefined)
assert.deepEqual(error.chunk, Buffer.concat(source))
})
it('throws if stream ends with not enough data, utf8 , long data', async () => {
const source = Buffer.from('a'.repeat(1500))
const error = await rejectionOf(readChunkStrict(makeStream([source]), 2000))
assert(error instanceof Error)
assert.strictEqual(error.message, `stream has ended with not enough data (actual: 1500, expected: 2000)`)
assert.strictEqual(error.text, undefined)
assert.deepEqual(error.chunk, source)
})
it('succeed', async () => {
const source = Buffer.from('a'.repeat(20))
const chunk = await readChunkStrict(makeStream([source]), 10)
assert.deepEqual(source.subarray(10), chunk)
})
})
describe('skip', function () {
makeErrorTests(skip)
it('returns 0 if size is 0', async () => {
assert.strictEqual(await skip(makeStream(['foo']), 0), 0)
})
it('returns 0 if the stream is already ended', async () => {
const stream = await makeStream([])
await readChunk(stream)
assert.strictEqual(await skip(stream, 10), 0)
})
it('skips a number of bytes', async () => {
const stream = makeStream('foo bar')
assert.strictEqual(await skip(stream, 4), 4)
assert.deepEqual(await readChunk(stream, 4), Buffer.from('bar'))
})
it('returns less size if stream ends', async () => {
assert.deepEqual(await skip(makeStream('foo bar'), 10), 7)
})
it('put back if it read too much', async () => {
let source = makeStream(['foo', 'bar'])
await skip(source, 1) // read part of data chunk
const chunk = (await readChunkStrict(source, 2)).toString('utf-8')
assert.strictEqual(chunk, 'oo')
source = makeStream(['foo', 'bar'])
assert.strictEqual(await skip(source, 3), 3) // read aligned with data chunk
})
})
describe('skipStrict', function () {
it('throws if stream ends with not enough data', async () => {
const error = await rejectionOf(skipStrict(makeStream('foo bar'), 10))
assert(error instanceof Error)
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 7, expected: 10)')
assert.deepEqual(error.bytesSkipped, 7)
})
it('succeed', async () => {
const source = makeStream(['foo', 'bar', 'baz'])
const res = await skipStrict(source, 4)
assert.strictEqual(res, undefined)
})
})

View File

@@ -19,22 +19,15 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "1.2.0",
"version": "0.1.2",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"postversion": "npm publish --access public"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"devDependencies": {
"test": "^3.2.1"
},
"dependencies": {
"isutf8": "^4.0.0"
}
}

View File

@@ -1,42 +0,0 @@
```js
import StreamReader from '@vates/stream-reader'
const reader = new StreamReader(stream)
```
### `.read([size])`
- returns the next available chunk of data
- like `stream.read()`, a number of bytes can be specified
- returns with less data than expected if stream has ended
- returns `null` if the stream has ended and no data has been read
```js
const chunk = await reader.read(512)
```
### `.readStrict([size])`
Similar behavior to `readChunk` but throws if the stream ended before the requested data could be read.
```js
const chunk = await reader.readStrict(512)
```
### `.skip(size)`
Skips a given number of bytes from a stream.
Returns the number of bytes actually skipped, which may be less than the requested size if the stream has ended.
```js
const bytesSkipped = await reader.skip(2 * 1024 * 1024 * 1024)
```
### `.skipStrict(size)`
Skips a given number of bytes from a stream and throws if the stream ended before enough stream has been skipped.
```js
await reader.skipStrict(2 * 1024 * 1024 * 1024)
```

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,75 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/stream-reader
[![Package Version](https://badgen.net/npm/v/@vates/stream-reader)](https://npmjs.org/package/@vates/stream-reader) ![License](https://badgen.net/npm/license/@vates/stream-reader) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/stream-reader)](https://bundlephobia.com/result?p=@vates/stream-reader) [![Node compatibility](https://badgen.net/npm/node/@vates/stream-reader)](https://npmjs.org/package/@vates/stream-reader)
> Efficiently reads and skips chunks of a given size in a stream
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/stream-reader):
```sh
npm install --save @vates/stream-reader
```
## Usage
```js
import StreamReader from '@vates/stream-reader'
const reader = new StreamReader(stream)
```
### `.read([size])`
- returns the next available chunk of data
- like `stream.read()`, a number of bytes can be specified
- returns with less data than expected if stream has ended
- returns `null` if the stream has ended and no data has been read
```js
const chunk = await reader.read(512)
```
### `.readStrict([size])`
Similar behavior to `readChunk` but throws if the stream ended before the requested data could be read.
```js
const chunk = await reader.readStrict(512)
```
### `.skip(size)`
Skips a given number of bytes from a stream.
Returns the number of bytes actually skipped, which may be less than the requested size if the stream has ended.
```js
const bytesSkipped = await reader.skip(2 * 1024 * 1024 * 1024)
```
### `.skipStrict(size)`
Skips a given number of bytes from a stream and throws if the stream ended before enough stream has been skipped.
```js
await reader.skipStrict(2 * 1024 * 1024 * 1024)
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,123 +0,0 @@
'use strict'
const assert = require('node:assert')
const { finished, Readable } = require('node:stream')
const noop = Function.prototype
// Inspired by https://github.com/nodejs/node/blob/85705a47958c9ae5dbaa1f57456db19bdefdc494/lib/internal/streams/readable.js#L1107
class StreamReader {
#ended = false
#error
#executor = resolve => {
this.#resolve = resolve
}
#stream
#resolve = noop
constructor(stream) {
stream = typeof stream.pipe === 'function' ? stream : Readable.from(stream)
this.#stream = stream
stream.on('readable', () => this.#resolve())
finished(stream, { writable: false }, error => {
this.#error = error
this.#ended = true
this.#resolve()
})
}
async read(size) {
if (size !== undefined) {
assert(size > 0)
}
do {
if (this.#ended) {
if (this.#error) {
throw this.#error
}
return null
}
const value = this.#stream.read(size)
if (value !== null) {
return value
}
await new Promise(this.#executor)
} while (true)
}
async readStrict(size) {
const chunk = await this.read(size)
if (chunk === null) {
throw new Error('stream has ended without data')
}
if (size !== undefined && chunk.length !== size) {
const error = new Error(`stream has ended with not enough data (actual: ${chunk.length}, expected: ${size})`)
Object.defineProperties(error, {
chunk: {
value: chunk,
},
})
throw error
}
return chunk
}
async skip(size) {
if (size === 0) {
return size
}
let toSkip = size
do {
if (this.#ended) {
if (this.#error) {
throw this.#error
}
return size - toSkip
}
const data = this.#stream.read()
if (data !== null) {
toSkip -= data === null ? 0 : data.length
if (toSkip > 0) {
// continue to read
} else {
// if more than wanted has been read, push back the rest
if (toSkip < 0) {
this.#stream.unshift(data.slice(toSkip))
}
return size
}
}
await new Promise(this.#executor)
} while (true)
}
async skipStrict(size) {
const bytesSkipped = await this.skip(size)
if (bytesSkipped !== size) {
const error = new Error(`stream has ended with not enough data (actual: ${bytesSkipped}, expected: ${size})`)
error.bytesSkipped = bytesSkipped
throw error
}
}
}
StreamReader.prototype[Symbol.asyncIterator] = async function* asyncIterator() {
let chunk
while ((chunk = await this.read()) !== null) {
yield chunk
}
}
module.exports = StreamReader

View File

@@ -1,141 +0,0 @@
'use strict'
const { describe, it } = require('test')
const assert = require('node:assert').strict
const { Readable } = require('stream')
const StreamReader = require('./index.js')
const makeStream = it => Readable.from(it, { objectMode: false })
makeStream.obj = Readable.from
const rejectionOf = promise =>
promise.then(
value => {
throw value
},
error => error
)
const makeErrorTests = method => {
it('rejects if the stream errors', async () => {
const error = new Error()
const stream = makeStream([])
const pError = rejectionOf(new StreamReader(stream)[method](10))
stream.destroy(error)
assert.strict(await pError, error)
})
it('rejects if the stream has already errored', async () => {
const error = new Error()
const stream = makeStream([])
await new Promise(resolve => {
stream.once('error', resolve).destroy(error)
})
assert.strict(await rejectionOf(new StreamReader(stream)[method](10)), error)
})
}
describe('read()', () => {
it('rejects if size is less than or equal to 0', async () => {
const error = await rejectionOf(new StreamReader(makeStream([])).read(0))
assert.strictEqual(error.code, 'ERR_ASSERTION')
})
it('returns null if stream is empty', async () => {
assert.strictEqual(await new StreamReader(makeStream([])).read(), null)
})
makeErrorTests('read')
it('returns null if the stream is already ended', async () => {
const reader = new StreamReader(makeStream([]))
await reader.read()
assert.strictEqual(await reader.read(), null)
})
describe('with binary stream', () => {
it('returns the first chunk of data', async () => {
assert.deepEqual(await new StreamReader(makeStream(['foo', 'bar'])).read(), Buffer.from('foo'))
})
it('returns a chunk of the specified size (smaller than first)', async () => {
assert.deepEqual(await new StreamReader(makeStream(['foo', 'bar'])).read(2), Buffer.from('fo'))
})
it('returns a chunk of the specified size (larger than first)', async () => {
assert.deepEqual(await new StreamReader(makeStream(['foo', 'bar'])).read(4), Buffer.from('foob'))
})
it('returns less data if stream ends', async () => {
assert.deepEqual(await new StreamReader(makeStream(['foo', 'bar'])).read(10), Buffer.from('foobar'))
})
})
describe('with object stream', () => {
it('returns the first chunk of data verbatim', async () => {
const chunks = [{}, {}]
assert.strictEqual(await new StreamReader(makeStream.obj(chunks)).read(), chunks[0])
})
})
})
describe('readStrict()', function () {
it('throws if stream is empty', async () => {
const error = await rejectionOf(new StreamReader(makeStream([])).readStrict())
assert(error instanceof Error)
assert.strictEqual(error.message, 'stream has ended without data')
assert.strictEqual(error.chunk, undefined)
})
it('throws if stream ends with not enough data', async () => {
const error = await rejectionOf(new StreamReader(makeStream(['foo', 'bar'])).readStrict(10))
assert(error instanceof Error)
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 6, expected: 10)')
assert.deepEqual(error.chunk, Buffer.from('foobar'))
})
})
describe('skip()', function () {
makeErrorTests('skip')
it('returns 0 if size is 0', async () => {
assert.strictEqual(await new StreamReader(makeStream(['foo'])).skip(0), 0)
})
it('returns 0 if the stream is already ended', async () => {
const reader = new StreamReader(makeStream([]))
await reader.read()
assert.strictEqual(await reader.skip(10), 0)
})
it('skips a number of bytes', async () => {
const reader = new StreamReader(makeStream('foo bar'))
assert.strictEqual(await reader.skip(4), 4)
assert.deepEqual(await reader.read(4), Buffer.from('bar'))
})
it('returns less size if stream ends', async () => {
assert.deepEqual(await new StreamReader(makeStream('foo bar')).skip(10), 7)
})
})
describe('skipStrict()', function () {
it('throws if stream ends with not enough data', async () => {
const error = await rejectionOf(new StreamReader(makeStream('foo bar')).skipStrict(10))
assert(error instanceof Error)
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 7, expected: 10)')
assert.deepEqual(error.bytesSkipped, 7)
})
})

View File

@@ -1,39 +0,0 @@
{
"private": false,
"name": "@vates/stream-reader",
"description": "Efficiently reads and skips chunks of a given size in a stream",
"keywords": [
"async",
"chunk",
"data",
"node",
"promise",
"read",
"reader",
"skip",
"stream"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/stream-reader",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/stream-reader",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.0",
"engines": {
"node": ">=10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
},
"devDependencies": {
"test": "^3.3.0"
}
}

View File

@@ -1,137 +0,0 @@
```js
import { Task } from '@vates/task'
const task = new Task({
// this object will be sent in the *start* event
properties: {
name: 'my task',
},
// if defined, a new detached task is created
//
// if not defined and created inside an existing task, the new task is considered a subtask
onProgress(event) {
// this function is called each time this task or one of it's subtasks change state
const { id, timestamp, type } = event
if (type === 'start') {
const { name, parentId, properties } = event
} else if (type === 'end') {
const { result, status } = event
} else if (type === 'info' || type === 'warning') {
const { data, message } = event
} else if (type === 'property') {
const { name, value } = event
} else if (type === 'abortionRequested') {
const { reason } = event
}
},
})
// this field is settable once before being observed
task.id
// contains the current status of the task
//
// possible statuses are:
// - pending
// - success
// - failure
task.status
// Triggers the abort signal associated to the task.
//
// This simply requests the task to abort, it will be up to the task to handle or not this signal.
task.abort(reason)
// if fn rejects, the task will be marked as failed
const result = await task.runInside(fn)
// if fn rejects, the task will be marked as failed
// if fn resolves, the task will be marked as succeeded
const result = await task.run(fn)
```
Inside a task:
```js
// the abort signal of the current task if any, otherwise is `undefined`
Task.abortSignal
// sends an info on the current task if any, otherwise does nothing
Task.info(message, data)
// sends an info on the current task if any, otherwise does nothing
Task.warning(message, data)
// attaches a property to the current task if any, otherwise does nothing
//
// the latest value takes precedence
//
// examples:
// - progress
Task.set(property, value)
```
### `combineEvents`
Create a consolidated log from individual events.
It can be used directly as an `onProgress` callback:
```js
import { makeOnProgress } from '@vates/task/combineEvents'
const onProgress = makeOnProgress({
// This function is called each time a root task starts.
//
// It will be called for as many times as there are tasks created with this `onProgress` function.
onRootTaskStart(taskLog) {
// `taskLog` is an object reflecting the state of this task and all its subtasks,
// and will be mutated in real-time to reflect the changes of the task.
// timestamp at which the task started
taskLog.start
// current status of the task as described in the previous section
taskLog.status
// undefined or a dictionary of properties attached to the task
taskLog.properties
// timestamp at which the abortion was requested, undefined otherwise
taskLog.abortionRequestedAt
// undefined or an array of infos emitted on the task
taskLog.infos
// undefined or an array of warnings emitted on the task
taskLog.warnings
// timestamp at which the task ended, undefined otherwise
taskLog.end
// undefined or the result value of the task
taskLog.result
},
// This function is called each time a root task ends.
onRootTaskEnd(taskLog) {},
// This function is called each time a root task or a subtask is updated.
//
// `taskLog.$root` can be used to uncondionally access the root task.
onTaskUpdate(taskLog) {},
})
Task.run({ data: { name: 'my task' }, onProgress }, asyncFn)
```
It can also be fed event logs directly:
```js
import { makeOnProgress } from '@vates/task/combineEvents'
const onProgress = makeOnProgress({ onRootTaskStart, onRootTaskEnd, onTaskUpdate })
eventLogs.forEach(onProgress)
```

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,168 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/task
[![Package Version](https://badgen.net/npm/v/@vates/task)](https://npmjs.org/package/@vates/task) ![License](https://badgen.net/npm/license/@vates/task) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/task)](https://bundlephobia.com/result?p=@vates/task) [![Node compatibility](https://badgen.net/npm/node/@vates/task)](https://npmjs.org/package/@vates/task)
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/task):
```sh
npm install --save @vates/task
```
## Usage
```js
import { Task } from '@vates/task'
const task = new Task({
// this object will be sent in the *start* event
properties: {
name: 'my task',
},
// if defined, a new detached task is created
//
// if not defined and created inside an existing task, the new task is considered a subtask
onProgress(event) {
// this function is called each time this task or one of it's subtasks change state
const { id, timestamp, type } = event
if (type === 'start') {
const { name, parentId, properties } = event
} else if (type === 'end') {
const { result, status } = event
} else if (type === 'info' || type === 'warning') {
const { data, message } = event
} else if (type === 'property') {
const { name, value } = event
} else if (type === 'abortionRequested') {
const { reason } = event
}
},
})
// this field is settable once before being observed
task.id
// contains the current status of the task
//
// possible statuses are:
// - pending
// - success
// - failure
task.status
// Triggers the abort signal associated to the task.
//
// This simply requests the task to abort, it will be up to the task to handle or not this signal.
task.abort(reason)
// if fn rejects, the task will be marked as failed
const result = await task.runInside(fn)
// if fn rejects, the task will be marked as failed
// if fn resolves, the task will be marked as succeeded
const result = await task.run(fn)
```
Inside a task:
```js
// the abort signal of the current task if any, otherwise is `undefined`
Task.abortSignal
// sends an info on the current task if any, otherwise does nothing
Task.info(message, data)
// sends an info on the current task if any, otherwise does nothing
Task.warning(message, data)
// attaches a property to the current task if any, otherwise does nothing
//
// the latest value takes precedence
//
// examples:
// - progress
Task.set(property, value)
```
### `combineEvents`
Create a consolidated log from individual events.
It can be used directly as an `onProgress` callback:
```js
import { makeOnProgress } from '@vates/task/combineEvents'
const onProgress = makeOnProgress({
// This function is called each time a root task starts.
//
// It will be called for as many times as there are tasks created with this `onProgress` function.
onRootTaskStart(taskLog) {
// `taskLog` is an object reflecting the state of this task and all its subtasks,
// and will be mutated in real-time to reflect the changes of the task.
// timestamp at which the task started
taskLog.start
// current status of the task as described in the previous section
taskLog.status
// undefined or a dictionnary of properties attached to the task
taskLog.properties
// timestamp at which the abortion was requested, undefined otherwise
taskLog.abortionRequestedAt
// undefined or an array of infos emitted on the task
taskLog.infos
// undefined or an array of warnings emitted on the task
taskLog.warnings
// timestamp at which the task ended, undefined otherwise
taskLog.end
// undefined or the result value of the task
taskLog.result
},
// This function is called each time a root task ends.
onRootTaskEnd(taskLog) {},
// This function is called each time a root task or a subtask is updated.
//
// `taskLog.$root` can be used to uncondionally access the root task.
onTaskUpdate(taskLog) {},
})
Task.run({ data: { name: 'my task' }, onProgress }, asyncFn)
```
It can also be fed event logs directly:
```js
import { makeOnProgress } from '@vates/task/combineEvents'
const onProgress = makeOnProgress({ onRootTaskStart, onRootTaskEnd, onTaskUpdate })
eventLogs.forEach(onProgress)
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,61 +0,0 @@
'use strict'
const assert = require('node:assert').strict
const noop = Function.prototype
exports.makeOnProgress = function ({ onRootTaskEnd = noop, onRootTaskStart = noop, onTaskUpdate = noop }) {
const taskLogs = new Map()
return function onProgress(event) {
const { id, type } = event
let taskLog
if (type === 'start') {
taskLog = {
id,
properties: { __proto__: null, ...event.properties },
start: event.timestamp,
status: 'pending',
}
taskLogs.set(id, taskLog)
const { parentId } = event
if (parentId === undefined) {
Object.defineProperty(taskLog, '$root', { value: taskLog })
// start of a root task
onRootTaskStart(taskLog)
} else {
// start of a subtask
const parent = taskLogs.get(parentId)
assert.notEqual(parent, undefined)
// inject a (non-enumerable) reference to the parent and the root task
Object.defineProperties(taskLog, { $parent: { value: parent }, $root: { value: parent.$root } })
;(parent.tasks ?? (parent.tasks = [])).push(taskLog)
}
} else {
taskLog = taskLogs.get(id)
assert.notEqual(taskLog, undefined)
if (type === 'info' || type === 'warning') {
const key = type + 's'
const { data, message } = event
;(taskLog[key] ?? (taskLog[key] = [])).push({ data, message })
} else if (type === 'property') {
;(taskLog.properties ?? (taskLog.properties = { __proto__: null }))[event.name] = event.value
} else if (type === 'end') {
taskLog.end = event.timestamp
taskLog.result = event.result
taskLog.status = event.status
} else if (type === 'abortionRequested') {
taskLog.abortionRequestedAt = event.timestamp
}
if (type === 'end' && taskLog.$root === taskLog) {
onRootTaskEnd(taskLog)
}
}
onTaskUpdate(taskLog)
}
}

View File

@@ -1,81 +0,0 @@
'use strict'
const assert = require('node:assert').strict
const { describe, it } = require('test')
const { makeOnProgress } = require('./combineEvents.js')
const { Task } = require('./index.js')
describe('makeOnProgress()', function () {
it('works', async function () {
const events = []
let log
const task = new Task({
properties: { name: 'task' },
onProgress: makeOnProgress({
onRootTaskStart(log_) {
assert.equal(log, undefined)
log = log_
events.push('onRootTaskStart')
},
onRootTaskEnd(log_) {
assert.equal(log_, log)
events.push('onRootTaskEnd')
},
onTaskUpdate(log_) {
assert.equal(log_.$root, log)
events.push('onTaskUpdate')
},
}),
})
assert.equal(events.length, 0)
let i = 0
await task.run(async () => {
assert.equal(events[i++], 'onRootTaskStart')
assert.equal(events[i++], 'onTaskUpdate')
assert.equal(log.id, task.id)
assert.equal(log.properties.name, 'task')
assert(Math.abs(log.start - Date.now()) < 10)
Task.set('name', 'new name')
assert.equal(events[i++], 'onTaskUpdate')
assert.equal(log.properties.name, 'new name')
Task.set('progress', 0)
assert.equal(events[i++], 'onTaskUpdate')
assert.equal(log.properties.progress, 0)
Task.info('foo', {})
assert.equal(events[i++], 'onTaskUpdate')
assert.deepEqual(log.infos, [{ data: {}, message: 'foo' }])
const subtask = new Task({ properties: { name: 'subtask' } })
await subtask.run(() => {
assert.equal(events[i++], 'onTaskUpdate')
assert.equal(log.tasks[0].properties.name, 'subtask')
Task.warning('bar', {})
assert.equal(events[i++], 'onTaskUpdate')
assert.deepEqual(log.tasks[0].warnings, [{ data: {}, message: 'bar' }])
subtask.abort()
assert.equal(events[i++], 'onTaskUpdate')
assert(Math.abs(log.tasks[0].abortionRequestedAt - Date.now()) < 10)
})
assert.equal(events[i++], 'onTaskUpdate')
assert.equal(log.tasks[0].status, 'success')
Task.set('progress', 100)
assert.equal(events[i++], 'onTaskUpdate')
assert.equal(log.properties.progress, 100)
})
assert.equal(events[i++], 'onRootTaskEnd')
assert.equal(events[i++], 'onTaskUpdate')
assert(Math.abs(log.end - Date.now()) < 10)
assert.equal(log.status, 'success')
})
})

View File

@@ -1,183 +0,0 @@
'use strict'
const assert = require('node:assert').strict
const { AsyncLocalStorage } = require('node:async_hooks')
// define a read-only, non-enumerable, non-configurable property
function define(object, property, value) {
Object.defineProperty(object, property, { value })
}
const noop = Function.prototype
const FAILURE = 'failure'
const PENDING = 'pending'
const SUCCESS = 'success'
exports.STATUS = { FAILURE, PENDING, SUCCESS }
// stored in the global context so that various versions of the library can interact.
const asyncStorageKey = '@vates/task@0'
const asyncStorage = global[asyncStorageKey] ?? (global[asyncStorageKey] = new AsyncLocalStorage())
const getTask = () => asyncStorage.getStore()
exports.Task = class Task {
static get abortSignal() {
const task = getTask()
if (task !== undefined) {
return task.#abortController.signal
}
}
static info(message, data) {
const task = getTask()
if (task !== undefined) {
task.#emit('info', { data, message })
}
}
static run(opts, fn) {
return new this(opts).run(fn)
}
static set(name, value) {
const task = getTask()
if (task !== undefined) {
task.#emit('property', { name, value })
}
}
static warning(message, data) {
const task = getTask()
if (task !== undefined) {
task.#emit('warning', { data, message })
}
}
static wrap(opts, fn) {
// compatibility with @decorateWith
if (typeof fn !== 'function') {
;[fn, opts] = [opts, fn]
}
return function taskRun() {
return Task.run(typeof opts === 'function' ? opts.apply(this, arguments) : opts, () => fn.apply(this, arguments))
}
}
#abortController = new AbortController()
#onProgress
get id() {
return (this.id = Math.random().toString(36).slice(2))
}
set id(value) {
define(this, 'id', value)
}
#startData
#status = PENDING
get status() {
return this.#status
}
constructor({ properties, onProgress } = {}) {
this.#startData = { properties }
if (onProgress !== undefined) {
this.#onProgress = onProgress
} else {
const parent = getTask()
if (parent !== undefined) {
const { signal } = parent.#abortController
signal.addEventListener('abort', () => {
this.#abortController.abort(signal.reason)
})
this.#onProgress = parent.#onProgress
this.#startData.parentId = parent.id
} else {
this.#onProgress = noop
}
}
const { signal } = this.#abortController
signal.addEventListener('abort', () => {
if (this.status === PENDING) {
this.#maybeStart()
this.#emit('abortionRequested', { reason: signal.reason })
if (!this.#running) {
const status = FAILURE
this.#status = status
this.#emit('end', { result: signal.reason, status })
}
}
})
}
abort(reason) {
this.#abortController.abort(reason)
}
#emit(type, data) {
data.id = this.id
data.timestamp = Date.now()
data.type = type
this.#onProgress(data)
}
#maybeStart() {
const startData = this.#startData
if (startData !== undefined) {
this.#startData = undefined
this.#emit('start', startData)
}
}
async run(fn) {
const result = await this.runInside(fn)
if (this.status === PENDING) {
this.#status = SUCCESS
this.#emit('end', { status: SUCCESS, result })
}
return result
}
#running = false
async runInside(fn) {
assert.equal(this.status, PENDING)
assert.equal(this.#running, false)
this.#running = true
this.#maybeStart()
try {
const result = await asyncStorage.run(this, fn)
this.#running = false
return result
} catch (result) {
const status = FAILURE
this.#status = status
this.#emit('end', { status, result })
throw result
}
}
wrap(fn) {
const task = this
return function taskRun() {
return task.run(() => fn.apply(this, arguments))
}
}
wrapInside(fn) {
const task = this
return function taskRunInside() {
return task.runInside(() => fn.apply(this, arguments))
}
}
}

View File

@@ -1,347 +0,0 @@
'use strict'
const assert = require('node:assert').strict
const { describe, it } = require('test')
const { Task } = require('./index.js')
const noop = Function.prototype
function assertEvent(task, expected, eventIndex = -1) {
const logs = task.$events
const actual = logs[eventIndex < 0 ? logs.length + eventIndex : eventIndex]
assert.equal(typeof actual, 'object')
assert.equal(typeof actual.id, 'string')
assert.equal(typeof actual.timestamp, 'number')
for (const keys of Object.keys(expected)) {
assert.deepEqual(actual[keys], expected[keys])
}
}
// like new Task() but with a custom onProgress which adds event to task.$events
function createTask(opts) {
const events = []
const task = new Task({ ...opts, onProgress: events.push.bind(events) })
task.$events = events
return task
}
describe('Task', function () {
describe('contructor', function () {
it('data properties are passed to the start event', async function () {
const properties = { foo: 0, bar: 1 }
const task = createTask({ properties })
await task.run(noop)
assertEvent(task, { type: 'start', properties }, 0)
})
})
it('subtasks events are passed to root task', async function () {
const task = createTask()
const result = {}
await task.run(async () => {
await new Task().run(() => result)
})
assert.equal(task.$events.length, 4)
assertEvent(task, { type: 'start', parentId: task.id }, 1)
assertEvent(task, { type: 'end', status: 'success', result }, 2)
})
describe('.abortSignal', function () {
it('is undefined when run outside a task', function () {
assert.equal(Task.abortSignal, undefined)
})
it('is the current abort signal when run inside a task', async function () {
const task = createTask()
await task.run(() => {
const { abortSignal } = Task
assert.equal(abortSignal.aborted, false)
task.abort()
assert.equal(abortSignal.aborted, true)
})
})
})
describe('.abort()', function () {
it('aborts if the task throws fails with the abort reason', async function () {
const task = createTask()
const reason = {}
await task
.run(() => {
task.abort(reason)
Task.abortSignal.throwIfAborted()
})
.catch(noop)
assert.equal(task.status, 'failure')
assert.equal(task.$events.length, 3)
assertEvent(task, { type: 'start' }, 0)
assertEvent(task, { type: 'abortionRequested', reason }, 1)
assertEvent(task, { type: 'end', status: 'failure', result: reason }, 2)
})
it('does not abort if the task fails without the abort reason', async function () {
const task = createTask()
const reason = {}
const result = new Error()
await task
.run(() => {
task.abort(reason)
throw result
})
.catch(noop)
assert.equal(task.status, 'failure')
assert.equal(task.$events.length, 3)
assertEvent(task, { type: 'start' }, 0)
assertEvent(task, { type: 'abortionRequested', reason }, 1)
assertEvent(task, { type: 'end', status: 'failure', result }, 2)
})
it('does not abort if the task succeed', async function () {
const task = createTask()
const reason = {}
const result = {}
await task
.run(() => {
task.abort(reason)
return result
})
.catch(noop)
assert.equal(task.status, 'success')
assert.equal(task.$events.length, 3)
assertEvent(task, { type: 'start' }, 0)
assertEvent(task, { type: 'abortionRequested', reason }, 1)
assertEvent(task, { type: 'end', status: 'success', result }, 2)
})
it('aborts before task is running', function () {
const task = createTask()
const reason = {}
task.abort(reason)
assert.equal(task.status, 'failure')
assert.equal(task.$events.length, 3)
assertEvent(task, { type: 'start' }, 0)
assertEvent(task, { type: 'abortionRequested', reason }, 1)
assertEvent(task, { type: 'end', status: 'failure', result: reason }, 2)
})
})
describe('.info()', function () {
it('does nothing when run outside a task', function () {
Task.info('foo')
})
it('emits an info message when run inside a task', async function () {
const task = createTask()
await task.run(() => {
Task.info('foo')
assertEvent(task, {
data: undefined,
message: 'foo',
type: 'info',
})
})
})
})
describe('.set()', function () {
it('does nothing when run outside a task', function () {
Task.set('progress', 10)
})
it('emits an info message when run inside a task', async function () {
const task = createTask()
await task.run(() => {
Task.set('progress', 10)
assertEvent(task, {
name: 'progress',
type: 'property',
value: 10,
})
})
})
})
describe('.warning()', function () {
it('does nothing when run outside a task', function () {
Task.warning('foo')
})
it('emits an warning message when run inside a task', async function () {
const task = createTask()
await task.run(() => {
Task.warning('foo')
assertEvent(task, {
data: undefined,
message: 'foo',
type: 'warning',
})
})
})
})
describe('#id', function () {
it('can be set', function () {
const task = createTask()
task.id = 'foo'
assert.equal(task.id, 'foo')
})
it('cannot be set more than once', function () {
const task = createTask()
task.id = 'foo'
assert.throws(() => {
task.id = 'bar'
}, TypeError)
})
it('is randomly generated if not set', function () {
assert.notEqual(createTask().id, createTask().id)
})
it('cannot be set after being observed', function () {
const task = createTask()
noop(task.id)
assert.throws(() => {
task.id = 'bar'
}, TypeError)
})
})
describe('#status', function () {
it('starts as pending', function () {
assert.equal(createTask().status, 'pending')
})
it('changes to success when finish without error', async function () {
const task = createTask()
await task.run(noop)
assert.equal(task.status, 'success')
})
it('changes to failure when finish with error', async function () {
const task = createTask()
await task
.run(() => {
throw Error()
})
.catch(noop)
assert.equal(task.status, 'failure')
})
it('changes to failure if aborted after run is complete', async function () {
const task = createTask()
await task
.run(() => {
task.abort()
assert.equal(task.status, 'pending')
Task.abortSignal.throwIfAborted()
})
.catch(noop)
assert.equal(task.status, 'failure')
})
it('changes to failure if aborted when not running', function () {
const task = createTask()
task.abort()
assert.equal(task.status, 'failure')
})
})
function makeRunTests(run) {
it('starts the task', async function () {
const task = createTask()
await run(task, () => {
assertEvent(task, { type: 'start' })
})
})
it('finishes the task on success', async function () {
const task = createTask()
await run(task, () => 'foo')
assert.equal(task.status, 'success')
assertEvent(task, {
status: 'success',
result: 'foo',
type: 'end',
})
})
it('fails the task on error', async function () {
const task = createTask()
const e = new Error()
await run(task, () => {
throw e
}).catch(noop)
assert.equal(task.status, 'failure')
assertEvent(task, {
status: 'failure',
result: e,
type: 'end',
})
})
}
describe('.run', function () {
makeRunTests((task, fn) => task.run(fn))
})
describe('.wrap', function () {
makeRunTests((task, fn) => task.wrap(fn)())
})
function makeRunInsideTests(run) {
it('starts the task', async function () {
const task = createTask()
await run(task, () => {
assertEvent(task, { type: 'start' })
})
})
it('does not finish the task on success', async function () {
const task = createTask()
await run(task, () => 'foo')
assert.equal(task.status, 'pending')
})
it('fails the task on error', async function () {
const task = createTask()
const e = new Error()
await run(task, () => {
throw e
}).catch(noop)
assert.equal(task.status, 'failure')
assertEvent(task, {
status: 'failure',
result: e,
type: 'end',
})
})
}
describe('.runInside', function () {
makeRunInsideTests((task, fn) => task.runInside(fn))
})
describe('.wrapInside', function () {
makeRunInsideTests((task, fn) => task.wrapInside(fn)())
})
})

View File

@@ -1,31 +0,0 @@
{
"private": false,
"name": "@vates/task",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/task",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/task",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.2.0",
"engines": {
"node": ">=14"
},
"devDependencies": {
"test": "^3.3.0"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
},
"exports": {
".": "./index.js",
"./combineEvents": "./combineEvents.js"
}
}

View File

@@ -10,8 +10,8 @@
Installation of the [npm package](https://npmjs.org/package/@vates/toggle-scripts):
```sh
npm install --save @vates/toggle-scripts
```
> npm install --save @vates/toggle-scripts
```
## Usage

View File

@@ -30,7 +30,6 @@ if (args.length === 0) {
${name} v${version}
`)
// eslint-disable-next-line n/no-process-exit
process.exit()
}

View File

@@ -10,8 +10,8 @@
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/async-map):
```sh
npm install --save @xen-orchestra/async-map
```
> npm install --save @xen-orchestra/async-map
```
## Usage

View File

@@ -1,8 +1,6 @@
'use strict'
const { describe, it } = require('test')
const assert = require('assert').strict
const sinon = require('sinon')
/* eslint-env jest */
const { asyncMapSettled } = require('./')
@@ -11,29 +9,26 @@ const noop = Function.prototype
describe('asyncMapSettled', () => {
it('works', async () => {
const values = [Math.random(), Math.random()]
const spy = sinon.spy(async v => v * 2)
const spy = jest.fn(async v => v * 2)
const iterable = new Set(values)
// returns an array containing the result of each calls
assert.deepStrictEqual(
await asyncMapSettled(iterable, spy),
values.map(value => value * 2)
)
expect(await asyncMapSettled(iterable, spy)).toEqual(values.map(value => value * 2))
for (let i = 0, n = values.length; i < n; ++i) {
// each call receive the current item as sole argument
assert.deepStrictEqual(spy.args[i], [values[i]])
expect(spy.mock.calls[i]).toEqual([values[i]])
// each call as this bind to the iterable
assert.deepStrictEqual(spy.thisValues[i], iterable)
expect(spy.mock.instances[i]).toBe(iterable)
}
})
it('can use a specified thisArg', () => {
const thisArg = {}
const spy = sinon.spy()
const spy = jest.fn()
asyncMapSettled(['foo'], spy, thisArg)
assert.deepStrictEqual(spy.thisValues[0], thisArg)
expect(spy.mock.instances[0]).toBe(thisArg)
})
it('rejects only when all calls as resolved', async () => {
@@ -60,22 +55,19 @@ describe('asyncMapSettled', () => {
// wait for all microtasks to settle
await new Promise(resolve => setImmediate(resolve))
assert.strictEqual(hasSettled, false)
expect(hasSettled).toBe(false)
defers[1].resolve()
// wait for all microtasks to settle
await new Promise(resolve => setImmediate(resolve))
assert.strictEqual(hasSettled, true)
await assert.rejects(promise, error)
expect(hasSettled).toBe(true)
await expect(promise).rejects.toBe(error)
})
it('issues when latest promise rejects', async () => {
const error = new Error()
await assert.rejects(
asyncMapSettled([1], () => Promise.reject(error)),
error
)
await expect(asyncMapSettled([1], () => Promise.reject(error))).rejects.toBe(error)
})
})

Some files were not shown because too many files have changed in this diff Show More