<commit_before>import { System } from '../System';
import { Rectangle } from '@pixi/math';
import { ENV } from '@pixi/constants';
import { settings } from '../settings';
import { Framebuffer } from './Framebuffer';

/**
 * System plugin to the renderer to manage framebuffers.
 *
 * @class
 * @extends PIXI.System
 * @memberof PIXI.systems
 */
export class FramebufferSystem extends System
{
    /**
     * @param {PIXI.Renderer} renderer - The renderer this System works for.
     */
    constructor(renderer)
    {
        super(renderer);

        /**
         * A list of managed framebuffers
         * @member {PIXI.Framebuffer[]}
         * @readonly
         */
        this.managedFramebuffers = [];

        /**
         * Framebuffer value that shows that we don't know what is bound
         * @member {Framebuffer}
         * @readonly
         */
        this.unknownFramebuffer = new Framebuffer(10, 10);
    }

    /**
     * Sets up the renderer context and necessary buffers.
     */
    contextChange()
    {
        const gl = this.gl = this.renderer.gl;

        this.CONTEXT_UID = this.renderer.CONTEXT_UID;
        this.current = this.unknownFramebuffer;
        this.viewport = new Rectangle();
        this.hasMRT = true;
        this.writeDepthTexture = true;

        this.disposeAll(true);

        // webgl2
        if (this.renderer.context.webGLVersion === 1)
        {
            // webgl 1!
            let nativeDrawBuffersExtension = this.renderer.context.extensions.drawBuffers;
            let nativeDepthTextureExtension = this.renderer.context.extensions.depthTexture;

            if (settings.PREFER_ENV === ENV.WEBGL_LEGACY)
            {
                nativeDrawBuffersExtension = null;
                nativeDepthTextureExtension = null;
            }

            if (nativeDrawBuffersExtension)
            {
                gl.drawBuffers = (activeTextures) =>
                    nativeDrawBuffersExtension.drawBuffersWEBGL(activeTextures);
            }
            else
            {
                this.hasMRT = false;
                gl.drawBuffers = () =>
                {
                    // empty
                };
            }

            if (!nativeDepthTextureExtension)
            {
                this.writeDepthTexture = false;
            }
        }
    }

    /**
     * Bind a framebuffer
     *
     * @param {PIXI.Framebuffer} framebuffer
     * @param {PIXI.Rectangle} [frame] frame, default is framebuffer size
     */
    bind(framebuffer, frame)
    {
        const { gl } = this;

        if (framebuffer)
        {
            // TODO caching layer!

            const fbo = framebuffer.glFramebuffers[this.CONTEXT_UID] || this.initFramebuffer(framebuffer);

            if (this.current !== framebuffer)
            {
                this.current = framebuffer;
                gl.bindFramebuffer(gl.FRAMEBUFFER, fbo.framebuffer);
            }
            // make sure all textures are unbound..

            // now check for updates...
            if (fbo.dirtyId !== framebuffer.dirtyId)
            {
                fbo.dirtyId = framebuffer.dirtyId;

                if (fbo.dirtyFormat !== framebuffer.dirtyFormat)
                {
                    fbo.dirtyFormat = framebuffer.dirtyFormat;
                    this.updateFramebuffer(framebuffer);
                }
                else if (fbo.dirtySize !== framebuffer.dirtySize)
                {
                    fbo.dirtySize = framebuffer.dirtySize;
                    this.resizeFramebuffer(framebuffer);
                }
            }

            for (let i = 0; i < framebuffer.colorTextures.length; i++)
            {
                if (framebuffer.colorTextures[i].texturePart)
                {
                    this.renderer.texture.unbind(framebuffer.colorTextures[i].texture);
                }
                else
                {
                    this.renderer.texture.unbind(framebuffer.colorTextures[i]);
                }
            }

            if (framebuffer.depthTexture)
            {
                this.renderer.texture.unbind(framebuffer.depthTexture);
            }

            if (frame)
            {
                this.setViewport(frame.x, frame.y, frame.width, frame.height);
            }
            else
            {
                this.setViewport(0, 0, framebuffer.width, framebuffer.height);
            }
        }
        else
        {
            if (this.current)
            {
                this.current = null;
                gl.bindFramebuffer(gl.FRAMEBUFFER, null);
            }

            if (frame)
            {
                this.setViewport(frame.x, frame.y, frame.width, frame.height);
            }
            else
            {
                this.setViewport(0, 0, this.renderer.width, this.renderer.height);
            }
        }
    }

    /**
     * Set the WebGLRenderingContext's viewport.
     *
     * @param {Number} x - X position of viewport
     * @param {Number} y - Y position of viewport
     * @param {Number} width - Width of viewport
     * @param {Number} height - Height of viewport
     */
    setViewport(x, y, width, height)
    {
        const v = this.viewport;

        if (v.width !== width || v.height !== height || v.x !== x || v.y !== y)
        {
            v.x = x;
            v.y = y;
            v.width = width;
            v.height = height;

            this.gl.viewport(x, y, width, height);
        }
    }

    /**
     * Get the size of the current width and height. Returns object with `width` and `height` values.
     *
     * @member {object}
     * @readonly
     */
    get size()
    {
        if (this.current)
        {
            // TODO store temp
            return { x: 0, y: 0, width: this.current.width, height: this.current.height };
        }

        return { x: 0, y: 0, width: this.renderer.width, height: this.renderer.height };
    }

    /**
     * Clear the color of the context
     *
     * @param {Number} r - Red value from 0 to 1
     * @param {Number} g - Green value from 0 to 1
     * @param {Number} b - Blue value from 0 to 1
     * @param {Number} a - Alpha value from 0 to 1
     */
    clear(r, g, b, a)
    {
        const { gl } = this;

        // TODO clear color can be set only one right?
        gl.clearColor(r, g, b, a);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    }

    /**
     * Initialize framebuffer
     *
     * @protected
     * @param {PIXI.Framebuffer} framebuffer
     */
    initFramebuffer(framebuffer)
    {
        const { gl } = this;

        // TODO - make this a class?
        const fbo = {
            framebuffer: gl.createFramebuffer(),
            stencil: null,
            dirtyId: 0,
            dirtyFormat: 0,
            dirtySize: 0,
        };

        framebuffer.glFramebuffers[this.CONTEXT_UID] = fbo;

        this.managedFramebuffers.push(framebuffer);
        framebuffer.disposeRunner.add(this);

        return fbo;
    }

    /**
     * Resize the framebuffer
     *
     * @protected
     * @param {PIXI.Framebuffer} framebuffer
     */
    resizeFramebuffer(framebuffer)
    {
        const { gl } = this;

        const fbo = framebuffer.glFramebuffers[this.CONTEXT_UID];

        if (fbo.stencil)
        {
            gl.bindRenderbuffer(gl.RENDERBUFFER, fbo.stencil);
            gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, framebuffer.width, framebuffer.height);
        }

        const colorTextures = framebuffer.colorTextures;

        for (let i = 0; i < colorTextures.length; i++)
        {
            this.renderer.texture.bind(colorTextures[i], 0);
        }

