From 2472f40d68dc0df42c5d6d6c0fd534daa8eef5d0 Mon Sep 17 00:00:00 2001 From: Marcello Urbani Date: Mon, 10 Dec 2018 08:56:38 +0000 Subject: [PATCH] Lock on change, fixes, refactor --- .vscode/settings.json | 25 +-- README.md | 11 +- images/abapfs_icon.png | Bin 0 -> 20927 bytes package-lock.json | 7 +- package.json | 9 +- src/adt/AdtConnection.ts | 31 +++- src/adt/AdtExceptions.ts | 8 +- src/adt/AdtServer.ts | 53 ++++-- src/adt/AdtTransports.ts | 6 +- src/{ => adt}/abap/AbapClass.ts | 29 +--- src/{ => adt}/abap/AbapClassInclude.ts | 6 +- src/{ => adt}/abap/AbapInclude.ts | 0 src/{ => adt}/abap/AbapObject.ts | 61 ++----- src/{ => adt}/abap/AbapObjectUtilities.ts | 6 +- src/{ => adt}/abap/AbapProgram.ts | 2 +- src/{ => adt}/abap/JSONToAbapXml.ts | 0 .../{ => operations}/AdtObjectActivator.ts | 19 ++- .../AdtObjectCreator.ts | 10 +- src/adt/{ => operations}/AdtObjectFinder.ts | 18 +- .../{create => operations}/AdtObjectTypes.ts | 2 +- src/adt/operations/LockManager.ts | 160 ++++++++++++++++++ src/adt/{ => parsers}/AdtLockParser.ts | 4 +- src/adt/{ => parsers}/AdtNodeStructParser.ts | 6 +- src/adt/{ => parsers}/AdtObjectParser.ts | 0 src/adt/{ => parsers}/AdtParserBase.ts | 5 +- src/commands.ts | 5 + src/extension.ts | 61 ++++--- src/fs/AbapNode.ts | 4 +- src/listeners.ts | 47 ++++- src/logger.ts | 7 + 30 files changed, 429 insertions(+), 173 deletions(-) create mode 100644 images/abapfs_icon.png rename src/{ => adt}/abap/AbapClass.ts (78%) rename src/{ => adt}/abap/AbapClassInclude.ts (92%) rename src/{ => adt}/abap/AbapInclude.ts (100%) rename src/{ => adt}/abap/AbapObject.ts (81%) rename src/{ => adt}/abap/AbapObjectUtilities.ts (94%) rename src/{ => adt}/abap/AbapProgram.ts (92%) rename src/{ => adt}/abap/JSONToAbapXml.ts (100%) rename src/adt/{ => operations}/AdtObjectActivator.ts (89%) rename src/adt/{create => operations}/AdtObjectCreator.ts (95%) rename src/adt/{ => operations}/AdtObjectFinder.ts (93%) rename src/adt/{create => operations}/AdtObjectTypes.ts (99%) create mode 100644 src/adt/operations/LockManager.ts rename src/adt/{ => parsers}/AdtLockParser.ts (73%) rename src/adt/{ => parsers}/AdtNodeStructParser.ts (91%) rename src/adt/{ => parsers}/AdtObjectParser.ts (100%) rename src/adt/{ => parsers}/AdtParserBase.ts (96%) create mode 100644 src/logger.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index d1e3f07..9d7c9df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,14 +1,15 @@ // Place your settings in this file to overwrite default and user settings. { - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off", - "javascript.format.enable": false, - "typescript.locale": "en", - "gitlens.codeLens.enabled": false -} \ No newline at end of file + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off", + "javascript.format.enable": false, + "typescript.locale": "en", + "gitlens.codeLens.enabled": false, + "editor.fontLigatures": true +} diff --git a/README.md b/README.md index 6cd6845..ea84d0e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # ABAP remote filesystem for visual studio code -This extension allows editing and activation of ABAP code on your server directly in Visual studio code. -Doesn't allow to create objects yet, but does read/save/activate several object types +This extension allows editing and activation of ABAP code on your server directly in Visual studio code, including transport assignment and creation (if your system supports it). **Unless your system is very modern, write support will require you to install [this extension](https://github.com/marcellourbani/abapfs_extensions)** in your dev server to enable locking files -**WRITE SUPPORT IS EXPERIMANTAL USE AT YOUR OWN RISK** +**THIS SOFTWARE IS IN BETA TEST, USE AT YOUR OWN RISK** ![anim](https://user-images.githubusercontent.com/2453277/47482169-ae0cc300-d82d-11e8-8d19-f55dd877c166.gif) ![image](https://user-images.githubusercontent.com/2453277/47466602-dd99dc00-d7e9-11e8-97ed-28e23dfd8f90.png) @@ -21,16 +20,16 @@ The complete list of editable objects depends on your installation, on my local - programs/includes - function groups - classes -- transformations +- transformations (except creation) ![anim](https://user-images.githubusercontent.com/2453277/48232926-30a78d80-e3ab-11e8-8a12-00844431f9af.gif) ## setup -Too early to publish as an extension, there's a compiled extension you can run from source or install from the command line with +Will soon be published in the marketplace, in the meanwhile there's a compiled extension you can run from source or install from the command line with ```shell -code --install-extension vscode-abap-remote-fs-0.1.0.vsix +code --install-extension vscode-abap-remote-fs-0.3.0.vsix ``` The compiled file can be either downloaded from for the diff --git a/images/abapfs_icon.png b/images/abapfs_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c51f063d49ba291571d7fd50b7dfc7d9b3ede04a GIT binary patch literal 20927 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 m_Y_ZloEGDVw)|(qeEuIh{2RZ5z@zH`0000 = [] private _clone?: AdtConnection + private _stateRequestors: Set = new Set() constructor(name: string, url: string, username: string, password: string) { this.name = name @@ -44,12 +51,15 @@ export class AdtConnection { this.username, this.password ) - this._clone.stateful = false } await this._clone.connect() return this._clone } + addStateRequestor(r: StateRequestor) { + this._stateRequestors.add(r) + } + isActive(): boolean { return this._status === ConnStatus.active } @@ -138,6 +148,21 @@ export class AdtConnection { query }) } + + dropSession() { + return this.myrequest( + "/sap/bc/adt/repository/informationsystem/objecttypes", + "GET", + { + headers: { + "x-csrf-token": this._csrftoken, + "X-sap-adt-sessiontype": "", + Accept: "*/*" + } + } + ) + } + connect(): Promise { return this.myrequest( "/sap/bc/adt/repository/informationsystem/objecttypes?maxItemCount=999&name=*&data=usedByProvider" diff --git a/src/adt/AdtExceptions.ts b/src/adt/AdtExceptions.ts index 3875a61..870897b 100644 --- a/src/adt/AdtExceptions.ts +++ b/src/adt/AdtExceptions.ts @@ -1,4 +1,8 @@ -import { parsetoPromise, getFieldAttribute, recxml2js } from "./AdtParserBase" +import { + parseToPromise, + getFieldAttribute, + recxml2js +} from "./parsers/AdtParserBase" import { Response } from "request" const TYPEID = Symbol() @@ -23,7 +27,7 @@ export class AdtException extends Error { } static async fromXml(xml: string): Promise { - const raw: any = await parsetoPromise()(xml) + const raw: any = await parseToPromise()(xml) const root: any = raw["exc:exception"] const namespace = getFieldAttribute("namespace", "id", root) const type = getFieldAttribute("type", "id", root) diff --git a/src/adt/AdtServer.ts b/src/adt/AdtServer.ts index aad23f3..4458efa 100644 --- a/src/adt/AdtServer.ts +++ b/src/adt/AdtServer.ts @@ -2,14 +2,16 @@ import { AdtConnection } from "./AdtConnection" import { Uri, FileSystemError, FileType, commands } from "vscode" import { MetaFolder } from "../fs/MetaFolder" import { AbapObjectNode, AbapNode, isAbapNode } from "../fs/AbapNode" -import { AbapObject, TransportStatus, isAbapObject } from "../abap/AbapObject" +import { AbapObject, TransportStatus, isAbapObject } from "./abap/AbapObject" import { getRemoteList } from "../config" import { selectTransport } from "./AdtTransports" -import { AdtObjectActivator } from "./AdtObjectActivator" +import { AdtObjectActivator } from "./operations/AdtObjectActivator" import { pick } from "../functions" -import { AdtObjectFinder } from "./AdtObjectFinder" -import { AdtObjectCreator } from "./create/AdtObjectCreator" -import { PACKAGE } from "./create/AdtObjectTypes" +import { AdtObjectFinder } from "./operations/AdtObjectFinder" +import { AdtObjectCreator } from "./operations/AdtObjectCreator" +import { PACKAGE } from "./operations/AdtObjectTypes" +import { LockManager } from "./operations/LockManager" +import { AdtException } from "./AdtExceptions" export const ADTBASEURL = "/sap/bc/adt/repository/nodestructure" /** @@ -20,7 +22,7 @@ export const ADTBASEURL = "/sap/bc/adt/repository/nodestructure" const uriParts = (uri: Uri): string[] => uri.path .split("/") - .filter((v, idx, arr) => (idx > 0 && idx < arr.length - 1) || v) //ignore empty at begginning or end + .filter((v, idx, arr) => (idx > 0 && idx < arr.length - 1) || v) //ignore empty at beginning or end /** * centralizes most API accesses * some will be delegated/provided from members or ABAP object nodes @@ -30,7 +32,8 @@ export class AdtServer { private readonly activator: AdtObjectActivator readonly root: MetaFolder readonly objectFinder: AdtObjectFinder - creator: AdtObjectCreator + readonly creator: AdtObjectCreator + readonly lockManager: LockManager private lastRefreshed?: string /** @@ -50,6 +53,7 @@ export class AdtServer { this.creator = new AdtObjectCreator(this) this.activator = new AdtObjectActivator(this.connection) this.objectFinder = new AdtObjectFinder(this.connection) + this.lockManager = new LockManager(this.connection) this.connection .connect() .then(pick("body")) @@ -109,32 +113,40 @@ export class AdtServer { if (!isAbapNode(file)) throw FileSystemError.NoPermissions("Can only save source code") - await file.abapObject.lock(this.connection) - if (file.abapObject.transport === TransportStatus.REQUIRED) { + const obj = file.abapObject + //check file is locked + if (!this.lockManager.isLocked(obj)) + throw new AdtException( + "lockNotFound", + `Object not locked ${obj.type} ${obj.name}` + ) + + if (obj.transport === TransportStatus.REQUIRED) { const transport = await selectTransport( - file.abapObject.getContentsUri(this.connection), + obj.getContentsUri(this.connection), "", this.connection ) if (transport) file.abapObject.transport = transport } - await file.abapObject.setContents(this.connection, content) + const lockId = this.lockManager.getLockId(obj) + await obj.setContents(this.connection, content, lockId) - await file.abapObject.unlock(this.connection) await file.stat(this.connection) + await this.lockManager.unlock(obj) //might have a race condition with user changing editor... commands.executeCommand("setContext", "abapfs:objectInactive", true) } /** * converts vscode URI to ADT URI - * @see findNodeHierarcy for more details + * @see findNodeHierarchy for more details * * @param uri vscode URI */ findNode(uri: Uri): AbapNode { - return this.findNodeHierarcy(uri)[0] + return this.findNodeHierarchy(uri)[0] } /** @@ -145,7 +157,7 @@ export class AdtServer { * @abstract visual studio paths are hierarchic, adt ones aren't * so we need a way to translate the hierarchic ones to the original ones * this file is concerned with telling whether a path is a real ADT one or one from vscode - * /sap/bc/adt/repository/nodestructure (with ampty query) is the root of both + * /sap/bc/adt/repository/nodestructure (with empty query) is the root of both * also, several objects have namespaces. * Class /foo/bar of package /foo/baz in code will have a path like * /sap/bc/adt/repository/nodestructure/foo/baz/foo/bar @@ -155,7 +167,7 @@ export class AdtServer { * * @param uri VSCode URI */ - findNodeHierarcy(uri: Uri): AbapNode[] { + findNodeHierarchy(uri: Uri): AbapNode[] { const parts = uriParts(uri) return parts.reduce( (current: AbapNode[], name) => { @@ -185,7 +197,7 @@ export class AdtServer { for (const part of parts) { let next: AbapNode | undefined = node.getChild(part) if (!next && refreshable) { - //refreshable will tipically be the current node or its first abap parent (usually a package) + //refreshable will typically be the current node or its first abap parent (usually a package) await refreshable.refresh(this.connection) next = node.getChild(part) } @@ -259,3 +271,10 @@ export const fromUri = (uri: Uri) => { if (uri.scheme === "adt") return getServer(uri.authority) throw FileSystemError.FileNotFound(uri) } +export async function disconnect() { + const promises: Promise[] = [] + for (const server of servers) { + promises.push(server[1].connection.dropSession()) + } + await Promise.all(promises) +} diff --git a/src/adt/AdtTransports.ts b/src/adt/AdtTransports.ts index 1adf9b3..2491e46 100644 --- a/src/adt/AdtTransports.ts +++ b/src/adt/AdtTransports.ts @@ -1,5 +1,5 @@ -import { JSON2AbapXML } from "../abap/JSONToAbapXml" -import { parsetoPromise, getNode, recxml2js } from "./AdtParserBase" +import { JSON2AbapXML } from "./abap/JSONToAbapXml" +import { parseToPromise, getNode, recxml2js } from "./parsers/AdtParserBase" import { mapWith, flat } from "../functions" import { AdtConnection } from "./AdtConnection" import { window, Uri } from "vscode" @@ -84,7 +84,7 @@ export async function getTransportCandidates( }) } ) - const rawdata = await parsetoPromise()(response.body) + const rawdata = await parseToPromise()(response.body) const header = getNode( "asx:abap/asx:values/DATA", mapWith(recxml2js), diff --git a/src/abap/AbapClass.ts b/src/adt/abap/AbapClass.ts similarity index 78% rename from src/abap/AbapClass.ts rename to src/adt/abap/AbapClass.ts index 4c61bc8..4d27492 100644 --- a/src/abap/AbapClass.ts +++ b/src/adt/abap/AbapClass.ts @@ -3,13 +3,13 @@ import { AbapNodeComponentByCategory, AbapMetaData } from "./AbapObject" -import { NodeStructure } from "../adt/AdtNodeStructParser" -import { AdtConnection } from "../adt/AdtConnection" +import { NodeStructure } from "../parsers/AdtNodeStructParser" +import { AdtConnection } from "../AdtConnection" import { FileSystemError } from "vscode" -import { pick, followLink } from "../functions" +import { pick, followLink } from "../../functions" import { aggregateNodes } from "./AbapObjectUtilities" -import { parseClass, firstTextLink } from "../adt/AdtObjectParser" -import { parsetoPromise } from "../adt/AdtParserBase" +import { parseClass, firstTextLink } from "../parsers/AdtObjectParser" +import { parseToPromise } from "../parsers/AdtParserBase" import { ClassIncludeMeta, isClassInclude } from "./AbapClassInclude" interface ClassMetaData extends AbapMetaData { @@ -26,13 +26,14 @@ export class AbapClass extends AbapObject { ) { super(type, name, path, expandable, techName) } + async loadMetadata(connection: AdtConnection): Promise { if (this.name) { const mainUri = this.getUri(connection) const meta = await connection .request(mainUri, "GET") .then(pick("body")) - .then(parsetoPromise()) + .then(parseToPromise()) .then(parseClass) const includes = meta.includes.map(i => { const sourcePath = i.header["abapsource:sourceUri"] @@ -64,6 +65,7 @@ export class AbapClass extends AbapObject { } return this } + async getChildren( connection: AdtConnection ): Promise> { @@ -94,21 +96,6 @@ export class AbapClass extends AbapObject { else ns.nodes.push(node) } - // for (const classInc of parsed.includes) { - // const name = this.name + "." + classInc.header["class:includeType"] - // const node = { - // EXPANDABLE: "", - // OBJECT_NAME: name, - // OBJECT_TYPE: classInc.header["adtcore:type"], - // OBJECT_URI: follow(classInc.header["abapsource:sourceUri"]).path, - // OBJECT_VIT_URI: "", - // TECH_NAME: name - // } - // if (classInc.header["abapsource:sourceUri"] === "source/main") - // ns.nodes.unshift(node) - // else ns.nodes.push(node) - // } - const aggregated = aggregateNodes(ns) for (const cat of aggregated) for (const type of cat.types) diff --git a/src/abap/AbapClassInclude.ts b/src/adt/abap/AbapClassInclude.ts similarity index 92% rename from src/abap/AbapClassInclude.ts rename to src/adt/abap/AbapClassInclude.ts index 87bd8bc..af54242 100644 --- a/src/abap/AbapClassInclude.ts +++ b/src/adt/abap/AbapClassInclude.ts @@ -1,5 +1,5 @@ import { AbapObject, AbapMetaData } from "./AbapObject" -import { AdtConnection } from "../adt/AdtConnection" +import { AdtConnection } from "../AdtConnection" import { AbapClass } from "./AbapClass" import { Uri, FileSystemError } from "vscode" export interface ClassIncludeMeta extends AbapMetaData { @@ -32,6 +32,10 @@ export class AbapClassInclude extends AbapObject { return this.parent || this } + getLockTarget(): AbapObject { + return this.parent || this + } + async loadMetadata(connection: AdtConnection): Promise { if (this.parent) { await this.parent.loadMetadata(connection) diff --git a/src/abap/AbapInclude.ts b/src/adt/abap/AbapInclude.ts similarity index 100% rename from src/abap/AbapInclude.ts rename to src/adt/abap/AbapInclude.ts diff --git a/src/abap/AbapObject.ts b/src/adt/abap/AbapObject.ts similarity index 81% rename from src/abap/AbapObject.ts rename to src/adt/abap/AbapObject.ts index 6986755..7702630 100644 --- a/src/abap/AbapObject.ts +++ b/src/adt/abap/AbapObject.ts @@ -1,15 +1,14 @@ import { Uri, FileSystemError } from "vscode" -import { AdtConnection } from "../adt/AdtConnection" -import { pick, followLink } from "../functions" +import { AdtConnection } from "../AdtConnection" +import { pick, followLink } from "../../functions" import { parseNode, NodeStructure, ObjectNode -} from "../adt/AdtNodeStructParser" -import { parsetoPromise, getNode } from "../adt/AdtParserBase" -import { parseObject, firstTextLink } from "../adt/AdtObjectParser" +} from "../parsers/AdtNodeStructParser" +import { parseToPromise, getNode } from "../parsers/AdtParserBase" +import { parseObject, firstTextLink } from "../parsers/AdtObjectParser" import { aggregateNodes } from "./AbapObjectUtilities" -import { adtLockParser } from "../adt/AdtLockParser" const TYPEID = Symbol() export const XML_EXTENSION = ".XML" @@ -49,7 +48,6 @@ export class AbapObject { readonly techName: string readonly path: string readonly expandable: boolean - lockId?: string transport: TransportStatus | string = TransportStatus.UNKNOWN metaData?: AbapMetaData protected sapguiOnly: boolean @@ -84,7 +82,7 @@ export class AbapObject { return this.name.replace(/\//g, "/") + this.getExtension() } - protected getUri(connection: AdtConnection) { + getUri(connection: AdtConnection) { return Uri.parse("adt://" + connection.name).with({ path: this.path }) @@ -111,10 +109,10 @@ export class AbapObject { const response = await connection.request(uri, "POST", { body: payload }) if (response.body) { //activation error(s?) - const raw = (await parsetoPromise()(response.body)) as any + const raw = (await parseToPromise()(response.body)) as any if (raw && raw["chkl:messages"]) { - const messages = (await parsetoPromise( + const messages = (await parseToPromise( getNode("chkl:messages/msg/shortText/txt") )(response.body)) as string[] @@ -130,40 +128,17 @@ export class AbapObject { followLink(this.getUri(connection), "mainprograms"), "GET" ) - const parsed: any = await parsetoPromise()(response.body) + const parsed: any = await parseToPromise()(response.body) return parsed["adtcore:objectReferences"]["adtcore:objectReference"].map( (link: any) => link["$"] ) } - async lock(connection: AdtConnection) { - this.checkWritable() - let contentUri = this.getContentsUri(connection) - - const response = await connection.request( - contentUri.with({ query: "_action=LOCK&accessMode=MODIFY" }), - "POST", - { headers: { "X-sap-adt-sessiontype": "stateful" } } - ) - const lockRecord = await parsetoPromise(adtLockParser)(response.body) - this.lockId = lockRecord.LOCK_HANDLE - this.transport = - lockRecord.CORRNR || - (lockRecord.IS_LOCAL ? TransportStatus.LOCAL : TransportStatus.REQUIRED) - } - async unlock(connection: AdtConnection) { - this.checkWritable() - if (!this.lockId) return - let contentUri = this.getContentsUri(connection) - await connection.request( - contentUri.with({ - query: `_action=UNLOCK&lockHandle=${encodeURIComponent(this.lockId)}` - }), - "POST" - ) + getLockTarget(): AbapObject { + return this } - protected checkWritable() { + canBeWritten() { if (!this.isLeaf()) throw FileSystemError.FileIsADirectory(this.vsName) if (this.sapguiOnly) throw FileSystemError.FileNotFound( @@ -173,9 +148,10 @@ export class AbapObject { async setContents( connection: AdtConnection, - contents: Uint8Array + contents: Uint8Array, + lockId: string ): Promise { - this.checkWritable() + this.canBeWritten() let contentUri = this.getContentsUri(connection) const trselection = @@ -183,9 +159,7 @@ export class AbapObject { await connection.request( contentUri.with({ - query: `lockHandle=${encodeURIComponent( - this.lockId || "" - )}${trselection}` + query: `lockHandle=${encodeURIComponent(lockId)}${trselection}` }), "PUT", { body: contents } @@ -198,7 +172,7 @@ export class AbapObject { const meta = await connection .request(mainUri, "GET") .then(pick("body")) - .then(parsetoPromise()) + .then(parseToPromise()) .then(parseObject) const link = firstTextLink(meta.links) const sourcePath = link ? link.href : "" @@ -213,6 +187,7 @@ export class AbapObject { } return this } + getContentsUri(connection: AdtConnection): Uri { if (!this.metaData) throw FileSystemError.FileNotFound(this.path) // baseUri = baseUri.with({ path: baseUri.path.replace(/\?.*/, "") }) diff --git a/src/abap/AbapObjectUtilities.ts b/src/adt/abap/AbapObjectUtilities.ts similarity index 94% rename from src/abap/AbapObjectUtilities.ts rename to src/adt/abap/AbapObjectUtilities.ts index 9f7e166..79edfa1 100644 --- a/src/abap/AbapObjectUtilities.ts +++ b/src/adt/abap/AbapObjectUtilities.ts @@ -4,13 +4,13 @@ import { AbapSimpleObject, AbapXmlObject } from "./AbapObject" -import { NodeStructure, ObjectNode } from "../adt/AdtNodeStructParser" -import { selectMap } from "../functions" +import { NodeStructure, ObjectNode } from "../parsers/AdtNodeStructParser" +import { selectMap } from "../../functions" import { AbapProgram } from "./AbapProgram" import { AbapClass } from "./AbapClass" import { AbapInclude } from "./AbapInclude" import { AbapClassInclude } from "./AbapClassInclude" -import { AbapNode, isAbapNode } from "../fs/AbapNode" +import { AbapNode, isAbapNode } from "../../fs/AbapNode" export interface NodePath { path: string diff --git a/src/abap/AbapProgram.ts b/src/adt/abap/AbapProgram.ts similarity index 92% rename from src/abap/AbapProgram.ts rename to src/adt/abap/AbapProgram.ts index 4acd30f..c912423 100644 --- a/src/abap/AbapProgram.ts +++ b/src/adt/abap/AbapProgram.ts @@ -1,5 +1,5 @@ import { AbapObject } from "./AbapObject" -import { NodeStructure } from "../adt/AdtNodeStructParser" +import { NodeStructure } from "../parsers/AdtNodeStructParser" export class AbapProgram extends AbapObject { constructor( diff --git a/src/abap/JSONToAbapXml.ts b/src/adt/abap/JSONToAbapXml.ts similarity index 100% rename from src/abap/JSONToAbapXml.ts rename to src/adt/abap/JSONToAbapXml.ts diff --git a/src/adt/AdtObjectActivator.ts b/src/adt/operations/AdtObjectActivator.ts similarity index 89% rename from src/adt/AdtObjectActivator.ts rename to src/adt/operations/AdtObjectActivator.ts index 2fcbe47..f427410 100644 --- a/src/adt/AdtObjectActivator.ts +++ b/src/adt/operations/AdtObjectActivator.ts @@ -1,9 +1,13 @@ -import { AdtConnection } from "./AdtConnection" +import { AdtConnection } from "../AdtConnection" import { commands, window } from "vscode" import { AbapObject } from "../abap/AbapObject" -import { isAdtException } from "./AdtExceptions" -import { parsetoPromise, getNode, getFieldAttributes } from "./AdtParserBase" -import { mapWith } from "../functions" +import { isAdtException } from "../AdtExceptions" +import { + parseToPromise, + getNode, + getFieldAttributes +} from "../parsers/AdtParserBase" +import { mapWith } from "../../functions" import { isString } from "util" import { JSON2AbapXMLNode } from "../abap/JSONToAbapXml" interface InactiveComponents { @@ -63,7 +67,7 @@ export class AdtObjectActivator { }) if (response.body) { //activation error(s?) - const raw = (await parsetoPromise()(response.body)) as any + const raw = (await parseToPromise()(response.body)) as any if (raw && raw["chkl:messages"]) { const messages = getNode( @@ -73,12 +77,13 @@ export class AdtObjectActivator { return messages[0] } else if (raw && raw["ioc:inactiveObjects"]) { - return getNode( + const components = (getNode( "ioc:inactiveObjects/ioc:entry", mapWith(getNode("ioc:object/ioc:ref")), mapWith(getFieldAttributes()), raw - ) as InactiveComponents[] + ) as InactiveComponents[]).filter(x => x) + return components } } return "" diff --git a/src/adt/create/AdtObjectCreator.ts b/src/adt/operations/AdtObjectCreator.ts similarity index 95% rename from src/adt/create/AdtObjectCreator.ts rename to src/adt/operations/AdtObjectCreator.ts index b811c25..28019e4 100644 --- a/src/adt/create/AdtObjectCreator.ts +++ b/src/adt/operations/AdtObjectCreator.ts @@ -1,5 +1,5 @@ import { Uri, window } from "vscode" -import { parsetoPromise, getNode, recxml2js } from "../AdtParserBase" +import { parseToPromise, getNode, recxml2js } from "../parsers/AdtParserBase" import { mapWith } from "../../functions" import { AdtServer } from "../AdtServer" import { isAbapNode, AbapNode } from "../../fs/AbapNode" @@ -11,7 +11,7 @@ import { PACKAGE } from "./AdtObjectTypes" import { selectTransport } from "../AdtTransports" -import { abapObjectFromNode } from "../../abap/AbapObjectUtilities" +import { abapObjectFromNode } from "../abap/AbapObjectUtilities" interface ValidationMessage { SEVERITY: string @@ -39,7 +39,7 @@ export class AdtObjectCreator { "/sap/bc/adt/repository/typestructure" ) const response = await this.server.connection.request(uri, "POST") - const raw = await parsetoPromise()(response.body) + const raw = await parseToPromise()(response.body) return getNode( "asx:abap/asx:values/DATA/SEU_ADT_OBJECT_TYPE_DESCRIPTOR", mapWith(recxml2js), @@ -66,7 +66,7 @@ export class AdtObjectCreator { private getHierarchy(uri: Uri | undefined): AbapNode[] { if (uri) try { - return this.server.findNodeHierarcy(uri) + return this.server.findNodeHierarchy(uri) } catch (e) {} return [] } @@ -101,7 +101,7 @@ export class AdtObjectCreator { objType.getValidatePath(objDetails) ) const response = await this.server.connection.request(url, "POST") - const rawValidation = await parsetoPromise()(response.body) + const rawValidation = await parseToPromise()(response.body) return getNode( "asx:abap/asx:values/DATA", mapWith(recxml2js), diff --git a/src/adt/AdtObjectFinder.ts b/src/adt/operations/AdtObjectFinder.ts similarity index 93% rename from src/adt/AdtObjectFinder.ts rename to src/adt/operations/AdtObjectFinder.ts index eab3983..ba2b852 100644 --- a/src/adt/AdtObjectFinder.ts +++ b/src/adt/operations/AdtObjectFinder.ts @@ -1,20 +1,20 @@ import { - parsetoPromise, + parseToPromise, getNode, recxml2js, nodeProperties -} from "./AdtParserBase" -import { mapWith, ArrayToMap, pick, sapEscape } from "../functions" -import { AdtConnection } from "./AdtConnection" +} from "../parsers/AdtParserBase" +import { mapWith, ArrayToMap, pick, sapEscape } from "../../functions" +import { AdtConnection } from "../AdtConnection" import { window, QuickPickItem, workspace } from "vscode" import * as vscode from "vscode" -import { getServer } from "./AdtServer" +import { getServer } from "../AdtServer" import { NodePath, findObjectInNode, findMainInclude } from "../abap/AbapObjectUtilities" -import { isAbapNode } from "../fs/AbapNode" +import { isAbapNode } from "../../fs/AbapNode" interface AdtObjectType { "nameditem:name": string @@ -78,7 +78,7 @@ export class AdtObjectFinder { `operation=quickSearch&query=${query}${ot}&maxResults=51` ) const response = await conn.request(uri, "GET") - const raw = await parsetoPromise()(response.body) + const raw = await parseToPromise()(response.body) const results = getNode( "adtcore:objectReferences/adtcore:objectReference", nodeProperties, @@ -96,7 +96,7 @@ export class AdtObjectFinder { const raw = await this.conn .request(uri, "POST") .then(pick("body")) - .then(parsetoPromise()) + .then(parseToPromise()) const objectPath = getNode( "projectexplorer:nodepath/projectexplorer:objectLinkReferences/objectLinkReference", nodeProperties, @@ -200,7 +200,7 @@ export class AdtObjectFinder { return o } async setTypes(source: string) { - const parser = parsetoPromise( + const parser = parseToPromise( getNode("nameditem:namedItemList/nameditem:namedItem", mapWith(recxml2js)) ) const raw = (await parser(source)) as AdtObjectType[] diff --git a/src/adt/create/AdtObjectTypes.ts b/src/adt/operations/AdtObjectTypes.ts similarity index 99% rename from src/adt/create/AdtObjectTypes.ts rename to src/adt/operations/AdtObjectTypes.ts index 9d3ce4c..378aa0b 100644 --- a/src/adt/create/AdtObjectTypes.ts +++ b/src/adt/operations/AdtObjectTypes.ts @@ -1,6 +1,6 @@ import { sapEscape } from "../../functions" import { window, QuickPickItem } from "vscode" -import { ObjectNode } from "../AdtNodeStructParser" +import { ObjectNode } from "../parsers/AdtNodeStructParser" export const PACKAGE = "DEVC/K" diff --git a/src/adt/operations/LockManager.ts b/src/adt/operations/LockManager.ts new file mode 100644 index 0000000..8924fff --- /dev/null +++ b/src/adt/operations/LockManager.ts @@ -0,0 +1,160 @@ +import { AdtConnection, StateRequestor } from "../AdtConnection" +import { AbapObject, TransportStatus } from "../abap/AbapObject" +import { parseToPromise } from "../parsers/AdtParserBase" +import { adtLockParser } from "../parsers/AdtLockParser" + +enum LockStatuses { + LOCKED, + UNLOCKED, + LOCKING, + UNLOCKING +} + +class LockObject { + children: Set = new Set() + listeners: Array<(s: LockStatuses) => void> = [] + private _lockStatus = LockStatuses.UNLOCKED + get lockStatus() { + return this._lockStatus + } + lockId: string = "" + + constructor(public main: AbapObject) {} + + setLockStatus(status: LockStatuses, lockId: string = "") { + this._lockStatus = status + this.lockId = status === LockStatuses.LOCKED ? lockId : "" + const l = this.listeners + this.listeners = [] + l.forEach(x => x(status)) + } + + needLock(child: AbapObject) { + this.children.add(child) + return ( + this.lockStatus === LockStatuses.UNLOCKED || + this.lockStatus === LockStatuses.UNLOCKING + ) + } + + isLocked(child: AbapObject) { + return this.children.has(child) + } + needUnlock(child: AbapObject) { + this.children.delete(child) + return ( + this.children.size === 0 && + (this.lockStatus === LockStatuses.LOCKED || + this.lockStatus === LockStatuses.LOCKING) + ) + } + + waitStatusUpdate() { + const waitUpdate = new Promise(resolve => { + this.listeners.push(resolve) + }) + return waitUpdate + } +} + +export class LockManager implements StateRequestor { + l: Map = new Map() + constructor(private conn: AdtConnection) { + conn.addStateRequestor(this) + } + + private getLockObject(child: AbapObject) { + const lockSubject = child.getLockTarget() + let lockObj = this.l.get(lockSubject) + if (!lockObj) { + lockObj = new LockObject(lockSubject) + this.l.set(lockSubject, lockObj) + } + return lockObj + } + + getLockId(obj: AbapObject): string { + const lockObj = this.getLockObject(obj) + // const lockId = this.locks.get(obj) + if (lockObj.lockId) return lockObj.lockId + throw new Error(`Object ${obj.name} is not locked`) + } + + get needStateFul(): boolean { + return this.lockedObjects.length > 0 + } + + async lock(obj: AbapObject) { + if (!obj.canBeWritten) return + const lockObj = this.getLockObject(obj) + if (!lockObj.needLock(obj)) return + //if unlocking in process, wait for it to finish and then lock + // perhaps we should check the status returned... + if (lockObj.lockStatus === LockStatuses.UNLOCKING) + await lockObj.waitStatusUpdate() + + if (!lockObj.needLock(obj)) return // in case another object triggered before + + lockObj.setLockStatus(LockStatuses.LOCKING) + try { + const uri = lockObj.main + .getUri(this.conn) + .with({ query: "_action=LOCK&accessMode=MODIFY" }) + const response = await this.conn.request(uri, "POST") + const lockRecord = await parseToPromise(adtLockParser)(response.body) + lockObj.setLockStatus(LockStatuses.LOCKED, lockRecord.LOCK_HANDLE) + obj.transport = + lockRecord.CORRNR || + (lockRecord.IS_LOCAL ? TransportStatus.LOCAL : TransportStatus.REQUIRED) + console.log("locked", obj.name) + } catch (e) { + if ( + lockObj.needUnlock(obj) && + lockObj.lockStatus === LockStatuses.LOCKING + ) + lockObj.setLockStatus(LockStatuses.UNLOCKED) + throw e + } + } + + async unlock(obj: AbapObject) { + if (!obj.canBeWritten) return + const lockObj = this.getLockObject(obj) + if (!lockObj.needUnlock(obj)) return + //if locking in process, wait for it to finish and then unlock + // perhaps we should check the status returned... + if (lockObj.lockStatus === LockStatuses.LOCKING) + await lockObj.waitStatusUpdate() + + if (!lockObj.needUnlock(obj)) return // in case another object triggered before + const lockId = lockObj.lockId + + lockObj.setLockStatus(LockStatuses.UNLOCKING) + try { + const uri = obj.getUri(this.conn).with({ + query: `_action=UNLOCK&lockHandle=${encodeURIComponent(lockObj.lockId)}` + }) + await this.conn.request(uri, "POST") + lockObj.setLockStatus(LockStatuses.UNLOCKED) + console.log("unlocked", obj.name) + } catch (e) { + //unlocking failed, restore the original ID + if ( + lockObj.needLock(obj) && + lockObj.lockStatus === LockStatuses.UNLOCKING + ) + lockObj.setLockStatus(LockStatuses.LOCKED, lockId) + } + } + + isLocked(obj: AbapObject) { + const lockObj = this.getLockObject(obj) + return lockObj.isLocked(obj) + } + + get lockedObjects() { + let children: AbapObject[] = [] + this.l.forEach(x => (children = [...children, ...[...x.children]])) + return children + } +} diff --git a/src/adt/AdtLockParser.ts b/src/adt/parsers/AdtLockParser.ts similarity index 73% rename from src/adt/AdtLockParser.ts rename to src/adt/parsers/AdtLockParser.ts index 569989e..1c491bf 100644 --- a/src/adt/AdtLockParser.ts +++ b/src/adt/parsers/AdtLockParser.ts @@ -1,6 +1,6 @@ -import { defaultVal, mapWith } from "../functions" +import { defaultVal, mapWith } from "../../functions" -import { getNode, recxml2js } from "./AdtParserBase" +import { getNode, recxml2js } from "../parsers/AdtParserBase" interface AdtLock { LOCK_HANDLE: string diff --git a/src/adt/AdtNodeStructParser.ts b/src/adt/parsers/AdtNodeStructParser.ts similarity index 91% rename from src/adt/AdtNodeStructParser.ts rename to src/adt/parsers/AdtNodeStructParser.ts index fd86b0b..2449a41 100644 --- a/src/adt/AdtNodeStructParser.ts +++ b/src/adt/parsers/AdtNodeStructParser.ts @@ -1,6 +1,6 @@ -import { getNode, recxml2js, parsetoPromise } from "./AdtParserBase" +import { getNode, recxml2js, parseToPromise } from "./AdtParserBase" -import { mapWith, ArrayToMap, filterComplex, defaultVal } from "../functions" +import { mapWith, ArrayToMap, filterComplex, defaultVal } from "../../functions" import { convertableToString } from "xml2js" export interface ObjectNode { @@ -62,7 +62,7 @@ const ObjectTypeParser: (a: string) => Map = defaultVal( export const parseNode: ( rawpayload: convertableToString -) => Promise = parsetoPromise((payload: any) => { +) => Promise = parseToPromise((payload: any) => { return { nodes: treecontentParser(payload), categories: categoryNodeParser(payload), diff --git a/src/adt/AdtObjectParser.ts b/src/adt/parsers/AdtObjectParser.ts similarity index 100% rename from src/adt/AdtObjectParser.ts rename to src/adt/parsers/AdtObjectParser.ts diff --git a/src/adt/AdtParserBase.ts b/src/adt/parsers/AdtParserBase.ts similarity index 96% rename from src/adt/AdtParserBase.ts rename to src/adt/parsers/AdtParserBase.ts index 4591004..9b59afa 100644 --- a/src/adt/AdtParserBase.ts +++ b/src/adt/parsers/AdtParserBase.ts @@ -1,5 +1,6 @@ +/* cSpell:disable */ import { parseString, convertableToString } from "xml2js" -import { pipe, pick, removeNameSpace, mapWith } from "../functions" +import { pipe, pick, removeNameSpace, mapWith } from "../../functions" import { isArray } from "util" /** @@ -120,7 +121,7 @@ export const getNode = (...args: any[]) => { } return fn(...args) } -export const parsetoPromise = (parser?: (raw: any) => T) => ( +export const parseToPromise = (parser?: (raw: any) => T) => ( xml: convertableToString ): Promise => new Promise(resolve => { diff --git a/src/commands.ts b/src/commands.ts index adfe76e..d268a56 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -2,18 +2,23 @@ import { AdtConnection } from "./adt/AdtConnection" import { workspace, Uri, window } from "vscode" import { fromUri } from "./adt/AdtServer" import { selectRemote, pickAdtRoot } from "./config" +import { log } from "./logger" export async function connectAdtServer(selector: any) { const connectionID = selector && selector.connection const remote = await selectRemote(connectionID) const connection = AdtConnection.fromRemote(remote) + log(`Connecting to server ${connectionID}`) + await connection.connect() // if connection raises an exception don't mount any folder workspace.updateWorkspaceFolders(0, 0, { uri: Uri.parse("adt://" + remote.name), name: remote.name + "(ABAP)" }) + + log(`Connected to server ${connectionID}`) } export async function activateCurrent(selector: Uri) { diff --git a/src/extension.ts b/src/extension.ts index ebed4d0..68136e0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,47 +1,60 @@ -"use strict"; -import * as vscode from "vscode"; -import { FsProvider } from "./fs/FsProvider"; -import { window, commands } from "vscode"; -import { activeTextEditorChangedListener } from "./listeners"; +"use strict" +import * as vscode from "vscode" +import { FsProvider } from "./fs/FsProvider" +import { window, commands, workspace } from "vscode" +import { + activeTextEditorChangedListener, + documentChangedListener, + documentClosedListener +} from "./listeners" import { connectAdtServer, activateCurrent, searchAdtObject, createAdtObject -} from "./commands"; +} from "./commands" +import { disconnect } from "./adt/AdtServer" +import { log } from "./logger" export function activate(context: vscode.ExtensionContext) { - const abapFS = new FsProvider(); + const abapFS = new FsProvider() + const sub = context.subscriptions //register the filesystem type - context.subscriptions.push( + sub.push( vscode.workspace.registerFileSystemProvider("adt", abapFS, { isCaseSensitive: true }) - ); + ) + + //change document listener, for locking (and possibly validation in future) + sub.push(workspace.onDidChangeTextDocument(documentChangedListener)) + //closed document listener, for locking + sub.push(workspace.onDidCloseTextDocument(documentClosedListener)) - //Editor changed listener - context.subscriptions.push( - window.onDidChangeActiveTextEditor(activeTextEditorChangedListener) - ); + //Editor changed listener, updates context and icons + sub.push(window.onDidChangeActiveTextEditor(activeTextEditorChangedListener)) //connect command - let disposable = commands.registerCommand("abapfs.connect", connectAdtServer); - context.subscriptions.push(disposable); + sub.push(commands.registerCommand("abapfs.connect", connectAdtServer)) //activate command - disposable = commands.registerCommand("abapfs.activate", activateCurrent); - context.subscriptions.push(disposable); + sub.push(commands.registerCommand("abapfs.activate", activateCurrent)) //search command - context.subscriptions.push( - commands.registerCommand("abapfs.search", searchAdtObject) - ); + sub.push(commands.registerCommand("abapfs.search", searchAdtObject)) //create command - context.subscriptions.push( - commands.registerCommand("abapfs.create", createAdtObject) - ); + sub.push(commands.registerCommand("abapfs.create", createAdtObject)) + + log(`Activated,pid=${process.pid}`) } // this method is called when your extension is deactivated -export function deactivate() {} +// it's important to kill these sessions as there might be an open process on the abap side +// most commonly because of locked sources. +// Locks will not be released until either explicitly closed or the session is terminates +// an open session can leave sources locked without any UI able to release them (except SM12 and the like) +export async function deactivate() { + await disconnect() + log(`Deactivated,pid=${process.pid}`) +} diff --git a/src/fs/AbapNode.ts b/src/fs/AbapNode.ts index a89694e..52f6103 100644 --- a/src/fs/AbapNode.ts +++ b/src/fs/AbapNode.ts @@ -1,6 +1,6 @@ import { FileStat, FileType, FileSystemError } from "vscode" -import { aggregateNodes } from "../abap/AbapObjectUtilities" -import { AbapObject, AbapNodeComponentByCategory } from "../abap/AbapObject" +import { aggregateNodes } from "../adt/abap/AbapObjectUtilities" +import { AbapObject, AbapNodeComponentByCategory } from "../adt/abap/AbapObject" import { MetaFolder } from "./MetaFolder" import { AdtConnection } from "../adt/AdtConnection" import { flatMap, pick } from "../functions" diff --git a/src/listeners.ts b/src/listeners.ts index 9335b24..b3dbf1d 100644 --- a/src/listeners.ts +++ b/src/listeners.ts @@ -1,7 +1,52 @@ -import { TextEditor, commands } from "vscode" +import { + TextEditor, + commands, + window, + StatusBarAlignment, + TextDocumentChangeEvent, + TextDocument +} from "vscode" import { fromUri } from "./adt/AdtServer" +const status = window.createStatusBarItem(StatusBarAlignment.Right, 100) + +export async function documentClosedListener(doc: TextDocument) { + const uri = doc.uri + if (uri.scheme === "adt") { + const server = fromUri(uri) + const obj = await server.findAbapObject(uri) + if (server.lockManager.isLocked(obj)) await server.lockManager.unlock(obj) + } +} + +export async function documentChangedListener(event: TextDocumentChangeEvent) { + const uri = event.document.uri + if (uri.scheme === "adt") { + const server = fromUri(uri) + const obj = await server.findAbapObject(uri) + const shouldLock = event.document.isDirty + //no need to lock objects already locked + if (shouldLock !== server.lockManager.isLocked(obj)) { + if (shouldLock) { + try { + await server.lockManager.lock(obj) + } catch (e) { + window.showErrorMessage( + `Object not locked ${obj.type} ${ + obj.name + }.Won't be able to save changes` + ) + } + } else await server.lockManager.unlock(obj) + } + + status.text = `${uri.authority}:${ + server.lockManager.lockedObjects.length + } objects locked` + status.show() + } +} export async function activeTextEditorChangedListener( editor: TextEditor | undefined ) { diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..2b5c968 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,7 @@ +import { window } from "vscode" + +const channel = window.createOutputChannel("ABAPFS") + +export function log(...messages: string[]) { + channel.appendLine(messages.join("")) +}