        if (framebuffer.depthTexture)
        {
            this.renderer.texture.bind(framebuffer.depthTexture, 0);
        }
    }

    /**
     * Update the framebuffer
     *
     * @protected
     * @param {PIXI.Framebuffer} framebuffer
     */
    updateFramebuffer(framebuffer)
    {
        const { gl } = this;

        const fbo = framebuffer.glFramebuffers[this.CONTEXT_UID];

        // bind the color texture
        const colorTextures = framebuffer.colorTextures;

        let count = colorTextures.length;

        if (!gl.drawBuffers)
        {
            count = Math.min(count, 1);
        }

        const activeTextures = [];

        for (let i = 0; i < count; i++)
        {
            const texture = framebuffer.colorTextures[i];

            if (texture.texturePart)
            {
                this.renderer.texture.bind(texture.texture, 0);

                gl.framebufferTexture2D(gl.FRAMEBUFFER,
                    gl.COLOR_ATTACHMENT0 + i,
                    gl.TEXTURE_CUBE_MAP_NEGATIVE_X + texture.side,
                    texture.texture._glTextures[this.CONTEXT_UID].texture,
                    0);
            }
            else
            {
                this.renderer.texture.bind(texture, 0);

                gl.framebufferTexture2D(gl.FRAMEBUFFER,
                    gl.COLOR_ATTACHMENT0 + i,
                    gl.TEXTURE_2D,
                    texture._glTextures[this.CONTEXT_UID].texture,
                    0);
            }

            activeTextures.push(gl.COLOR_ATTACHMENT0 + i);
        }

        if (activeTextures.length > 1)
        {
            gl.drawBuffers(activeTextures);
        }

        if (framebuffer.depthTexture)
        {
            const writeDepthTexture = this.writeDepthTexture;

            if (writeDepthTexture)
            {
                const depthTexture = framebuffer.depthTexture;

                this.renderer.texture.bind(depthTexture, 0);

                gl.framebufferTexture2D(gl.FRAMEBUFFER,
                    gl.DEPTH_ATTACHMENT,
                    gl.TEXTURE_2D,
                    depthTexture._glTextures[this.CONTEXT_UID].texture,
                    0);
            }
        }

        if (!fbo.stencil && (framebuffer.stencil || framebuffer.depth))
        {
            fbo.stencil = gl.createRenderbuffer();

            gl.bindRenderbuffer(gl.RENDERBUFFER, fbo.stencil);

            gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, framebuffer.width, framebuffer.height);
            // TODO.. this is depth AND stencil?
            if (!framebuffer.depthTexture)
            { // you can't have both, so one should take priority if enabled
                gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, fbo.stencil);
            }
        }
    }

    /**
     * Disposes framebuffer
     * @param {PIXI.Framebuffer} framebuffer framebuffer that has to be disposed of
     * @param {boolean} [contextLost=false] If context was lost, we suppress all delete function calls
     */
    disposeFramebuffer(framebuffer, contextLost)
    {
        const fbo = framebuffer.glFramebuffers[this.CONTEXT_UID];
        const gl = this.gl;

        if (!fbo)
        {
            return;
        }

        delete framebuffer.glFramebuffers[this.CONTEXT_UID];

        const index = this.managedFramebuffers.indexOf(framebuffer);

        if (index >= 0)
        {
            this.managedFramebuffers.splice(index, 1);
        }

        framebuffer.disposeRunner.remove(this);

        if (!contextLost)
        {
            gl.deleteFramebuffer(fbo.framebuffer);
            if (fbo.stencil)
            {
                gl.deleteRenderbuffer(fbo.stencil);
            }
        }
    }

    /**
     * Disposes all framebuffers, but not textures bound to them
     * @param {boolean} [contextLost=false] If context was lost, we suppress all delete function calls
     */
    disposeAll(contextLost)
    {
        const list = this.managedFramebuffers;

        this.managedFramebuffers = [];

        for (let i = 0; i < list.length; i++)
        {
            this.disposeFramebuffer(list[i], contextLost);
        }
    }

    /**
     * Forcing creation of stencil buffer for current framebuffer, if it wasn't done before.
     * Used by MaskSystem, when its time to use stencil mask for Graphics element.
     *
     * Its an alternative for public lazy `framebuffer.enableStencil`, in case we need stencil without rebind.
     *
     * @private
     */
    forceStencil()
    {
        const framebuffer = this.current;

        if (!framebuffer)
        {
            return;
        }

        const fbo = framebuffer.glFramebuffers[this.CONTEXT_UID];

        if (!fbo || fbo.stencil)
        {
            return;
        }
        framebuffer.enableStencil();

        const w = framebuffer.width;
        const h = framebuffer.height;
        const gl = this.gl;
        const stencil = gl.createRenderbuffer();

        gl.bindRenderbuffer(gl.RENDERBUFFER, stencil);
        gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, w, h);

        fbo.stencil = stencil;
        gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, stencil);
        gl.bindFramebuffer(gl.FRAMEBUFFER, fbo.framebuffer);
    }

    /**
     * resets framebuffer stored state, binds screen framebuffer
     *
     * should be called before renderTexture reset()
     */
    reset()
    {
        this.current = this.unknownFramebuffer;
        this.viewport = new Rectangle();
    }
}
<commit_msg>Critical fix for forceStencil with WebGL1 (#6115)<commit_after>import { System } from '../System';
import { Rectangle } from '@pixi/math';
import { ENV } from '@pixi/constants';
import { settings } from '../settings';
import { Framebuffer } from './Framebuffer';

/**
 * System plugin to the renderer to manage framebuffers.
 *
 * @class
 * @extends PIXI.System
 * @memberof PIXI.systems
 */
export class FramebufferSystem extends System
{
    /**
     * @param {PIXI.Renderer} renderer - The renderer this System works for.
     */
    constructor(renderer)
    {
        super(renderer);

        /**
         * A list of managed framebuffers
         * @member {PIXI.Framebuffer[]}
         * @readonly
         */
        this.managedFramebuffers = [];

        /**
         * Framebuffer value that shows that we don't know what is bound
         * @member {Framebuffer}
         * @readonly
         */
        this.unknownFramebuffer = new Framebuffer(10, 10);
    }

    /**
     * Sets up the renderer context and necessary buffers.
     */
    contextChange()
    {
        const gl = this.gl = this.renderer.gl;

        this.CONTEXT_UID = this.renderer.CONTEXT_UID;
        this.current = this.unknownFramebuffer;
        this.viewport = new Rectangle();
        this.hasMRT = true;
        this.writeDepthTexture = true;

        this.disposeAll(true);

        // webgl2
        if (this.renderer.context.webGLVersion === 1)
        {
            // webgl 1!
            let nativeDrawBuffersExtension = this.renderer.context.extensions.drawBuffers;
            let nativeDepthTextureExtension = this.renderer.context.extensions.depthTexture;

            if (settings.PREFER_ENV === ENV.WEBGL_LEGACY)
            {
                nativeDrawBuffersExtension = null;
                nativeDepthTextureExtension = null;
            }

            if (nativeDrawBuffersExtension)
            {
                gl.drawBuffers = (activeTextures) =>
                    nativeDrawBuffersExtension.drawBuffersWEBGL(activeTextures);
            }
            else
            {
                this.hasMRT = false;
                gl.drawBuffers = () =>
                {
                    // empty
                };
            }

            if (!nativeDepthTextureExtension)
            {
                this.writeDepthTexture = false;
            }
        }
    }

    /**
     * Bind a framebuffer
     *
     * @param {PIXI.Framebuffer} framebuffer
     * @param {PIXI.Rectangle} [frame] frame, default is framebuffer size
     */
    bind(framebuffer, frame)
    {
        const { gl } = this;

        if (framebuffer)
        {
            // TODO caching layer!

            const fbo = framebuffer.glFramebuffers[this.CONTEXT_UID] || this.initFramebuffer(framebuffer);

            if (this.current !== framebuffer)
            {
                this.current = framebuffer;
                gl.bindFramebuffer(gl.FRAMEBUFFER, fbo.framebuffer);
            }
            // make sure all textures are unbound..

            // now check for updates...
            if (fbo.dirtyId !== framebuffer.dirtyId)
            {
                fbo.dirtyId = framebuffer.dirtyId;

                if (fbo.dirtyFormat !== framebuffer.dirtyFormat)
                {
                    fbo.dirtyFormat = framebuffer.dirtyFormat;
                    this.updateFramebuffer(framebuffer);
                }
                else if (fbo.dirtySize !== framebuffer.dirtySize)
                {
                    fbo.dirtySize = framebuffer.dirtySize;
                    this.resizeFramebuffer(framebuffer);
                }
            }

            for (let i = 0; i < framebuffer.colorTextures.length; i++)
            {
                if (framebuffer.colorTextures[i].texturePart)
                {
                    this.renderer.texture.unbind(framebuffer.colorTextures[i].texture);
                }
                else
                {
                    this.renderer.texture.unbind(framebuffer.colorTextures[i]);
                }
            }

            if (framebuffer.depthTexture)
            {
                this.renderer.texture.unbind(framebuffer.depthTexture);
            }

            if (frame)
            {
                this.setViewport(frame.x, frame.y, frame.width, frame.height);
            }
            else
            {
                this.setViewport(0, 0, framebuffer.width, framebuffer.height);
            }
        }
        else
        {
            if (this.current)
            {
                this.current = null;
                gl.bindFramebuffer(gl.FRAMEBUFFER, null);
            }

            if (frame)
            {
                this.setViewport(frame.x, frame.y, frame.width, frame.height);
            }
            else
            {
                this.setViewport(0, 0, this.renderer.width, this.renderer.height);
            }
        }
    }

    /**
     * Set the WebGLRenderingContext's viewport.
     *
     * @param {Number} x - X position of viewport
     * @param {Number} y - Y position of viewport
     * @param {Number} width - Width of viewport
     * @param {Number} height - Height of viewport
     */
    setViewport(x, y, width, height)
    {
        const v = this.viewport;

        if (v.width !== width || v.height !== height || v.x !== x || v.y !== y)
        {
            v.x = x;
            v.y = y;
            v.width = width;
            v.height = height;

            this.gl.viewport(x, y, width, height);
        }
    }

    /**
     * Get the size of the current width and height. Returns object with `width` and `height` values.
     *
     * @member {object}
     * @readonly
     */
    get size()
    {
        if (this.current)
        {
            // TODO store temp
            return { x: 0, y: 0, width: this.current.width, height: this.current.height };
        }

        return { x: 0, y: 0, width: this.renderer.width, height: this.renderer.height };
    }

    /**
     * Clear the color of the context
     *
     * @param {Number} r - Red value from 0 to 1
     * @param {Number} g - Green value from 0 to 1
     * @param {Number} b - Blue value from 0 to 1
     * @param {Number} a - Alpha value from 0 to 1
     */
    clear(r, g, b, a)
    {
        const { gl } = this;

        // TODO clear color can be set only one right?
        gl.clearColor(r, g, b, a);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    }

    /**
     * Initialize framebuffer
     *
     * @protected
     * @param {PIXI.Framebuffer} framebuffer
     */
    initFramebuffer(framebuffer)
    {
        const { gl } = this;

        // TODO - make this a class?
        const fbo = {
            framebuffer: gl.createFramebuffer(),
            stencil: null,
            dirtyId: 0,
            dirtyFormat: 0,
            dirtySize: 0,
        };

        framebuffer.glFramebuffers[this.CONTEXT_UID] = fbo;

        this.managedFramebuffers.push(framebuffer);
        framebuffer.disposeRunner.add(this);

        return fbo;
    }

    /**
     * Resize the framebuffer
     *
     * @protected
     * @param {PIXI.Framebuffer} framebuffer
     */
    resizeFramebuffer(framebuffer)
    {
        const { gl } = this;

        const fbo = framebuffer.glFramebuffers[this.CONTEXT_UID];

        if (fbo.stencil)
        {
            gl.bindRenderbuffer(gl.RENDERBUFFER, fbo.stencil);
            gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, framebuffer.width, framebuffer.height);
        }

        const colorTextures = framebuffer.colorTextures;

        for (let i = 0; i < colorTextures.length; i++)
        {
            this.renderer.texture.bind(colorTextures[i], 0);
        }

        if (framebuffer.depthTexture)
        {
            this.renderer.texture.bind(framebuffer.depthTexture, 0);
        }
    }

    /**
     * Update the framebuffer
     *
     * @protected
     * @param {PIXI.Framebuffer} framebuffer
     */
    updateFramebuffer(framebuffer)
    {
        const { gl } = this;

        const fbo = framebuffer.glFramebuffers[this.CONTEXT_UID];

        // bind the color texture
        const colorTextures = framebuffer.colorTextures;

        let count = colorTextures.length;

        if (!gl.drawBuffers)
        {
            count = Math.min(count, 1);
        }

        const activeTextures = [];

        for (let i = 0; i < count; i++)
        {
            const texture = framebuffer.colorTextures[i];

            if (texture.texturePart)
            {
                this.renderer.texture.bind(texture.texture, 0);

                gl.framebufferTexture2D(gl.FRAMEBUFFER,
                    gl.COLOR_ATTACHMENT0 + i,
                    gl.TEXTURE_CUBE_MAP_NEGATIVE_X + texture.side,
                    texture.texture._glTextures[this.CONTEXT_UID].texture,
                    0);
            }
            else
            {
                this.renderer.texture.bind(texture, 0);

                gl.framebufferTexture2D(gl.FRAMEBUFFER,
                    gl.COLOR_ATTACHMENT0 + i,
                    gl.TEXTURE_2D,
                    texture._glTextures[this.CONTEXT_UID].texture,
                    0);
            }

            activeTextures.push(gl.COLOR_ATTACHMENT0 + i);
        }

        if (activeTextures.length > 1)
        {
            gl.drawBuffers(activeTextures);
        }

        if (framebuffer.depthTexture)
        {
            const writeDepthTexture = this.writeDepthTexture;

            if (writeDepthTexture)
            {
                const depthTexture = framebuffer.depthTexture;

                this.renderer.texture.bind(depthTexture, 0);

                gl.framebufferTexture2D(gl.FRAMEBUFFER,
                    gl.DEPTH_ATTACHMENT,
                    gl.TEXTURE_2D,
                    depthTexture._glTextures[this.CONTEXT_UID].texture,
                    0);
            }
        }

        if (!fbo.stencil && (framebuffer.stencil || framebuffer.depth))
        {
            fbo.stencil = gl.createRenderbuffer();

            gl.bindRenderbuffer(gl.RENDERBUFFER, fbo.stencil);

            gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, framebuffer.width, framebuffer.height);
            // TODO.. this is depth AND stencil?
            if (!framebuffer.depthTexture)
            { // you can't have both, so one should take priority if enabled
                gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, fbo.stencil);
            }
        }
    }

    /**
     * Disposes framebuffer
     * @param {PIXI.Framebuffer} framebuffer framebuffer that has to be disposed of
     * @param {boolean} [contextLost=false] If context was lost, we suppress all delete function calls
     */
    disposeFramebuffer(framebuffer, contextLost)
    {
        const fbo = framebuffer.glFramebuffers[this.CONTEXT_UID];
        const gl = this.gl;

        if (!fbo)
        {
            return;
        }

        delete framebuffer.glFramebuffers[this.CONTEXT_UID];

        const index = this.managedFramebuffers.indexOf(framebuffer);

        if (index >= 0)
        {
            this.managedFramebuffers.splice(index, 1);
        }

        framebuffer.disposeRunner.remove(this);

        if (!contextLost)
        {
            gl.deleteFramebuffer(fbo.framebuffer);
            if (fbo.stencil)
            {
                gl.deleteRenderbuffer(fbo.stencil);
            }
        }
    }

    /**
     * Disposes all framebuffers, but not textures bound to them
     * @param {boolean} [contextLost=false] If context was lost, we suppress all delete function calls
     */
    disposeAll(contextLost)
    {
        const list = this.managedFramebuffers;

        this.managedFramebuffers = [];

        for (let i = 0; i < list.length; i++)
        {
            this.disposeFramebuffer(list[i], contextLost);
        }
    }

    /**
     * Forcing creation of stencil buffer for current framebuffer, if it wasn't done before.
     * Used by MaskSystem, when its time to use stencil mask for Graphics element.
     *
     * Its an alternative for public lazy `framebuffer.enableStencil`, in case we need stencil without rebind.
     *
     * @private
     */
    forceStencil()
    {
        const framebuffer = this.current;

        if (!framebuffer)
        {
            return;
        }

        const fbo = framebuffer.glFramebuffers[this.CONTEXT_UID];

        if (!fbo || fbo.stencil)
        {
            return;
        }
        framebuffer.enableStencil();

        const w = framebuffer.width;
        const h = framebuffer.height;
        const gl = this.gl;
        const stencil = gl.createRenderbuffer();

        gl.bindRenderbuffer(gl.RENDERBUFFER, stencil);
        gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, w, h);

        fbo.stencil = stencil;
        gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, stencil);
    }

    /**
     * resets framebuffer stored state, binds screen framebuffer
     *
     * should be called before renderTexture reset()
     */
    reset()
    {
        this.current = this.unknownFramebuffer;
        this.viewport = new Rectangle();
    }
}
<|endoftext|>


<commit_before>// The file system API.
// Copyright © 2012 Thaddee Tyl, Jan Keromnes. All rights reserved.
// The following code is covered by the GPLv2 license.

var fs = require('./fs'),
    nodepath = require('path');

// read(1) - Read information about a file or folder, represented by a `path`.
// If `depth` is non-negative, read folder subfiles recursively.
// If it is zero, read file content.
// If non-positive or undefined, give information about the file.
//
// The `end` function gets an object {file: []} which is a list of
// {path: …, date: …, meta: …}.
function read(path, depth, end) {
  var data = { files: [] }, depth = depth || 0;
  fs.file(path, function(err, file) {
    if (err) { data.err = err; end(data); return; }
    data.files.push({ path: file.path, date: +file.date, meta: file.meta });
    // negative depth: return only file info, not content or subfiles
    if (depth < 0) { end(data); return; }
    if (file.meta.type !== 'dir') {
      // get the file's content
      file.open(function(err, content) {
        if (err) { data.err = err; end(data); return; }
        data.files[0].content = content;
        end(data);
      });
    } else {
      // get the folder's subfiles
      file.files(function (err, files) {
        if (err) { data.err = err; end(data); return; }
        var counter = files.length;
        if (counter <= 0) { end(data); return; }
        for (var i = 0; i < files.length; i++) {
          // only recurse on subfolders
          var subdepth = files[i].meta.type === 'dir' ? depth - 1 : -1;
          read(files[i].path, subdepth, function(d) {
            if (d.err) { data.err = d.err; }
            data.files.push.apply(data.files, d.files);
            counter--;
            if (counter <= 0) end(data);
          });
        }
      });
    }
  });
}

// apply(1) - Apply a set of operations to a file's content.
// See Operational Transformation.
function apply(path, operations, end) {
  // TODO implement if needed.
}

// create(1) - Create a new file or folder, represented by a `path`.
// If `type` is set to "dir", a folder will be created -- default is file.
function create(path, type, end) {
  console.log('called create', arguments);
  var data = {}, parent = nodepath.dirname(path), name = nodepath.basename(path);;
  fs.file(parent, function (err, dir) {
    if (err) { data.err = err; end(data); return; }
    // file or folder?
    (type === "dir" ? dir.mkdir : dir.mkfile).call(dir, name, function (err) {
      if (err) { data.err = err; end(data); return; }
      data.path = path;
      end(data);
    });
  });
}

// delete(1) - Delete a file or folder, represented by a `path`.
function rm(path, end) {
  fs.file(path, function (err, file) {
    if (err) { end({err: err}); return; }
    file.rm(function (err) {
      if (err) { end({err: err}); return; }
      end({path: path});
    });
  });
}

function fsOperation(query, end) {
  // `query` must have an `op` field, which is a String.
  // It must also have a `path` field, which is a String.
  if (query.op == null) {
    end({err: 'An invalid request was sent to the file system.'});
  }
  if (query.path == null) {
    end({err: 'A request was sent to the file system, ' +
              'but did not specify the path.'});
  }
  var data = {};
  switch (query.op) {
    case 'read': case 'cat': // case 'ls':
      read(query.path, query.depth || 0, end);
      break;
    case 'create': case 'new':
      create(query.path, query.type, end);
      break;
    case 'delete': case 'rm':
      rm(query.path, end);
      break;
    // FIXME `ls` is deprecated, use `read` instead.
    case 'ls':
      fs.file(query.path, function (err, dir) {
        if (err) {
          data.err = err;
          return end(data);
        }
        // ls -r
        if (query.depth && query.depth > 1) {
          dir.subfiles(function (err, subfiles) {
            data.leafs = subfiles;
            end(data);
          }, query.depth);
        // ls .
        } else {
          dir.files(function (err, files) {
            if (err) { data.err = err; end(data); return; }
            data.files = [];
            for (var i = 0; i < files.length; i++) {
              data.files.push({
                name: files[i],
                type: fs.type.nameFromType[files[i].type]
              });
            }
            end(data);
          });
        }
      });
      break;
    default:
      end({err: 'Unknown file system operation ' + query.op + '.'});
  }
}

function metaSave(query, end) {
  console.log('meta-save', query.path, query.meta);
  fs.file(query.path, function(err, file) {
    if (err) {
      end({err:err});
    } else if (file) {
      file.meta = query.meta;
      file.writeMeta(function(err) { end({err:err}); });
    } else end({err:'File ' + query.path + ' is undefined.'});
  });
}

exports.read = read;
exports.apply = apply;
exports.create = create;
exports.rm = rm;
exports.fs = fsOperation;
exports.meta = metaSave;
<commit_msg>fsapi: cleanup in comments and style.<commit_after>// The file system API.
// Copyright © 2012 Thaddee Tyl, Jan Keromnes. All rights reserved.
// The following code is covered by the GPLv2 license.

var fs = require('./fs'),
    nodepath = require('path');

// read(1) - Read information about a file or folder, represented by a `path`.
// If `depth` is non-negative, read folder subfiles recursively.
// If it is zero, read file content.
// If non-positive or undefined, give information about the file.
//
// The `end` function gets either an object {files: []} which is a list of
// {path: …, date: …, meta: …}, or {err: 'message'}.
function read(path, depth, end) {
  var data = { files: [] }, depth = depth || 0;
  fs.file(path, function(err, file) {
    if (err) { data.err = err; end(data); return; }
    data.files.push({ path: file.path, date: +file.date, meta: file.meta });
    // negative depth: return only file info, not content or subfiles
    if (depth < 0) { end(data); return; }
    if (file.meta.type !== 'dir') {
      // get the file's content
      file.open(function(err, content) {
        if (err) { data.err = err; end(data); return; }
        data.files[0].content = content;
        end(data);
      });
    } else {
      // get the folder's subfiles
      file.files(function (err, files) {
        if (err) { data.err = err; end(data); return; }
        var counter = files.length;
        if (counter <= 0) { end(data); return; }
        for (var i = 0; i < files.length; i++) {
          // only recurse on subfolders
          var subdepth = files[i].meta.type === 'dir' ? depth - 1 : -1;
          read(files[i].path, subdepth, function(d) {
            if (d.err) { data.err = d.err; }
            data.files.push.apply(data.files, d.files);
            counter--;
            if (counter <= 0) end(data);
          });
        }
      });
    }
  });
}

// apply(1) - Apply a set of operations to a file's content.
// See Operational Transformation.
function apply(path, operations, end) {
  // TODO implement if needed.
}

// create(1) - Create a new file or folder, represented by a `path`.
// If `type` is set to "dir", a folder will be created -- default is file.
function create(path, type, end) {
  console.log('creating file', path);
  var data = {};
  var parent = nodepath.dirname(path);
  var name = nodepath.basename(path);
  fs.file(parent, function (err, dir) {
    if (err) {
      data.err = err;
      end(data);
      return;
    }
    // file or folder?
    (type === "dir" ? dir.mkdir : dir.mkfile).call(dir, name, function (err) {
      if (err) { data.err = err; end(data); return; }
      data.path = path;
      end(data);
    });
  });
}

// delete(1) - Delete a file or folder, represented by a `path`.
function rm(path, end) {
  fs.file(path, function (err, file) {
    if (err) { end({err: err}); return; }
    file.rm(function (err) {
      if (err) { end({err: err}); return; }
      end({path: path});
    });
  });
}

function fsOperation(query, end) {
  // `query` must have an `op` field, which is a String.
  // It must also have a `path` field, which is a String.
  if (query.op == null) {
    end({err: 'An invalid request was sent to the file system.'});
  }
  if (query.path == null) {
    end({err: 'A request was sent to the file system, ' +
              'but did not specify the path.'});
  }
  var data = {};
  switch (query.op) {
    case 'read': case 'cat': // case 'ls':
      read(query.path, query.depth || 0, end);
      break;
    case 'create': case 'new':
      create(query.path, query.type, end);
      break;
    case 'delete': case 'rm':
      rm(query.path, end);
      break;
    // FIXME `ls` is deprecated, use `read` instead.
    case 'ls':
      fs.file(query.path, function (err, dir) {
        if (err) {
          data.err = err;
          return end(data);
        }
        // ls -r
        if (query.depth && query.depth > 1) {
          dir.subfiles(function (err, subfiles) {
            data.leafs = subfiles;
            end(data);
          }, query.depth);
        // ls .
        } else {
          dir.files(function (err, files) {
            if (err) { data.err = err; end(data); return; }
            data.files = [];
            for (var i = 0; i < files.length; i++) {
              data.files.push({
                name: files[i],
                type: fs.type.nameFromType[files[i].type]
              });
            }
            end(data);
          });
        }
      });
      break;
    default:
      end({err: 'Unknown file system operation ' + query.op + '.'});
  }
}

function metaSave(query, end) {
  console.log('meta-save', query.path, query.meta);
  fs.file(query.path, function(err, file) {
    if (err) {
      end({err:err});
    } else if (file) {
      file.meta = query.meta;
      file.writeMeta(function(err) { end({err:err}); });
    } else end({err:'File ' + query.path + ' is undefined.'});
  });
}

exports.read = read;
exports.apply = apply;
exports.create = create;
exports.rm = rm;
exports.fs = fsOperation;
exports.meta = metaSave;
<|endoftext|>


<commit_before>var generators = require('yeoman-generator');
var gh = require('parse-github-url');
var deepAssign = require('deep-assign');
var compact = require('lodash.compact');
var uniq = require('lodash.uniq');
var sortPkg = require('sort-pkg');
var dashCase = require('lodash.kebabcase');
var isScopedPackage = require('is-scoped-package');

/**
 * @param  {String} pkgName
 *
 * @return {String}
 */
function preparePkgName ( pkgName ) {
	var scopedPkgName;
	if ( isScopedPackage(pkgName) ) {
		scopedPkgName = pkgName.split('/');
		return [scopedPkgName[0], dashCase(scopedPkgName[1])].join('/');
	}
	return dashCase(pkgName);
}

module.exports = generators.Base.extend({

	prompting: function () {

		var done = this.async();
		var pkg = this.fs.readJSON(this.destinationPath('package.json'), {});

		this.prompt([
			{
				type: 'input',
				name: 'name',
				message: 'What is the name of the project?',
				'default': function () {
					var pkgName;
					if ( pkg.name ) {
						pkgName = pkg.name;
					} else if ( this.appname ) {
						pkgName = this.appname;
					} else {
						pkgName = '';
					}
					return preparePkgName(pkgName);
				}.bind(this)
			},
			{
				type: 'input',
				name: 'description',
				message: 'What is the description of the project?',
				'default': function () {
					if ( pkg.description ) {
						return pkg.description;
					}
					return '';
				}
			},
			{
				type: 'input',
				name: 'keywords',
				message: 'What are the keywords for this project?',
				'default': function () {
					if ( pkg.keywords ) {
						return pkg.keywords.join(', ');
					}
					return '';
				}
			},
			{
				type: 'input',
				name: 'gitRepo',
				message: 'What is the Git repository of the project?',
				'default': function ( answers ) {
					if ( pkg.repository ) {
						return gh(pkg.repository.url).repository;
					}
					return 'niksy/' + answers.name;
				}
			},
			{
				type: 'confirm',
				name: 'onlyNodeLts',
				message: 'Run only on Node LTS (>=4)?',
				'default': true
			},
			{
				type: 'confirm',
				name: 'cli',
				message: 'Does this project has CLI interface?',
				'default': false
			},
			{
				type: 'confirm',
				name: 'browserModule',
				message: 'Is this project meant to be used in browser?',
				'default': false
			},
			{
				type: 'confirm',
				name: 'styles',
				message: 'Does this project has styles?',
				'default': false,
				when: function ( answers ) {
					return answers.browserModule;
				}
			},
			{
				type: 'confirm',
				name: 'jqueryModule',
				message: 'Does this project need jQuery as dependancy?',
				'default': false,
				when: function ( answers ) {
					return answers.browserModule;
				}
			},
			{
				type: 'confirm',
				name: 'manualTests',
				message: 'Do you have manual tests?',
				'default': false
			},
			{
				type: 'confirm',
				name: 'automatedTests',
				message: 'Do you have automated tests?',
				'default': false
			},
			{
				type: 'confirm',
				name: 'integrationTests',
				message: 'Do you have integration (UI/E2E/Selenium) tests?',
				'default': false,
				when: function ( answers ) {
					return answers.automatedTests;
				}
			},
			{
				type: 'list',
				name: 'testingInterface',
				message: 'What testing interface do you use?',
				'default': 'bdd',
				choices: [{
					name: 'BDD',
					value: 'bdd'
				}, {
					name: 'TDD',
					value: 'tdd'
				}],
				when: function ( answers ) {
					return answers.automatedTests;
				}
			},
			{
				type: 'confirm',
				name: 'codeCoverage',
				message: 'Do you need code coverage?',
				'default': false,
				when: function ( answers ) {
					return answers.automatedTests;
				}
			},
			{
				type: 'confirm',
				name: 'codeCoverageService',
				message: 'Do you want to send code coverage report to Coveralls?',
				'default': false,
				when: function ( answers ) {
					return answers.codeCoverage;
				}
			}
		], function ( answers ) {

			var keywords = uniq(compact(answers.keywords.split(',')
				.map(function ( keyword ) {
					return keyword.trim();
				})
				.filter(function ( keyword ) {
					return keyword !== '';
				})));

			var tpl = {
				moduleName: preparePkgName(answers.name),
				cleanModuleName: preparePkgName(answers.name).replace(/^@.+?\//, ''),
				moduleDescription: answers.description,
				browserModule: answers.browserModule,
				styles: answers.styles,
				jqueryModule: answers.jqueryModule,
				onlyNodeLts: answers.onlyNodeLts,
				cli: answers.cli,
				manualTests: answers.manualTests,
				automatedTests: answers.automatedTests,
				integrationTests: answers.integrationTests,
				testingInterface: answers.testingInterface,
				codeCoverage: answers.codeCoverage,
				codeCoverageService: answers.codeCoverageService,
				gitRepo: gh(answers.gitRepo),
				keywords: keywords,
				version: pkg.version,
				humanName: 'Ivan Nikolić',
				username: 'niksy',
				website: 'http://ivannikolic.com',
				email: 'niksy5@gmail.com'
			};

			var cp = function ( from, to ) {
				this.fs.copyTpl(this.templatePath(from), this.destinationPath(to), tpl);
			}.bind(this);
			var rm = function ( to ) {
				this.fs.delete(this.destinationPath(to));
			}.bind(this);
			var newPkg, mergedPkg;

			cp('README.md', 'README.md');
			cp('LICENSE.md', 'LICENSE.md');
			cp('editorconfig', '.editorconfig');
			cp('eslintrc', '.eslintrc');
			cp('gitignore', '.gitignore');
			cp('index.js', 'index.js');

			if ( answers.cli ) {
				cp('cli.js', 'cli.js');
			}

			if ( answers.browserModule && answers.styles ) {
				cp('stylelintrc', '.stylelintrc');
			}

			if ( answers.manualTests || answers.automatedTests ) {
				cp('test/eslintrc', 'test/.eslintrc');
			}
			if ( !answers.manualTests && !answers.automatedTests ) {
				rm('test');
			}

			if ( answers.automatedTests ) {
				cp('travis.yml', '.travis.yml');
				if ( answers.browserModule ) {
					cp('test/automated/fixtures', 'test/automated/fixtures');
					cp('test/automated/index.js', 'test/automated/index.js');
					cp('karma.conf.js', 'karma.conf.js');
				} else {
					cp('test/index.js', 'test/index.js');
				}
				if ( answers.codeCoverage ) {
					cp('istanbul.yml', '.istanbul.yml');
				}
			} else {
				rm('.travis.yml');
				rm('test/index.js');
				rm('test/automated');
				rm('karma.conf.js');
				rm('.istanbul.yml');
			}

			if ( answers.manualTests || answers.integrationTests ) {
				cp('test/manual', 'test/manual');
				cp('gulpfile.js', 'gulpfile.js');
			} else {
				rm('test/manual');
				rm('gulpile.js');
			}

			if ( answers.integrationTests ) {
				cp('test/integration/index.js', 'test/integration/index.js');
				cp('test/integration/eslintrc', 'test/integration/.eslintrc');
				cp('wdio.conf.js', 'wdio.conf.js');
			} else {
				rm('test/integration');
				rm('wdio.conf.js');
			}

			// Write package.json, handling property order
			cp('_package.json', '_package.json');
			newPkg = this.fs.readJSON(this.destinationPath('_package.json'));
			rm('_package.json');

			mergedPkg = deepAssign({}, pkg, newPkg);

			// deep-assign overwrites arrays so we have to prepare
			// new array before writing new JSON file
			if ( Array.isArray(keywords) && keywords.length ) {
				mergedPkg.keywords = keywords;
			}

			mergedPkg = sortPkg(mergedPkg);

			this.fs.writeJSON(this.destinationPath('package.json'), mergedPkg);

			done();

		}.bind(this));

	},

	install: function () {
		this.npmInstall();
	}

});
<commit_msg>Use "initializing" and "writing" methods<commit_after>var generators = require('yeoman-generator');
var gh = require('parse-github-url');
var deepAssign = require('deep-assign');
var compact = require('lodash.compact');
var uniq = require('lodash.uniq');
var sortPkg = require('sort-pkg');
var dashCase = require('lodash.kebabcase');
var isScopedPackage = require('is-scoped-package');

/**
 * @param  {String} pkgName
 *
 * @return {String}
 */
function preparePkgName ( pkgName ) {
	var scopedPkgName;
	if ( isScopedPackage(pkgName) ) {
		scopedPkgName = pkgName.split('/');
		return [scopedPkgName[0], dashCase(scopedPkgName[1])].join('/');
	}
	return dashCase(pkgName);
}

module.exports = generators.Base.extend({

	initializing: function () {
		this.pkg = this.fs.readJSON(this.destinationPath('package.json'), {});
	},

	prompting: function () {

		var done = this.async();
		var pkg = this.pkg;

		this.prompt([
			{
				type: 'input',
				name: 'name',
				message: 'What is the name of the project?',
				'default': function () {
					var pkgName;
					if ( pkg.name ) {
						pkgName = pkg.name;
					} else if ( this.appname ) {
						pkgName = this.appname;
					} else {
						pkgName = '';
					}
					return preparePkgName(pkgName);
				}.bind(this)
			},
			{
				type: 'input',
				name: 'description',
				message: 'What is the description of the project?',
				'default': function () {
					if ( pkg.description ) {
						return pkg.description;
					}
					return '';
				}
			},
			{
				type: 'input',
				name: 'keywords',
				message: 'What are the keywords for this project?',
				'default': function () {
					if ( pkg.keywords ) {
						return pkg.keywords.join(', ');
					}
					return '';
				}
			},
			{
				type: 'input',
				name: 'gitRepo',
				message: 'What is the Git repository of the project?',
				'default': function ( answers ) {
					if ( pkg.repository ) {
						return gh(pkg.repository.url).repository;
					}
					return 'niksy/' + answers.name;
				}
			},
			{
				type: 'confirm',
				name: 'onlyNodeLts',
				message: 'Run only on Node LTS (>=4)?',
				'default': true
			},
			{
				type: 'confirm',
				name: 'cli',
				message: 'Does this project has CLI interface?',
				'default': false
			},
			{
				type: 'confirm',
				name: 'browserModule',
				message: 'Is this project meant to be used in browser?',
				'default': false
			},
			{
				type: 'confirm',
				name: 'styles',
				message: 'Does this project has styles?',
				'default': false,
				when: function ( answers ) {
					return answers.browserModule;
				}
			},
			{
				type: 'confirm',
				name: 'jqueryModule',
				message: 'Does this project need jQuery as dependancy?',
				'default': false,
				when: function ( answers ) {
					return answers.browserModule;
				}
			},
			{
				type: 'confirm',
				name: 'manualTests',
				message: 'Do you have manual tests?',
				'default': false
			},
			{
				type: 'confirm',
				name: 'automatedTests',
				message: 'Do you have automated tests?',
				'default': false
			},
			{
				type: 'confirm',
				name: 'integrationTests',
				message: 'Do you have integration (UI/E2E/Selenium) tests?',
				'default': false,
				when: function ( answers ) {
					return answers.automatedTests;
				}
			},
			{
				type: 'list',
				name: 'testingInterface',
				message: 'What testing interface do you use?',
				'default': 'bdd',
				choices: [{
					name: 'BDD',
					value: 'bdd'
				}, {
					name: 'TDD',
					value: 'tdd'
				}],
				when: function ( answers ) {
					return answers.automatedTests;
				}
			},
			{
				type: 'confirm',
				name: 'codeCoverage',
				message: 'Do you need code coverage?',
				'default': false,
				when: function ( answers ) {
					return answers.automatedTests;
				}
			},
			{
				type: 'confirm',
				name: 'codeCoverageService',
				message: 'Do you want to send code coverage report to Coveralls?',
				'default': false,
				when: function ( answers ) {
					return answers.codeCoverage;
				}
			}
		], function ( answers ) {
			this.answers = answers;
			done();
		}.bind(this));

	},

	writing: function () {

		var answers = this.answers;
		var pkg = this.pkg;

		var keywords = uniq(compact(answers.keywords.split(',')
			.map(function ( keyword ) {
				return keyword.trim();
			})
			.filter(function ( keyword ) {
				return keyword !== '';
			})));

		var tpl = {
			moduleName: preparePkgName(answers.name),
			cleanModuleName: preparePkgName(answers.name).replace(/^@.+?\//, ''),
			moduleDescription: answers.description,
			browserModule: answers.browserModule,
			styles: answers.styles,
			jqueryModule: answers.jqueryModule,
			onlyNodeLts: answers.onlyNodeLts,
			cli: answers.cli,
			manualTests: answers.manualTests,
			automatedTests: answers.automatedTests,
			integrationTests: answers.integrationTests,
			testingInterface: answers.testingInterface,
			codeCoverage: answers.codeCoverage,
			codeCoverageService: answers.codeCoverageService,
			gitRepo: gh(answers.gitRepo),
			keywords: keywords,
			version: pkg.version,
			humanName: 'Ivan Nikolić',
			username: 'niksy',
			website: 'http://ivannikolic.com',
			email: 'niksy5@gmail.com'
		};

		var cp = function ( from, to ) {
			this.fs.copyTpl(this.templatePath(from), this.destinationPath(to), tpl);
		}.bind(this);
		var rm = function ( to ) {
			this.fs.delete(this.destinationPath(to));
		}.bind(this);
		var newPkg, mergedPkg;

		cp('README.md', 'README.md');
		cp('LICENSE.md', 'LICENSE.md');
		cp('editorconfig', '.editorconfig');
		cp('eslintrc', '.eslintrc');
		cp('gitignore', '.gitignore');
		cp('index.js', 'index.js');

		if ( answers.cli ) {
			cp('cli.js', 'cli.js');
		}

		if ( answers.browserModule && answers.styles ) {
			cp('stylelintrc', '.stylelintrc');
		}

		if ( answers.manualTests || answers.automatedTests ) {
			cp('test/eslintrc', 'test/.eslintrc');
		}
		if ( !answers.manualTests && !answers.automatedTests ) {
			rm('test');
		}

		if ( answers.automatedTests ) {
			cp('travis.yml', '.travis.yml');
			if ( answers.browserModule ) {
				cp('test/automated/fixtures', 'test/automated/fixtures');
				cp('test/automated/index.js', 'test/automated/index.js');
				cp('karma.conf.js', 'karma.conf.js');
			} else {
				cp('test/index.js', 'test/index.js');
			}
			if ( answers.codeCoverage ) {
				cp('istanbul.yml', '.istanbul.yml');
			}
		} else {
			rm('.travis.yml');
			rm('test/index.js');
			rm('test/automated');
			rm('karma.conf.js');
			rm('.istanbul.yml');
		}

		if ( answers.manualTests || answers.integrationTests ) {
			cp('test/manual', 'test/manual');
			cp('gulpfile.js', 'gulpfile.js');
		} else {
			rm('test/manual');
			rm('gulpile.js');
		}

		if ( answers.integrationTests ) {
			cp('test/integration/index.js', 'test/integration/index.js');
			cp('test/integration/eslintrc', 'test/integration/.eslintrc');
			cp('wdio.conf.js', 'wdio.conf.js');
		} else {
			rm('test/integration');
			rm('wdio.conf.js');
		}

		// Write package.json, handling property order
		cp('_package.json', '_package.json');
		newPkg = this.fs.readJSON(this.destinationPath('_package.json'));
		rm('_package.json');

		mergedPkg = deepAssign({}, pkg, newPkg);

		// deep-assign overwrites arrays so we have to prepare
		// new array before writing new JSON file
		if ( Array.isArray(keywords) && keywords.length ) {
			mergedPkg.keywords = keywords;
		}

		mergedPkg = sortPkg(mergedPkg);

		this.fs.writeJSON(this.destinationPath('package.json'), mergedPkg);

	},

	install: function () {
		this.npmInstall();
	}

});
<|endoftext|>


<commit_before>import { onConnect, onMessage, sendToTab } from 'crossmessaging';
import updateState from 'remotedev-app/lib/store/updateState';
import syncOptions from '../options/syncOptions';
import createMenu from './contextMenus';
import openDevToolsWindow from './openWindow';
let panelConnections = {};
let tabConnections = {};
let catchedErrors = {};

window.syncOptions = syncOptions; // Used in the options page

const naMessage = { type: 'NA' };

function initPanel(msg, port) {
  panelConnections[msg.tabId] = port;
  if (msg.tabId !== store.id) return naMessage;
}

function initInstance(msg, port) {
  const id = port.sender.tab.id;
  tabConnections[id] = port;
  store.liftedStore.instances[id] = msg.instance;
  store.id = id;
  if (typeof id === 'number') chrome.pageAction.show(id);
  return { type: 'START' };
}

function disconnect(port) {
  if (!port.sender.tab) return;
  const id = port.sender.tab.id;
  delete tabConnections[id];
  if (panelConnections[id]) panelConnections[id].postMessage(naMessage);
  if (window.store.liftedStore.instances[id]) {
    delete window.store.liftedStore.instances[id];
    window.store.liftedStore.deleteInstance(id);
  }
}

onConnect(undefined, {
  INIT_PANEL: initPanel,
  INIT_INSTANCE: initInstance,
  RELAY: (msg, port) => { messaging(msg.message, port.sender); }
}, panelConnections, disconnect);

function handleInstancesChanged(instance, name) {
  window.store.liftedStore.instances[instance] = name || instance;
}

// Receive message from content script
function messaging(request, sender, sendResponse) {
  const tabId = sender.tab ? sender.tab.id : sender.id;
  if (tabId) {
    if (request.type === 'GET_OPTIONS') {
      syncOptions.get(options => {
        sendResponse({options: options});
      });
      return true;
    }
    if (request.type === 'OPEN') {
      let position = 'devtools-left';
      if (['remote', 'panel', 'left', 'right', 'bottom'].indexOf(request.position) !== -1) position = 'devtools-' + request.position;
      openDevToolsWindow(position);
      return true;
    }
    if (request.type === 'ERROR') {
      chrome.notifications.create('app-error', {
        type: 'basic',
        title: 'An error occurred in the app',
        message: request.message,
        iconUrl: 'img/logo/48x48.png',
        isClickable: false
      });
      return true;
    }

    request.id = tabId;
    const payload = updateState(store, request, handleInstancesChanged, store.liftedStore.instance);
    if (!payload) return true;

    // Relay the message to the devTools panel
    if (tabId in panelConnections) {
      panelConnections[tabId].postMessage(request);
    }

    // Notify when errors occur in the app
    syncOptions.get(options => {
      if (!options.notifyErrors) return;
      const error = payload.computedStates[payload.currentStateIndex].error;
      if (error === 'Interrupted by an error up the chain') return;
      if (error) {
        chrome.notifications.create('redux-error', {
          type: 'basic',
          title: 'An error occurred in the reducer',
          message: error,
          iconUrl: 'img/logo/48x48.png',
          isClickable: true
        });
        if (typeof store.id === 'number') {
          chrome.pageAction.setIcon({tabId: store.id, path: 'img/logo/error.png'});
          catchedErrors.tab = store.id;
        }
      } else if (catchedErrors.last && typeof store.id === 'number' && catchedErrors.tab === store.id) {
        chrome.pageAction.setIcon({tabId: store.id, path: 'img/logo/38x38.png'});
      }
      catchedErrors.last = error;
    });
  }
  return true;
}

onMessage(messaging);

chrome.notifications.onClicked.addListener(id => {
  chrome.notifications.clear(id);
  if (id === 'redux-error') openDevToolsWindow('devtools-right');
});

export function toContentScript(action) {
  const message = { type: 'DISPATCH', action: action };
  let id = store.liftedStore.instance;
  if (!id || id === 'auto') id = store.id;
  if (id in panelConnections) {
    panelConnections[id].postMessage(message);
  } else {
    tabConnections[id].postMessage(message);
  }
}
<commit_msg>Support inspecting chrome extensions and apps<commit_after>import { onConnect, onMessage, sendToTab } from 'crossmessaging';
import updateState from 'remotedev-app/lib/store/updateState';
import syncOptions from '../options/syncOptions';
import createMenu from './contextMenus';
import openDevToolsWindow from './openWindow';
let panelConnections = {};
let tabConnections = {};
let catchedErrors = {};

window.syncOptions = syncOptions; // Used in the options page

const naMessage = { type: 'NA' };

function initPanel(msg, port) {
  panelConnections[msg.tabId] = port;
  if (msg.tabId !== store.id) return naMessage;
}

function getId(port) {
  return port.sender.tab ? port.sender.tab.id : port.sender.id;
}

function initInstance(msg, port) {
  const id = getId(port);
  tabConnections[id] = port;
  store.liftedStore.instances[id] = msg.instance;
  store.id = id;
  if (typeof id === 'number') chrome.pageAction.show(id);
  return { type: 'START' };
}

function disconnect(port) {
  if (!port.sender.tab && port.id === chrome.runtime.id) return;
  const id = getId(port);
  delete tabConnections[id];
  if (panelConnections[id]) panelConnections[id].postMessage(naMessage);
  if (window.store.liftedStore.instances[id]) {
    delete window.store.liftedStore.instances[id];
    window.store.liftedStore.deleteInstance(id);
  }
}

onConnect(undefined, {
  INIT_PANEL: initPanel,
  INIT_INSTANCE: initInstance,
  RELAY: (msg, port) => { messaging(msg.message, port.sender); }
}, panelConnections, disconnect);

function handleInstancesChanged(instance, name) {
  window.store.liftedStore.instances[instance] = name || instance;
}

// Receive message from content script
function messaging(request, sender, sendResponse) {
  const tabId = sender.tab ? sender.tab.id : sender.id;
  if (tabId) {
    if (request.type === 'GET_OPTIONS') {
      syncOptions.get(options => {
        sendResponse({options: options});
      });
      return true;
    }
    if (request.type === 'OPEN') {
      let position = 'devtools-left';
      if (['remote', 'panel', 'left', 'right', 'bottom'].indexOf(request.position) !== -1) position = 'devtools-' + request.position;
      openDevToolsWindow(position);
      return true;
    }
    if (request.type === 'ERROR') {
      chrome.notifications.create('app-error', {
        type: 'basic',
        title: 'An error occurred in the app',
        message: request.message,
        iconUrl: 'img/logo/48x48.png',
        isClickable: false
      });
      return true;
    }

    request.id = tabId;
    const payload = updateState(store, request, handleInstancesChanged, store.liftedStore.instance);
    if (!payload) return true;

    // Relay the message to the devTools panel
    if (tabId in panelConnections) {
      panelConnections[tabId].postMessage(request);
    }

    // Notify when errors occur in the app
    syncOptions.get(options => {
      if (!options.notifyErrors) return;
      const error = payload.computedStates[payload.currentStateIndex].error;
      if (error === 'Interrupted by an error up the chain') return;
      if (error) {
        chrome.notifications.create('redux-error', {
          type: 'basic',
          title: 'An error occurred in the reducer',
          message: error,
          iconUrl: 'img/logo/48x48.png',
          isClickable: true
        });
        if (typeof store.id === 'number') {
          chrome.pageAction.setIcon({tabId: store.id, path: 'img/logo/error.png'});
          catchedErrors.tab = store.id;
        }
      } else if (catchedErrors.last && typeof store.id === 'number' && catchedErrors.tab === store.id) {
        chrome.pageAction.setIcon({tabId: store.id, path: 'img/logo/38x38.png'});
      }
      catchedErrors.last = error;
    });
  }
  return true;
}

onMessage(messaging);

chrome.notifications.onClicked.addListener(id => {
  chrome.notifications.clear(id);
  if (id === 'redux-error') openDevToolsWindow('devtools-right');
});

export function toContentScript(action) {
  const message = { type: 'DISPATCH', action: action };
  let id = store.liftedStore.instance;
  if (!id || id === 'auto') id = store.id;
  if (id in panelConnections) {
    panelConnections[id].postMessage(message);
  } else {
    tabConnections[id].postMessage(message);
  }
}
<|endoftext|>


<commit_before>import { _ } from 'meteor/underscore';
import { Meteor } from 'meteor/meteor';

import { DDP } from '../common/namespace.js';
import { toWebsocketUrl } from '../common/urlHelpers.js';
import StreamClientCommon from '../common/stream_client_common.js';

// @param endpoint {String} URL to Meteor app
//   "http://subdomain.meteor.com/" or "/" or
//   "ddp+sockjs://foo-**.meteor.com/sockjs"
//
// We do some rewriting of the URL to eventually make it "ws://" or "wss://",
// whatever was passed in.  At the very least, what Meteor.absoluteUrl() returns
// us should work.
//
// We don't do any heartbeating. (The logic that did this in sockjs was removed,
// because it used a built-in sockjs mechanism. We could do it with WebSocket
// ping frames or with DDP-level messages.)
export default class ClientStream extends StreamClientCommon {
  constructor(endpoint, options) {
    super();

    options = options || {};

    this.options = Object.assign(
      {
        retry: true
      },
      options
    );

    this.client = null; // created in _launchConnection
    this.endpoint = endpoint;

    this.headers = this.options.headers || {};
    this.npmFayeOptions = this.options.npmFayeOptions || {};

    this._initCommon(this.options);

    //// Kickoff!
    this._launchConnection();
  }

  // data is a utf8 string. Data sent while not connected is dropped on
  // the floor, and it is up the user of this API to retransmit lost
  // messages on 'reset'
  send(data) {
    if (this.currentStatus.connected) {
      this.client.send(data);
    }
  }

  // Changes where this connection points
  _changeUrl(url) {
    this.endpoint = url;
  }

  _onConnect(client) {
    if (client !== this.client) {
      // This connection is not from the last call to _launchConnection.
      // But _launchConnection calls _cleanup which closes previous connections.
      // It's our belief that this stifles future 'open' events, but maybe
      // we are wrong?
      throw new Error('Got open from inactive client ' + !!this.client);
    }

    if (this._forcedToDisconnect) {
      // We were asked to disconnect between trying to open the connection and
      // actually opening it. Let's just pretend this never happened.
      this.client.close();
      this.client = null;
      return;
    }

    if (this.currentStatus.connected) {
      // We already have a connection. It must have been the case that we
      // started two parallel connection attempts (because we wanted to
      // 'reconnect now' on a hanging connection and we had no way to cancel the
      // connection attempt.) But this shouldn't happen (similarly to the client
      // !== this.client check above).
      throw new Error('Two parallel connections?');
    }

    this._clearConnectionTimer();

    // update status
    this.currentStatus.status = 'connected';
    this.currentStatus.connected = true;
    this.currentStatus.retryCount = 0;
    this.statusChanged();

    // fire resets. This must come after status change so that clients
    // can call send from within a reset callback.
    this.forEachCallback('reset', callback => {
      callback();
    });
  }

  _cleanup(maybeError) {
    this._clearConnectionTimer();
    if (this.client) {
      var client = this.client;
      this.client = null;
      client.close();

      this.forEachCallback('disconnect', callback => {
        callback(maybeError);
      });
    }
  }

  _clearConnectionTimer() {
    if (this.connectionTimer) {
      clearTimeout(this.connectionTimer);
      this.connectionTimer = null;
    }
  }

  _getProxyUrl(targetUrl) {
    // Similar to code in tools/http-helpers.js.
    var proxy = process.env.HTTP_PROXY || process.env.http_proxy || null;
    // if we're going to a secure url, try the https_proxy env variable first.
    if (targetUrl.match(/^wss:/)) {
      proxy = process.env.HTTPS_PROXY || process.env.https_proxy || proxy;
    }
    return proxy;
  }

  _launchConnection() {
    this._cleanup(); // cleanup the old socket, if there was one.

    // Since server-to-server DDP is still an experimental feature, we only
    // require the module if we actually create a server-to-server
    // connection.
    var FayeWebSocket = Npm.require('faye-websocket');
    var deflate = Npm.require('permessage-deflate');

    var targetUrl = toWebsocketUrl(this.endpoint);
    var fayeOptions = {
      headers: this.headers,
      extensions: [deflate]
    };
    fayeOptions = Object.assign(fayeOptions, this.npmFayeOptions);
    var proxyUrl = this._getProxyUrl(targetUrl);
    if (proxyUrl) {
      fayeOptions.proxy = { origin: proxyUrl };
    }

    // We would like to specify 'ddp' as the subprotocol here. The npm module we
    // used to use as a client would fail the handshake if we ask for a
    // subprotocol and the server doesn't send one back (and sockjs doesn't).
    // Faye doesn't have that behavior; it's unclear from reading RFC 6455 if
    // Faye is erroneous or not.  So for now, we don't specify protocols.
    var subprotocols = [];

    var client = (this.client = new FayeWebSocket.Client(
      targetUrl,
      subprotocols,
      fayeOptions
    ));

    this._clearConnectionTimer();
    this.connectionTimer = Meteor.setTimeout(() => {
      this._lostConnection(new DDP.ConnectionError('DDP connection timed out'));
    }, this.CONNECT_TIMEOUT);

    this.client.on(
      'open',
      Meteor.bindEnvironment(() => {
        return this._onConnect(client);
      }, 'stream connect callback')
    );

    var clientOnIfCurrent = (event, description, f) => {
      this.client.on(
        event,
        Meteor.bindEnvironment((...args) => {
          // Ignore events from any connection we've already cleaned up.
          if (client !== this.client) return;
          f.apply(this, args);
        }, description)
      );
    };

    clientOnIfCurrent('error', 'stream error callback', error => {
      if (!this.options._dontPrintErrors)
        Meteor._debug('stream error', error.message);

      // Faye's 'error' object is not a JS error (and among other things,
      // doesn't stringify well). Convert it to one.
      this._lostConnection(new DDP.ConnectionError(error.message));
    });

    clientOnIfCurrent('close', 'stream close callback', () => {
      this._lostConnection();
    });

    clientOnIfCurrent('message', 'stream message callback', message => {
      // Ignore binary frames, where message.data is a Buffer
      if (typeof message.data !== 'string') return;

      this.forEachCallback('message', callback => {
        callback(message.data);
      });
    });
  }
}
<commit_msg>Apply is confusing, we don't actually use `this`<commit_after>import { _ } from 'meteor/underscore';
import { Meteor } from 'meteor/meteor';

import { DDP } from '../common/namespace.js';
import { toWebsocketUrl } from '../common/urlHelpers.js';
import StreamClientCommon from '../common/stream_client_common.js';

// @param endpoint {String} URL to Meteor app
//   "http://subdomain.meteor.com/" or "/" or
//   "ddp+sockjs://foo-**.meteor.com/sockjs"
//
// We do some rewriting of the URL to eventually make it "ws://" or "wss://",
// whatever was passed in.  At the very least, what Meteor.absoluteUrl() returns
// us should work.
//
// We don't do any heartbeating. (The logic that did this in sockjs was removed,
// because it used a built-in sockjs mechanism. We could do it with WebSocket
// ping frames or with DDP-level messages.)
export default class ClientStream extends StreamClientCommon {
  constructor(endpoint, options) {
    super();

    options = options || {};

    this.options = Object.assign(
      {
        retry: true
      },
      options
    );

    this.client = null; // created in _launchConnection
    this.endpoint = endpoint;

    this.headers = this.options.headers || {};
    this.npmFayeOptions = this.options.npmFayeOptions || {};

    this._initCommon(this.options);

    //// Kickoff!
    this._launchConnection();
  }

  // data is a utf8 string. Data sent while not connected is dropped on
  // the floor, and it is up the user of this API to retransmit lost
  // messages on 'reset'
  send(data) {
    if (this.currentStatus.connected) {
      this.client.send(data);
    }
  }

  // Changes where this connection points
  _changeUrl(url) {
    this.endpoint = url;
  }

  _onConnect(client) {
    if (client !== this.client) {
      // This connection is not from the last call to _launchConnection.
      // But _launchConnection calls _cleanup which closes previous connections.
      // It's our belief that this stifles future 'open' events, but maybe
      // we are wrong?
      throw new Error('Got open from inactive client ' + !!this.client);
    }

    if (this._forcedToDisconnect) {
      // We were asked to disconnect between trying to open the connection and
      // actually opening it. Let's just pretend this never happened.
      this.client.close();
      this.client = null;
      return;
    }

    if (this.currentStatus.connected) {
      // We already have a connection. It must have been the case that we
      // started two parallel connection attempts (because we wanted to
      // 'reconnect now' on a hanging connection and we had no way to cancel the
      // connection attempt.) But this shouldn't happen (similarly to the client
      // !== this.client check above).
      throw new Error('Two parallel connections?');
    }

    this._clearConnectionTimer();

    // update status
    this.currentStatus.status = 'connected';
    this.currentStatus.connected = true;
    this.currentStatus.retryCount = 0;
    this.statusChanged();

    // fire resets. This must come after status change so that clients
    // can call send from within a reset callback.
    this.forEachCallback('reset', callback => {
      callback();
    });
  }

  _cleanup(maybeError) {
    this._clearConnectionTimer();
    if (this.client) {
      var client = this.client;
      this.client = null;
      client.close();

      this.forEachCallback('disconnect', callback => {
        callback(maybeError);
      });
    }
  }

  _clearConnectionTimer() {
    if (this.connectionTimer) {
      clearTimeout(this.connectionTimer);
      this.connectionTimer = null;
    }
  }

  _getProxyUrl(targetUrl) {
    // Similar to code in tools/http-helpers.js.
    var proxy = process.env.HTTP_PROXY || process.env.http_proxy || null;
    // if we're going to a secure url, try the https_proxy env variable first.
    if (targetUrl.match(/^wss:/)) {
      proxy = process.env.HTTPS_PROXY || process.env.https_proxy || proxy;
    }
    return proxy;
  }

  _launchConnection() {
    this._cleanup(); // cleanup the old socket, if there was one.

    // Since server-to-server DDP is still an experimental feature, we only
    // require the module if we actually create a server-to-server
    // connection.
    var FayeWebSocket = Npm.require('faye-websocket');
    var deflate = Npm.require('permessage-deflate');

    var targetUrl = toWebsocketUrl(this.endpoint);
    var fayeOptions = {
      headers: this.headers,
      extensions: [deflate]
    };
    fayeOptions = Object.assign(fayeOptions, this.npmFayeOptions);
    var proxyUrl = this._getProxyUrl(targetUrl);
    if (proxyUrl) {
      fayeOptions.proxy = { origin: proxyUrl };
    }

    // We would like to specify 'ddp' as the subprotocol here. The npm module we
    // used to use as a client would fail the handshake if we ask for a
    // subprotocol and the server doesn't send one back (and sockjs doesn't).
    // Faye doesn't have that behavior; it's unclear from reading RFC 6455 if
    // Faye is erroneous or not.  So for now, we don't specify protocols.
    var subprotocols = [];

    var client = (this.client = new FayeWebSocket.Client(
      targetUrl,
      subprotocols,
      fayeOptions
    ));

    this._clearConnectionTimer();
    this.connectionTimer = Meteor.setTimeout(() => {
      this._lostConnection(new DDP.ConnectionError('DDP connection timed out'));
    }, this.CONNECT_TIMEOUT);

    this.client.on(
      'open',
      Meteor.bindEnvironment(() => {
        return this._onConnect(client);
      }, 'stream connect callback')
    );

    var clientOnIfCurrent = (event, description, callback) => {
      this.client.on(
        event,
        Meteor.bindEnvironment((...args) => {
          // Ignore events from any connection we've already cleaned up.
          if (client !== this.client) return;
          callback(...args);
        }, description)
      );
    };

    clientOnIfCurrent('error', 'stream error callback', error => {
      if (!this.options._dontPrintErrors)
        Meteor._debug('stream error', error.message);

      // Faye's 'error' object is not a JS error (and among other things,
      // doesn't stringify well). Convert it to one.
      this._lostConnection(new DDP.ConnectionError(error.message));
    });

    clientOnIfCurrent('close', 'stream close callback', () => {
      this._lostConnection();
    });

    clientOnIfCurrent('message', 'stream message callback', message => {
      // Ignore binary frames, where message.data is a Buffer
      if (typeof message.data !== 'string') return;

      this.forEachCallback('message', callback => {
        callback(message.data);
      });
    });
  }
}
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2013, Joyent, Inc. All rights reserved.
 *
 * FWAPI: workflow shared functions
 */

// These must match the names available in the workflow VM:
var restify = require('restify');
var sdcClients = require('sdc-clients');

// --- Globals

// Make jslint happy:
var fwapiUrl;
var vmapiUrl;

// --- Exports

/**
 * Filters the FWAPI server list so that it doesn't include the server UUID of
 * the VM being updated
 *
 * Inputs:
 *
 * - job.params {Object} :
 *   - fwapiServers {Object} : server UUIDs from the FWAPI resolve endpoint
 *
 * Outputs:
 *
 * Once function is complete, the following will be stored in job.params:
 * - fwapiServers {Object} : filtered list of server UUIDs
 */
function filterServers(job, cb) {
  if (!job.params.fwapiServers || job.params.fwapiServers.length === 0) {
    return cb(null, 'No FWAPI servers: not filtering');
  }

  if (!job.params.server_uuid) {
    return cb(null, 'No server_uuid in params: not filtering');
  }

  job.params.fwapiServers = job.params.fwapiServers.filter(function (u) {
    return (u !== job.params.server_uuid);
  });

  return cb(null, 'Removed server_uuid ' + job.params.server_uuid
    + ' from FWAPI server list');
}

/**
 * Get VMs from VMAPI
 *
 * Inputs:
 *
 * - job.params {Object} : must specify at least one of tags, vms, or all vms:
 *   - fwapiTags {Array} : tag names to search for
 *   - fwapiVMs {Array} : VM UUIDs to search for
 *   - fwapiAllVMs {Bool} : set to true if all VMs are wanted
 *   - fwapiRuleOwner {UUID} : owner of the VMs to filter by
 *
 * Outputs:
 *
 * Once function is complete, the following will be stored in job.params:
 * - fwapiMatchingVMs {Object} : mapping of machines to IP addresses
 * - fwapiServers {Object} : server UUIDs that contain the matching VMs
 * - serverUUIDs {Object} : server UUIDs for cnapi.sysinfoList() to obtain
 *   sysinfo objects for
 */
function getVMs(job, callback) {
  if (!job.params.hasOwnProperty('fwapiTags') &&
    !job.params.hasOwnProperty('fwapiVMs') &&
    !job.params.fwapiAllVMs) {
    return callback(null, 'No tags or VMs to get');
  }

  var t;
  var tags = job.params.fwapiTags || {};
  var vms = job.params.fwapiVMs || [];
  if (tags.length === 0 && vms.length === 0 && !job.params.fwapiAllVMs) {
    return callback(null, 'No tags or VMs to get');
  }

  var left = {
    tags: {},
    vms:  vms.reduce(function (acc, vm) { acc[vm] = 1; return acc; }, {})
  };

  for (t in tags) {
    left.tags[t] = tags[t];
  }

  var filter = [];

  if (!job.params.fwapiAllVMs) {
    for (t in tags) {
      filter.push('(tags=*' + t + '='
        + (tags[t] !== true ? tags[t] : '') + '*)');
    }

    vms.forEach(function (vm) {
      filter.push('(uuid=' + vm + ')');
    });

    if (filter.length > 1) {
      filter = ['(|'].concat(filter).concat(')');
    }
  }

  // Don't get data for destroyed or failed VMs
  if (filter.length !== 0 || job.params.fwapiRuleOwner) {
    filter.unshift('(&');
    filter.push('(!(state=destroyed))');
    filter.push('(!(state=failed))');
  }

  if (job.params.fwapiRuleOwner) {
    filter.push('(owner_uuid=' + job.params.fwapiRuleOwner + ')');
  }

  if (filter.length !== 0) {
    filter.push(')');
  }

  var filterTxt = filter.join('');
  job.log.debug('listing VMs: %s', filterTxt);

  var vmapi = new sdcClients.VMAPI({ url: vmapiUrl });
  return vmapi.listVms({ query: filterTxt }, function (err, vmList) {
    if (err) {
      err.message = 'VMAPI list VMs: ' + err.message;
      return callback(err);
    }

    var remoteVMs = [];
    var servers = {};

    if (job.params.task && job.params.task === 'provision') {
      // Remove the old entry from VMAPI if it exists - it will be
      // incomplete since provisioning hasn't finished
      vmList = vmList.filter(function (vm) {
        return (vm.hasOwnProperty('uuid') && vm.uuid != job.params.vm_uuid);
      });

      vmList.push({
        firewall_enabled: job.params.firewall_enabled,
        nics: job.params.nics,
        owner_uuid: job.params.owner_uuid,
        server_uuid: job.params.server_uuid,
        tags: job.params.tags || {},
        uuid: job.params.vm_uuid
      });
    }

    vmList.forEach(function (vm) {
      var rvm = {
        enabled: vm.firewall_enabled ? true : false,
        ips: vm.nics.map(function (n) { return n.ip; }),
        owner_uuid: vm.owner_uuid,
        server_uuid: vm.server_uuid,
        tags: {},
        uuid: vm.uuid
      };

      for (var k in vm.tags) {
        rvm.tags[k] = vm.tags[k];
      }

      remoteVMs.push(rvm);
      servers[vm.server_uuid] = 1;

      for (var tag in vm.tags) {
        delete left.tags[tag];
      }

      delete left.vms[vm.uuid];
    });

    job.log.info({ vms: remoteVMs, filter: filterTxt },
      'Found %d remote VMs', remoteVMs.length);

    var s;
    var errs = [];
    var vmsLeft = Object.keys(left.vms);
    var tagsLeft = Object.keys(left.tags);

    // Allow unknown tags - the rules could have been created in advance
    // with the intention of adding VMs with those tags later
    if (tagsLeft.length !== 0) {
      s = tagsLeft.length === 0 ? '' : 's';
      job.log.info('No VMs with tag%s: %s', s, tagsLeft.join(', '));
    }

    if (vmsLeft.length !== 0) {
      s = vmsLeft.length === 0 ? '' : 's';
      errs.push(new Error('Unknown VM' + s + ': '
        + vmsLeft.join(', ')));
    }

    if (errs.length !== 0) {
      return callback(new Error(errs.map(function (e) {
        return e.message;
      }).join('\n')));
    }

    job.params.fwapiMatchingVMs = remoteVMs;
    job.params.fwapiServers = Object.keys(servers).filter(function (uuid) {
        return (uuid !== 'null');
    });
    if (job.params.fwapiServers.length !== 0) {
      job.params.serverUUIDs = job.params.fwapiServers;
    }

    job.log.info({ matchingVMs: remoteVMs, servers: job.params.fwapiServers },
      'firewall VM data retrieved');
    return callback(null, 'firewall VM data retrieved');
  });
}

/**
 * Populates firewall data for provisioning
 *
 * Inputs:
 *
 * - job.params {Object} : containing the following data from the FWAPI
 *   resolve endpoint:
 *   - fwapiResolveData {Object} : firewall rules
 *   - fwapiMatchingVMs {Object} : UUIDs of matching VMs
 *   - fwapiServers {Object} : UUIDs of servers containing the matching VMs
 *
 * Outputs:
 *
 * Once function is complete, the following will be stored in job.params:
 * - firewall {Object} : firewall payload suitable for passing to vmadm
 */
function populateFirewallData(job, cb) {
  if (!job.params.fwapiResolveData) {
    return cb(null, 'No firewall data to populate');
  }

  var resolved = job.params.fwapiResolveData;
  var firewall = {};
  var haveData = false;
  var matchingVMs = job.params.fwapiMatchingVMs || [];
  var msg;
  var server_uuid = job.params.server_uuid;

  if (resolved.rules && resolved.rules.length !== 0) {
    firewall.rules = resolved.rules;
    haveData = true;
  }

  if (matchingVMs.length !== 0) {
    var remoteVMs = matchingVMs.filter(function (rvm) {
      return (rvm.server_uuid != server_uuid);
    });

    if (remoteVMs.length !== 0) {
      firewall.remoteVMs = remoteVMs;
      haveData = true;
    }
  }

  // Don't bother sending a separate provisioner message for the server
  // the VM is going to be provisioned on: it will have this data already
  if (job.params.fwapiServers && job.params.fwapiServers.length !== 0) {
    job.params.fwapiServers = job.params.fwapiServers.filter(function (u) {
      return (u !== server_uuid);
    });

    if (job.params.fwapiServers.length !== 0) {
      job.params.serverUUIDs = job.params.fwapiServers;
    }
  }

  if (haveData) {
    job.params.firewall = firewall;
    msg = 'Added firewall data to payload';
  } else {
    msg = 'No firewall data added to payload';
  }

  job.log.debug(firewall, msg);
  return cb(null, msg);
}

/**
 * Gets firewall data from FWAPI
 */
function resolveFirewallData(job, cb) {
    var fwapi = restify.createJsonClient({ url: fwapiUrl });
    var ips = [];
    var tags = {};
    var t;

    if (job.params.hasOwnProperty('nics')) {
      ips = job.params.nics.map(function (n) { return n.ip; });
    }

    if (job.params.hasOwnProperty('tags')) {
      for (t in job.params.tags) {
        tags[t] = job.params.tags[t];
      }
    }

    if (job.params.hasOwnProperty('remove_tags')) {
      for (t in job.params.remove_tags) {
        tags[t] = job.params.remove_tags[t];
      }
    }

    if (job.params.hasOwnProperty('set_tags')) {
      for (t in job.params.set_tags) {
        tags[t] = job.params.set_tags[t];
      }
    }

    if (job.params.task == 'update' && ips.length === 0 &&
      Object.keys(tags).length === 0) {
      return cb(null, 'No tags or ips updated: not retrieving firewall data');
    }

    var params = {
        ips: ips,
        owner_uuid: job.params.owner_uuid,
        tags: tags,
        vms: [ job.params.vm_uuid ]
    };

    job.log.debug({ query: params }, 'Resolving firewall data from FWAPI');
    return fwapi.post('/resolve', params, function (err, req, res, firewall) {
        if (err) {
            return cb(err);
        } else {
            job.params.fwapiResolveData = firewall;
            job.params.fwapiVMs = firewall.vms || [];
            job.params.fwapiRuleOwner = firewall.owner_uuid || [];
            job.params.fwapiTags = firewall.tags || [];
            job.params.fwapiAllVMs = firewall.allVMs || false;
            return cb(null, 'Firewall data retrieved');
        }
    });
}

module.exports = {
  filterServers: filterServers,
  getVMs: getVMs,
  populateFirewallData: populateFirewallData,
  resolveFirewallData: resolveFirewallData,
  _set: function (params) {
    fwapiUrl = params.fwapiUrl;
    vmapiUrl = params.vmapiUrl;
  }
};
<commit_msg>FWAPI-52: Do not include VMs with state "provisioning" in vmapi.get_firewall_targets<commit_after>/*
 * Copyright (c) 2013, Joyent, Inc. All rights reserved.
 *
 * FWAPI: workflow shared functions
 */

// These must match the names available in the workflow VM:
var restify = require('restify');
var sdcClients = require('sdc-clients');

// --- Globals

// Make jslint happy:
var fwapiUrl;
var vmapiUrl;

// --- Exports

/**
 * Filters the FWAPI server list so that it doesn't include the server UUID of
 * the VM being updated
 *
 * Inputs:
 *
 * - job.params {Object} :
 *   - fwapiServers {Object} : server UUIDs from the FWAPI resolve endpoint
 *
 * Outputs:
 *
 * Once function is complete, the following will be stored in job.params:
 * - fwapiServers {Object} : filtered list of server UUIDs
 */
function filterServers(job, cb) {
  if (!job.params.fwapiServers || job.params.fwapiServers.length === 0) {
    return cb(null, 'No FWAPI servers: not filtering');
  }

  if (!job.params.server_uuid) {
    return cb(null, 'No server_uuid in params: not filtering');
  }

  job.params.fwapiServers = job.params.fwapiServers.filter(function (u) {
    return (u !== job.params.server_uuid);
  });

  return cb(null, 'Removed server_uuid ' + job.params.server_uuid
    + ' from FWAPI server list');
}

/**
 * Get VMs from VMAPI
 *
 * Inputs:
 *
 * - job.params {Object} : must specify at least one of tags, vms, or all vms:
 *   - fwapiTags {Array} : tag names to search for
 *   - fwapiVMs {Array} : VM UUIDs to search for
 *   - fwapiAllVMs {Bool} : set to true if all VMs are wanted
 *   - fwapiRuleOwner {UUID} : owner of the VMs to filter by
 *
 * Outputs:
 *
 * Once function is complete, the following will be stored in job.params:
 * - fwapiMatchingVMs {Object} : mapping of machines to IP addresses
 * - fwapiServers {Object} : server UUIDs that contain the matching VMs
 * - serverUUIDs {Object} : server UUIDs for cnapi.sysinfoList() to obtain
 *   sysinfo objects for
 */
function getVMs(job, callback) {
  if (!job.params.hasOwnProperty('fwapiTags') &&
    !job.params.hasOwnProperty('fwapiVMs') &&
    !job.params.fwapiAllVMs) {
    return callback(null, 'No tags or VMs to get');
  }

  var t;
  var tags = job.params.fwapiTags || {};
  var vms = job.params.fwapiVMs || [];
  if (tags.length === 0 && vms.length === 0 && !job.params.fwapiAllVMs) {
    return callback(null, 'No tags or VMs to get');
  }

  var left = {
    tags: {},
    vms:  vms.reduce(function (acc, vm) { acc[vm] = 1; return acc; }, {})
  };

  for (t in tags) {
    left.tags[t] = tags[t];
  }

  var filter = [];

  if (!job.params.fwapiAllVMs) {
    for (t in tags) {
      filter.push('(tags=*' + t + '='
        + (tags[t] !== true ? tags[t] : '') + '*)');
    }

    vms.forEach(function (vm) {
      filter.push('(uuid=' + vm + ')');
    });

    if (filter.length > 1) {
      filter = ['(|'].concat(filter).concat(')');
    }
  }

  // Don't get data for VMs in a state where they have no IPs: destroyed,
  // failed, provisioning
  if (filter.length !== 0 || job.params.fwapiRuleOwner) {
    filter.unshift('(&');
    filter.push('(!(state=destroyed))');
    filter.push('(!(state=failed))');
    filter.push('(!(state=provisioning))');
  }

  if (job.params.fwapiRuleOwner) {
    filter.push('(owner_uuid=' + job.params.fwapiRuleOwner + ')');
  }

  if (filter.length !== 0) {
    filter.push(')');
  }

  var filterTxt = filter.join('');
  job.log.debug('listing VMs: %s', filterTxt);

  var vmapi = new sdcClients.VMAPI({ url: vmapiUrl });
  return vmapi.listVms({ query: filterTxt }, function (err, vmList) {
    if (err) {
      err.message = 'VMAPI list VMs: ' + err.message;
      return callback(err);
    }

    var remoteVMs = [];
    var servers = {};

    if (job.params.task && job.params.task === 'provision') {
      // Remove the old entry from VMAPI if it exists - it will be
      // incomplete since provisioning hasn't finished
      vmList = vmList.filter(function (vm) {
        return (vm.hasOwnProperty('uuid') && vm.uuid != job.params.vm_uuid);
      });

      vmList.push({
        firewall_enabled: job.params.firewall_enabled,
        nics: job.params.nics,
        owner_uuid: job.params.owner_uuid,
        server_uuid: job.params.server_uuid,
        tags: job.params.tags || {},
        uuid: job.params.vm_uuid
      });
    }

    vmList.forEach(function (vm) {
      if (!vm.hasOwnProperty('nics') || vm.nics.length === 0) {
        job.log.info(vm, 'No nics in vm: not adding to remote VMs');
        return;
      }

      var rvm = {
        enabled: vm.firewall_enabled ? true : false,
        ips: vm.nics.map(function (n) { return n.ip; }),
        owner_uuid: vm.owner_uuid,
        server_uuid: vm.server_uuid,
        tags: {},
        uuid: vm.uuid
      };

      for (var k in vm.tags) {
        rvm.tags[k] = vm.tags[k];
      }

      remoteVMs.push(rvm);
      servers[vm.server_uuid] = 1;

      for (var tag in vm.tags) {
        delete left.tags[tag];
      }

      delete left.vms[vm.uuid];
    });

    job.log.info({ vms: remoteVMs, filter: filterTxt },
      'Found %d remote VMs', remoteVMs.length);

    var s;
    var errs = [];
    var vmsLeft = Object.keys(left.vms);
    var tagsLeft = Object.keys(left.tags);

    // Allow unknown tags - the rules could have been created in advance
    // with the intention of adding VMs with those tags later
    if (tagsLeft.length !== 0) {
      s = tagsLeft.length === 0 ? '' : 's';
      job.log.info('No VMs with tag%s: %s', s, tagsLeft.join(', '));
    }

    if (vmsLeft.length !== 0) {
      s = vmsLeft.length === 0 ? '' : 's';
      errs.push(new Error('Unknown VM' + s + ': '
        + vmsLeft.join(', ')));
    }

    if (errs.length !== 0) {
      return callback(new Error(errs.map(function (e) {
        return e.message;
      }).join('\n')));
    }

    job.params.fwapiMatchingVMs = remoteVMs;
    job.params.fwapiServers = Object.keys(servers).filter(function (uuid) {
        return (uuid !== 'null');
    });
    if (job.params.fwapiServers.length !== 0) {
      job.params.serverUUIDs = job.params.fwapiServers;
    }

    job.log.info({ matchingVMs: remoteVMs, servers: job.params.fwapiServers },
      'firewall VM data retrieved');
    return callback(null, 'firewall VM data retrieved');
  });
}

/**
 * Populates firewall data for provisioning
 *
 * Inputs:
 *
 * - job.params {Object} : containing the following data from the FWAPI
 *   resolve endpoint:
 *   - fwapiResolveData {Object} : firewall rules
 *   - fwapiMatchingVMs {Object} : UUIDs of matching VMs
 *   - fwapiServers {Object} : UUIDs of servers containing the matching VMs
 *
 * Outputs:
 *
 * Once function is complete, the following will be stored in job.params:
 * - firewall {Object} : firewall payload suitable for passing to vmadm
 */
function populateFirewallData(job, cb) {
  if (!job.params.fwapiResolveData) {
    return cb(null, 'No firewall data to populate');
  }

  var resolved = job.params.fwapiResolveData;
  var firewall = {};
  var haveData = false;
  var matchingVMs = job.params.fwapiMatchingVMs || [];
  var msg;
  var server_uuid = job.params.server_uuid;

  if (resolved.rules && resolved.rules.length !== 0) {
    firewall.rules = resolved.rules;
    haveData = true;
  }

  if (matchingVMs.length !== 0) {
    var remoteVMs = matchingVMs.filter(function (rvm) {
      return (rvm.server_uuid != server_uuid);
    });

    if (remoteVMs.length !== 0) {
      firewall.remoteVMs = remoteVMs;
      haveData = true;
    }
  }

  // Don't bother sending a separate provisioner message for the server
  // the VM is going to be provisioned on: it will have this data already
  if (job.params.fwapiServers && job.params.fwapiServers.length !== 0) {
    job.params.fwapiServers = job.params.fwapiServers.filter(function (u) {
      return (u !== server_uuid);
    });

    if (job.params.fwapiServers.length !== 0) {
      job.params.serverUUIDs = job.params.fwapiServers;
    }
  }

  if (haveData) {
    job.params.firewall = firewall;
    msg = 'Added firewall data to payload';
  } else {
    msg = 'No firewall data added to payload';
  }

  job.log.debug(firewall, msg);
  return cb(null, msg);
}

/**
 * Gets firewall data from FWAPI
 */
function resolveFirewallData(job, cb) {
    var fwapi = restify.createJsonClient({ url: fwapiUrl });
    var ips = [];
    var tags = {};
    var t;

    if (job.params.hasOwnProperty('nics')) {
      ips = job.params.nics.map(function (n) { return n.ip; });
    }

    if (job.params.hasOwnProperty('tags')) {
      for (t in job.params.tags) {
        tags[t] = job.params.tags[t];
      }
    }

    if (job.params.hasOwnProperty('remove_tags')) {
      for (t in job.params.remove_tags) {
        tags[t] = job.params.remove_tags[t];
      }
    }

    if (job.params.hasOwnProperty('set_tags')) {
      for (t in job.params.set_tags) {
        tags[t] = job.params.set_tags[t];
      }
    }

    if (job.params.task == 'update' && ips.length === 0 &&
      Object.keys(tags).length === 0) {
      return cb(null, 'No tags or ips updated: not retrieving firewall data');
    }

    var params = {
        ips: ips,
        owner_uuid: job.params.owner_uuid,
        tags: tags,
        vms: [ job.params.vm_uuid ]
    };

    job.log.debug({ query: params }, 'Resolving firewall data from FWAPI');
    return fwapi.post('/resolve', params, function (err, req, res, firewall) {
        if (err) {
            return cb(err);
        } else {
            job.params.fwapiResolveData = firewall;
            job.params.fwapiVMs = firewall.vms || [];
            job.params.fwapiRuleOwner = firewall.owner_uuid || [];
            job.params.fwapiTags = firewall.tags || [];
            job.params.fwapiAllVMs = firewall.allVMs || false;
            return cb(null, 'Firewall data retrieved');
        }
    });
}

module.exports = {
  filterServers: filterServers,
  getVMs: getVMs,
  populateFirewallData: populateFirewallData,
  resolveFirewallData: resolveFirewallData,
  _set: function (params) {
    fwapiUrl = params.fwapiUrl;
    vmapiUrl = params.vmapiUrl;
  }
};
<|endoftext|>