diff options
author | Andrej Shadura <andrew.shadura@collabora.co.uk> | 2019-08-28 14:13:29 +0200 |
---|---|---|
committer | Andrej Shadura <andrew.shadura@collabora.co.uk> | 2019-08-29 17:48:13 +0200 |
commit | e19ef5983707e6a5c8d127f1ac8f02754cef82fd (patch) | |
tree | 9e3852cb9abc81ed6aa444465928d45fd7763dea /platform/script-debugger/backend/src/debugger |
New upstream version 0~183.5153.4+dfsg
Diffstat (limited to 'platform/script-debugger/backend/src/debugger')
55 files changed, 3687 insertions, 0 deletions
diff --git a/platform/script-debugger/backend/src/debugger/Breakpoint.kt b/platform/script-debugger/backend/src/debugger/Breakpoint.kt new file mode 100755 index 00000000..24de5d2b --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/Breakpoint.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +/** + * A breakpoint in the browser JavaScript virtual machine. The `set*` + * method invocations will not take effect until + * [.flush] is called. + */ +interface Breakpoint { + companion object { + /** + * This value is used when the corresponding parameter is absent + */ + const val EMPTY_VALUE: Int = -1 + + /** + * A breakpoint has this ID if it does not reflect an actual breakpoint in a + * JavaScript VM debugger. + */ + const val INVALID_ID: Int = -1 + } + + val target: BreakpointTarget + + val line: Int + + val column: Int + + /** + * @return whether this breakpoint is enabled + */ + /** + * Sets whether this breakpoint is enabled. + * Requires subsequent [.flush] call. + */ + var enabled: Boolean + + /** + * Sets the breakpoint condition as plain JavaScript (`null` to clear). + * Requires subsequent [.flush] call. + */ + var condition: String? + + val isResolved: Boolean + + /** + * Be aware! V8 doesn't provide reliable debugger API, so, sometimes actual locations is empty - in this case this methods return "true". + * V8 debugger doesn't report about resolved breakpoint if it is happened after initial breakpoint set. So, you cannot trust "actual locations". + */ + fun isActualLineCorrect(): Boolean = true +} + +/** + * Visitor interface that includes all extensions. + */ +interface TargetExtendedVisitor<R> : FunctionVisitor<R>, ScriptRegExpSupportVisitor<R> + + +/** + * Additional interface that user visitor may implement for [BreakpointTarget.accept] + * method. + */ +interface FunctionVisitor<R> : BreakpointTarget.Visitor<R> { + fun visitFunction(expression: String): R +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/BreakpointBase.kt b/platform/script-debugger/backend/src/debugger/BreakpointBase.kt new file mode 100644 index 00000000..cd995190 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/BreakpointBase.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.util.containers.ContainerUtil +import org.jetbrains.concurrency.Promise + +abstract class BreakpointBase<L : Any>(override val target: BreakpointTarget, + override var line: Int, + override val column: Int, + condition: String?, + enabled: Boolean) : Breakpoint { + val actualLocations: MutableList<L> = ContainerUtil.createLockFreeCopyOnWriteList<L>() + + /** + * Whether the breakpoint data have changed with respect + * to the JavaScript VM data + */ + @Volatile + protected var dirty: Boolean = false + + override val isResolved: Boolean + get() = !actualLocations.isEmpty() + + override var condition: String? = condition + set(value) { + if (field != value) { + field = value + dirty = true + } + } + + override var enabled: Boolean = enabled + set(value) { + if (value != field) { + field = value + dirty = true + } + } + + fun setActualLocations(value: List<L>?) { + actualLocations.clear() + if (!ContainerUtil.isEmpty(value)) { + actualLocations.addAll(value!!) + } + } + + fun setActualLocation(value: L?) { + actualLocations.clear() + if (value != null) { + actualLocations.add(value) + } + } + + abstract fun isVmRegistered(): Boolean + + override fun hashCode(): Int { + var result = line + result *= 31 + column + result *= 31 + (if (enabled) 1 else 0) + if (condition != null) { + result *= 31 + condition!!.hashCode() + } + result *= 31 + target.hashCode() + return result + } + + abstract fun flush(breakpointManager: BreakpointManager): Promise<*> +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/BreakpointManager.kt b/platform/script-debugger/backend/src/debugger/BreakpointManager.kt new file mode 100644 index 00000000..2f1e355e --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/BreakpointManager.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.util.Url +import org.jetbrains.concurrency.Promise +import java.util.* + +interface BreakpointManager { + enum class MUTE_MODE { + ALL, + ONE, + NONE + } + + val breakpoints: Iterable<Breakpoint> + + val regExpBreakpointSupported: Boolean + get() = false + + @Deprecated("use another overload") + fun setBreakpoint(target: BreakpointTarget, + line: Int, + condition: String? = null): Breakpoint { + throw UnsupportedOperationException() + } + + fun setBreakpoint(target: BreakpointTarget, + line: Int, + column: Int = Breakpoint.EMPTY_VALUE, + url: Url? = null, + condition: String? = null, + ignoreCount: Int = Breakpoint.EMPTY_VALUE): SetBreakpointResult + + fun remove(breakpoint: Breakpoint): Promise<*> + + /** + * Supports targets that refer to function text in form of function-returning + * JavaScript expression. + * E.g. you can set a breakpoint on the 5th line of user method addressed as + * 'PropertiesDialog.prototype.loadData'. + * Expression is calculated immediately and never recalculated again. + */ + val functionSupport: ((expression: String) -> BreakpointTarget)? + get() = null + + // Could be called multiple times for breakpoint + fun addBreakpointListener(listener: BreakpointListener) + + fun removeAll(): Promise<*> + + fun getMuteMode(): MUTE_MODE = BreakpointManager.MUTE_MODE.ONE + + /** + * Flushes the breakpoint parameter changes (set* methods) into the browser + * and invokes the callback once the operation has finished. This method must + * be called for the set* method invocations to take effect. + + */ + fun flush(breakpoint: Breakpoint): Promise<*> + + /** + * Asynchronously enables or disables all breakpoints on remote. 'Enabled' means that + * breakpoints behave as normal, 'disabled' means that VM doesn't stop on breakpoints. + * It doesn't update individual properties of [Breakpoint]s. Method call + * with a null value and not null callback simply returns current value. + */ + fun enableBreakpoints(enabled: Boolean): Promise<*> + + fun setBreakOnFirstStatement() + + fun isBreakOnFirstStatement(context: SuspendContext<*>): Boolean + + interface SetBreakpointResult + data class BreakpointExist(val existingBreakpoint: Breakpoint) : SetBreakpointResult + data class BreakpointCreated(val breakpoint: Breakpoint, val isResolved: Promise<out Breakpoint>) : SetBreakpointResult +} + +interface BreakpointListener : EventListener { + fun resolved(breakpoint: Breakpoint) + + fun errorOccurred(breakpoint: Breakpoint, errorMessage: String?) + + fun nonProvisionalBreakpointRemoved(breakpoint: Breakpoint) { + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/BreakpointManagerBase.kt b/platform/script-debugger/backend/src/debugger/BreakpointManagerBase.kt new file mode 100644 index 00000000..19f4749c --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/BreakpointManagerBase.kt @@ -0,0 +1,134 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.concurrency.ConcurrentCollectionFactory +import com.intellij.openapi.util.text.StringUtil +import com.intellij.util.EventDispatcher +import com.intellij.util.SmartList +import com.intellij.util.Url +import com.intellij.util.containers.ContainerUtil +import gnu.trove.TObjectHashingStrategy +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.all +import org.jetbrains.concurrency.nullPromise +import org.jetbrains.concurrency.rejectedPromise +import java.util.concurrent.ConcurrentMap + +abstract class BreakpointManagerBase<T : BreakpointBase<*>> : BreakpointManager { + override val breakpoints: MutableSet<T> = ContainerUtil.newConcurrentSet<T>() + + protected val breakpointDuplicationByTarget: ConcurrentMap<T, T> = ConcurrentCollectionFactory.createMap<T, T>(object : TObjectHashingStrategy<T> { + override fun computeHashCode(b: T): Int { + var result = b.line + result *= 31 + b.column + if (b.condition != null) { + result *= 31 + b.condition!!.hashCode() + } + result *= 31 + b.target.hashCode() + return result + } + + override fun equals(b1: T, b2: T) = + b1.target.javaClass == b2.target.javaClass && + b1.target == b2.target && + b1.line == b2.line && + b1.column == b2.column && + StringUtil.equals(b1.condition, b2.condition) + }) + + protected val dispatcher: EventDispatcher<BreakpointListener> = EventDispatcher.create(BreakpointListener::class.java) + + protected abstract fun createBreakpoint(target: BreakpointTarget, line: Int, column: Int, condition: String?, ignoreCount: Int, enabled: Boolean): T + + protected abstract fun doSetBreakpoint(target: BreakpointTarget, url: Url?, breakpoint: T): Promise<out Breakpoint> + + override fun setBreakpoint(target: BreakpointTarget, + line: Int, + column: Int, + url: Url?, + condition: String?, + ignoreCount: Int): BreakpointManager.SetBreakpointResult { + val breakpoint = createBreakpoint(target, line, column, condition, ignoreCount, true) + val existingBreakpoint = breakpointDuplicationByTarget.putIfAbsent(breakpoint, breakpoint) + if (existingBreakpoint != null) { + return BreakpointManager.BreakpointExist(existingBreakpoint) + } + + breakpoints.add(breakpoint) + val promise = doSetBreakpoint(target, url, breakpoint) + .onError { dispatcher.multicaster.errorOccurred(breakpoint, it.message ?: it.toString()) } + return BreakpointManager.BreakpointCreated(breakpoint, promise) + } + + final override fun remove(breakpoint: Breakpoint): Promise<*> { + @Suppress("UNCHECKED_CAST") + val b = breakpoint as T + val existed = breakpoints.remove(b) + if (existed) { + breakpointDuplicationByTarget.remove(b) + } + return if (!existed || !b.isVmRegistered()) nullPromise() else doClearBreakpoint(b) + } + + final override fun removeAll(): Promise<*> { + val list = breakpoints.toList() + breakpoints.clear() + breakpointDuplicationByTarget.clear() + val promises = SmartList<Promise<*>>() + for (b in list) { + if (b.isVmRegistered()) { + promises.add(doClearBreakpoint(b)) + } + } + return promises.all() + } + + protected abstract fun doClearBreakpoint(breakpoint: T): Promise<*> + + final override fun addBreakpointListener(listener: BreakpointListener) { + dispatcher.addListener(listener) + } + + protected fun notifyBreakpointResolvedListener(breakpoint: T) { + if (breakpoint.isResolved) { + dispatcher.multicaster.resolved(breakpoint) + } + } + + @Suppress("UNCHECKED_CAST") + override fun flush(breakpoint: Breakpoint): Promise<*> = (breakpoint as T).flush(this) + + override fun enableBreakpoints(enabled: Boolean): Promise<*> = rejectedPromise<Any?>("Unsupported") + + override fun setBreakOnFirstStatement() { + } + + override fun isBreakOnFirstStatement(context: SuspendContext<*>): Boolean = false +} + +// used in goland +@Suppress("unused") +class DummyBreakpointManager : BreakpointManager { + override val breakpoints: Iterable<Breakpoint> + get() = emptyList() + + override fun setBreakpoint(target: BreakpointTarget, line: Int, column: Int, url: Url?, condition: String?, ignoreCount: Int): BreakpointManager.SetBreakpointResult { + throw UnsupportedOperationException() + } + + override fun remove(breakpoint: Breakpoint): Promise<*> = nullPromise() + + override fun addBreakpointListener(listener: BreakpointListener) { + } + + override fun removeAll(): Promise<*> = nullPromise() + + override fun flush(breakpoint: Breakpoint): Promise<*> = nullPromise() + + override fun enableBreakpoints(enabled: Boolean): Promise<*> = nullPromise() + + override fun setBreakOnFirstStatement() { + } + + override fun isBreakOnFirstStatement(context: SuspendContext<*>): Boolean = false +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/BreakpointTarget.java b/platform/script-debugger/backend/src/debugger/BreakpointTarget.java new file mode 100644 index 00000000..55b952b6 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/BreakpointTarget.java @@ -0,0 +1,128 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.annotations.NotNull; + +/** + * A reference to some JavaScript text that you can set breakpoints on. The reference may + * be in form of script name, script id etc. + * This type is essentially an Algebraic Type with several cases. Additional cases are provided + * in form of optional extensions. + * + * @see ScriptName + * @see ScriptId + */ +public abstract class BreakpointTarget { + /** + * Dispatches call on the actual Target type. + * + * @param visitor user-provided {@link Visitor} that may also implement some additional + * interfaces (for extended types) that is checked on runtime + */ + public abstract <R> R accept(Visitor<R> visitor); + + public interface Visitor<R> { + R visitScriptName(String scriptName); + + R visitScript(Script script); + + R visitUnknown(BreakpointTarget target); + } + + /** + * A target that refers to a script by its id + */ + public static final class ScriptId extends BreakpointTarget { + public final Script script; + + public ScriptId(@NotNull Script script) { + this.script = script; + } + + @Override + public <R> R accept(Visitor<R> visitor) { + return visitor.visitScript(script); + } + + @Override + public String toString() { + return script.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + return script.equals(((ScriptId)o).script); + } + + @Override + public int hashCode() { + return script.hashCode(); + } + } + + public abstract String toString(); + + /** + * A target that refers to a script by its name. Breakpoint will be set on every matching script currently loaded in VM. + */ + public static final class ScriptName extends BreakpointTarget { + private final String name; + + public ScriptName(@NotNull String name) { + this.name = name; + } + + @NotNull + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + + @Override + public <R> R accept(@NotNull Visitor<R> visitor) { + return visitor.visitScriptName(name); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + return name.equals(((ScriptName)o).name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/CallFrame.kt b/platform/script-debugger/backend/src/debugger/CallFrame.kt new file mode 100755 index 00000000..3fde012a --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/CallFrame.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise + +interface CallFrame { + /** + * @return the scopes known in this frame + */ + val variableScopes: List<Scope> + + val hasOnlyGlobalScope: Boolean + + /** + * receiver variable known in this frame ("this" variable) + * Computed variable must be null if no receiver variable + */ + val receiverVariable: Promise<Variable?> + + val line: Int + + val column: Int + + /** + * @return the name of the current function of this frame + */ + val functionName: String? + + /** + * @return context for evaluating expressions in scope of this frame + */ + val evaluateContext: EvaluateContext + + /** + * @see com.intellij.xdebugger.frame.XStackFrame.getEqualityObject + */ + val equalityObject: Any + + /** + * Name of function which scheduled some handler for top frames of async stack. + */ + val asyncFunctionName: String? + + val isFromAsyncStack: Boolean +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/CallFrameBase.kt b/platform/script-debugger/backend/src/debugger/CallFrameBase.kt new file mode 100644 index 00000000..46d73486 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/CallFrameBase.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.util.NotNullLazyValue + +const val RECEIVER_NAME: String = "this" + +@Deprecated("") +/** + * Use kotlin - base class is not required in this case (no boilerplate code) + */ +/** + * You must initialize [.scopes] or override [.getVariableScopes] + */ +abstract class CallFrameBase(override val functionName: String?, override val line: Int, override val column: Int, override val evaluateContext: EvaluateContext) : CallFrame { + protected var scopes: NotNullLazyValue<List<Scope>>? = null + + override var hasOnlyGlobalScope: Boolean = false + protected set(value: Boolean) { + field = value + } + + override val variableScopes: List<Scope> + get() = scopes!!.value + + override val asyncFunctionName: String? = null + + override val isFromAsyncStack: Boolean = false +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/DebugEventListener.java b/platform/script-debugger/backend/src/debugger/DebugEventListener.java new file mode 100755 index 00000000..6a3a5a65 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/DebugEventListener.java @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.EventListener; + +public interface DebugEventListener extends EventListener { + /** + * Reports the virtual machine has suspended (on hitting + * breakpoints or a step end). The {@code context} can be used to access the + * current backtrace. + */ + default void suspended(@NotNull SuspendContext<?> context) { + } + + /** + * Reports the virtual machine has resumed. This can happen + * asynchronously, due to a user action in the browser (without explicitly resuming the VM through + * @param vm + */ + default void resumed(@NotNull Vm vm) { + } + + /** + * Reports that a new script has been loaded. + */ + default void scriptAdded(@NotNull Vm vm, @NotNull Script script, @Nullable String sourceMapUrl) { + } + + /** + * Reports that the script has been collected and is no longer used in VM. + */ + default void scriptRemoved(@NotNull Script script) { + } + + default void scriptsCleared() { + } + + /** + * Reports that script source has been altered in remote VM. + */ + default void scriptContentChanged(@NotNull Script newScript) { + } + + /** + * Reports a navigation event on the target. + * + * @param newUrl the new URL of the debugged target + */ + default void navigated(String newUrl) { + } + + default void errorOccurred(@NotNull String errorMessage) { + } + + default void childVmAdded(@NotNull Vm childVm) { + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/DeclarativeScope.kt b/platform/script-debugger/backend/src/debugger/DeclarativeScope.kt new file mode 100644 index 00000000..b413c249 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/DeclarativeScope.kt @@ -0,0 +1,22 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.cancelledPromise +import org.jetbrains.debugger.values.ObjectValue +import org.jetbrains.debugger.values.ValueManager + +abstract class DeclarativeScope<VALUE_MANAGER : ValueManager>(type: ScopeType, description: String? = null) : ScopeBase(type, description) { + protected abstract val childrenManager: VariablesHost<VALUE_MANAGER> + + override val variablesHost: VariablesHost<*> + get() = childrenManager + + protected fun loadScopeObjectProperties(value: ObjectValue): Promise<List<Variable>> { + if (childrenManager.valueManager.isObsolete) { + return cancelledPromise() + } + + return value.properties.onSuccess { childrenManager.updateCacheStamp() } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/EvaluateContext.kt b/platform/script-debugger/backend/src/debugger/EvaluateContext.kt new file mode 100644 index 00000000..3c82383a --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/EvaluateContext.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.project.Project +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.values.Value + +data class EvaluateResult(val value: Value, val wasThrown: Boolean = false) + +/** + * A context in which watch expressions may be evaluated. Typically corresponds to stack frame + * of suspended process, but may also be detached from any stack frame + */ +interface EvaluateContext { + /** + * Evaluates an arbitrary `expression` in the particular context. + * Previously loaded [org.jetbrains.debugger.values.ObjectValue]s can be addressed from the expression if listed in + * additionalContext parameter. + */ + fun evaluate(expression: String, additionalContext: Map<String, Any>? = null, enableBreak: Boolean = false, project: Project? = null): Promise<EvaluateResult> + + /** + * optional to implement, some protocols, WIP for example, require you to release remote objects + */ + fun withValueManager(objectGroup: String): EvaluateContext + + /** + * If you evaluate "foo.bar = 4" and want to update Variables view (and all other clients), you can use use this task + * @param promise + */ + fun refreshOnDone(promise: Promise<*>): Promise<*> + + /** + * call only if withLoader was called before + */ + fun releaseObjects() { + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/EvaluateContextBase.kt b/platform/script-debugger/backend/src/debugger/EvaluateContextBase.kt new file mode 100644 index 00000000..cc622e9a --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/EvaluateContextBase.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.thenRun +import org.jetbrains.debugger.values.ValueManager + +abstract class EvaluateContextBase<VALUE_MANAGER : ValueManager>(val valueManager: VALUE_MANAGER) : EvaluateContext { + override fun withValueManager(objectGroup: String): EvaluateContextBase<VALUE_MANAGER> = this + + override fun refreshOnDone(promise: Promise<*>): Promise<Unit> = promise.thenRun { valueManager.clearCaches() } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ExceptionCatchMode.java b/platform/script-debugger/backend/src/debugger/ExceptionCatchMode.java new file mode 100644 index 00000000..1b757939 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ExceptionCatchMode.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +/** + * Defines when VM will break on exception throw (before stack unwind happened) + */ +public enum ExceptionCatchMode { + /** + * VM always breaks when exception is being thrown + */ + ALL, + + /** + * VM breaks when exception is being thrown without try-catch that is going to catch it + */ + UNCAUGHT, + + /** + * VM doesn't break when exception is being thrown + */ + NONE +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ExceptionData.java b/platform/script-debugger/backend/src/debugger/ExceptionData.java new file mode 100755 index 00000000..f423b5a9 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ExceptionData.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import com.intellij.util.ThreeState; +import org.jetbrains.debugger.values.Value; + +/** + * A JavaScript exception data holder for exceptions reported by a JavaScript + * virtual machine. + */ +public interface ExceptionData { + /** + * @return the thrown exception value + */ + Value getExceptionValue(); + + /** + * @return whether this exception is uncaught + */ + ThreeState isUncaught(); + + /** + * @return the text of the source line where the exception was thrown or null + */ + String getSourceText(); + + /** + * @return the exception description (plain text) + */ + String getExceptionMessage(); +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ExceptionDataBase.java b/platform/script-debugger/backend/src/debugger/ExceptionDataBase.java new file mode 100644 index 00000000..2dd0f5d7 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ExceptionDataBase.java @@ -0,0 +1,31 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.debugger.values.Value; + +public abstract class ExceptionDataBase implements ExceptionData { + private final Value exceptionValue; + + protected ExceptionDataBase(Value exceptionValue) { + this.exceptionValue = exceptionValue; + } + + @Override + public final Value getExceptionValue() { + return exceptionValue; + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ObjectProperty.kt b/platform/script-debugger/backend/src/debugger/ObjectProperty.kt new file mode 100644 index 00000000..8162f245 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ObjectProperty.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.debugger.values.FunctionValue + +/** + * Exposes additional data if variable is a property of object and its property descriptor + * is available. + */ +interface ObjectProperty : Variable { + val isWritable: Boolean + + val getter: FunctionValue? + + val setter: FunctionValue? + + + val isConfigurable: Boolean + + val isEnumerable: Boolean +} diff --git a/platform/script-debugger/backend/src/debugger/ObjectPropertyImpl.kt b/platform/script-debugger/backend/src/debugger/ObjectPropertyImpl.kt new file mode 100644 index 00000000..2fd3f0aa --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ObjectPropertyImpl.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.util.BitUtil +import org.jetbrains.debugger.values.FunctionValue +import org.jetbrains.debugger.values.Value + +class ObjectPropertyImpl(name: String, + value: Value?, + override val getter: FunctionValue? = null, + override val setter: FunctionValue? = null, + valueModifier: ValueModifier? = null, + private val flags: Int = 0) : VariableImpl(name, value, valueModifier), ObjectProperty { + companion object { + val WRITABLE: Int = 0x01 + val CONFIGURABLE: Int = 0x02 + val ENUMERABLE: Int = 0x04 + } + + override val isWritable: Boolean + get() = BitUtil.isSet(flags, WRITABLE) + + override val isConfigurable: Boolean + get() = BitUtil.isSet(flags, CONFIGURABLE) + + override val isEnumerable: Boolean + get() = BitUtil.isSet(flags, ENUMERABLE) +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/Scope.kt b/platform/script-debugger/backend/src/debugger/Scope.kt new file mode 100644 index 00000000..36e145dc --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/Scope.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.debugger.values.ObjectValue + +enum class ScopeType { + GLOBAL, + LOCAL, + WITH, + CLOSURE, + CATCH, + LIBRARY, + CLASS, + INSTANCE, + BLOCK, + SCRIPT, + UNKNOWN +} + +interface Scope { + val type: ScopeType + + /** + * Class or function or file name + */ + val description: String? + + val variablesHost: VariablesHost<*> + + val isGlobal: Boolean +} + +abstract class ScopeBase(override val type: ScopeType, override val description: String?) : Scope { + override val isGlobal: Boolean + get() = type === ScopeType.GLOBAL || type === ScopeType.LIBRARY +} + +class ObjectScope(type: ScopeType, private val value: ObjectValue) : ScopeBase(type, value.valueString), Scope { + override val variablesHost: VariablesHost<*> + get() = value.variablesHost +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/Script.kt b/platform/script-debugger/backend/src/debugger/Script.kt new file mode 100755 index 00000000..e2d57e08 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/Script.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.util.UserDataHolderEx +import com.intellij.util.Url +import org.jetbrains.debugger.sourcemap.SourceMap + +interface Script : UserDataHolderEx { + enum class Type { + /** A native, internal JavaScript VM script */ + NATIVE, + + /** A script supplied by an extension */ + EXTENSION, + + /** A normal user script */ + NORMAL + } + + val type: Type + + var sourceMap: SourceMap? + + val url: Url + + val functionName: String? + get() = null + + val line: Int + + val column: Int + + val endLine: Int + + val isWorker: Boolean +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ScriptBase.kt b/platform/script-debugger/backend/src/debugger/ScriptBase.kt new file mode 100755 index 00000000..0f41a9cf --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ScriptBase.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.util.Url +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.sourcemap.SourceMap + +abstract class ScriptBase(override val type: Script.Type, + override val url: Url, + line: Int, + override val column: Int, + override val endLine: Int) : UserDataHolderBase(), Script { + override val line: Int = Math.max(line, 0) + + @SuppressWarnings("UnusedDeclaration") + @Volatile + private var source: Promise<String>? = null + + override var sourceMap: SourceMap? = null + + override fun toString(): String = "[url=$url, lineRange=[$line;$endLine]]" + + override val isWorker: Boolean = false +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ScriptManager.kt b/platform/script-debugger/backend/src/debugger/ScriptManager.kt new file mode 100644 index 00000000..a8cf29a6 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ScriptManager.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.util.Processor +import com.intellij.util.Url +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.values.FunctionValue + +const val VM_SCHEME: String = "vm" + +interface ScriptManager { + fun getSource(script: Script): Promise<String> + + fun hasSource(script: Script): Boolean + + fun containsScript(script: Script): Boolean + + fun forEachScript(scriptProcessor: (Script) -> Boolean) + + fun forEachScript(scriptProcessor: Processor<Script>): Unit = forEachScript { scriptProcessor.process(it)} + + fun getScript(function: FunctionValue): Promise<Script> + + fun getScript(frame: CallFrame): Script? + + fun findScriptByUrl(rawUrl: String): Script? + + fun findScriptByUrl(url: Url): Script? + + fun findScriptById(id: String): Script? = null +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ScriptManagerBase.kt b/platform/script-debugger/backend/src/debugger/ScriptManagerBase.kt new file mode 100644 index 00000000..d863f7bf --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ScriptManagerBase.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.util.Url +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.PromiseManager +import org.jetbrains.concurrency.rejectedPromise + +abstract class ScriptManagerBase<SCRIPT : ScriptBase> : ScriptManager { + @Suppress("UNCHECKED_CAST") + @SuppressWarnings("unchecked") + private val scriptSourceLoader = object : PromiseManager<ScriptBase, String>(ScriptBase::class.java) { + override fun load(script: ScriptBase) = loadScriptSource(script as SCRIPT) + } + + protected abstract fun loadScriptSource(script: SCRIPT): Promise<String> + + override fun getSource(script: Script): Promise<String> { + if (!containsScript(script)) { + return rejectedPromise("No Script") + } + @Suppress("UNCHECKED_CAST") + return scriptSourceLoader.get(script as SCRIPT) + } + + override fun hasSource(script: Script): Boolean { + @Suppress("UNCHECKED_CAST") + return scriptSourceLoader.has(script as SCRIPT) + } + + fun setSource(script: SCRIPT, source: String?) { + scriptSourceLoader.set(script, source) + } +} + +val Url.isSpecial: Boolean + get() = !isInLocalFileSystem && (scheme == null || scheme == VM_SCHEME || authority == null)
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ScriptManagerBaseEx.kt b/platform/script-debugger/backend/src/debugger/ScriptManagerBaseEx.kt new file mode 100644 index 00000000..a20ef260 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ScriptManagerBaseEx.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.util.Url +import com.intellij.util.Urls +import com.intellij.util.containers.ContainerUtil +import java.util.concurrent.ConcurrentMap + +abstract class ScriptManagerBaseEx<SCRIPT : ScriptBase> : ScriptManagerBase<SCRIPT>() { + protected val idToScript: ConcurrentMap<String, SCRIPT> = ContainerUtil.newConcurrentMap<String, SCRIPT>() + + final override fun forEachScript(scriptProcessor: (Script) -> Boolean) { + for (script in idToScript.values) { + if (!scriptProcessor(script)) { + return + } + } + } + + final override fun findScriptById(id: String): SCRIPT? = idToScript[id] + + fun clear(listener: DebugEventListener) { + idToScript.clear() + listener.scriptsCleared() + } + + final override fun findScriptByUrl(rawUrl: String): SCRIPT? = findScriptByUrl(rawUrlToOurUrl(rawUrl)) + + final override fun findScriptByUrl(url: Url): SCRIPT? { + return idToScript.values.find { url == it.url } + // TODO Searching ignoring parameters may be fragile, because parameters define script e.g. in webpack. Consider dropping it. + ?: idToScript.values.find { url.equalsIgnoreParameters(it.url) } + } + + open fun rawUrlToOurUrl(rawUrl: String): Url = Urls.parseEncoded(rawUrl)!! +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ScriptRegExpBreakpointTarget.kt b/platform/script-debugger/backend/src/debugger/ScriptRegExpBreakpointTarget.kt new file mode 100644 index 00000000..bfd9d98e --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ScriptRegExpBreakpointTarget.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +class ScriptRegExpBreakpointTarget(private val regExp: String, val language: String? = null) : BreakpointTarget() { + override fun <R> accept(visitor: BreakpointTarget.Visitor<R>): R { + if (visitor is ScriptRegExpSupportVisitor<*>) { + return (visitor as ScriptRegExpSupportVisitor<R>).visitRegExp(this) + } + else { + return visitor.visitUnknown(this) + } + } + + override fun toString(): String = regExp + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null || javaClass != other.javaClass) { + return false + } + return regExp == (other as ScriptRegExpBreakpointTarget).regExp + } + + override fun hashCode(): Int = regExp.hashCode() +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ScriptRegExpSupportVisitor.java b/platform/script-debugger/backend/src/debugger/ScriptRegExpSupportVisitor.java new file mode 100644 index 00000000..9ab738d0 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ScriptRegExpSupportVisitor.java @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.annotations.NotNull; + +/** + * Additional interface that user visitor may implement for {@link BreakpointTarget#accept} + * method. + */ +public interface ScriptRegExpSupportVisitor<R> extends BreakpointTarget.Visitor<R> { + R visitRegExp(@NotNull ScriptRegExpBreakpointTarget target); +} diff --git a/platform/script-debugger/backend/src/debugger/StandaloneVmHelper.kt b/platform/script-debugger/backend/src/debugger/StandaloneVmHelper.kt new file mode 100644 index 00000000..1d7889ef --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/StandaloneVmHelper.kt @@ -0,0 +1,84 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.util.io.addChannelListener +import com.intellij.util.io.shutdownIfOio +import io.netty.channel.Channel +import org.jetbrains.concurrency.AsyncPromise +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.errorIfNotMessage +import org.jetbrains.concurrency.nullPromise +import org.jetbrains.jsonProtocol.Request +import org.jetbrains.rpc.CONNECTION_CLOSED_MESSAGE +import org.jetbrains.rpc.LOG +import org.jetbrains.rpc.MessageProcessor + +open class StandaloneVmHelper(private val vm: Vm, private val messageProcessor: MessageProcessor, channel: Channel) : AttachStateManager { + @Volatile + private var channel: Channel? = channel + + fun getChannelIfActive(): Channel? { + val currentChannel = channel + return if (currentChannel == null || !currentChannel.isActive) null else currentChannel + } + + fun write(content: Any): Boolean { + val channel = getChannelIfActive() + return channel != null && !channel.writeAndFlush(content).isCancelled + } + + interface VmEx : Vm { + fun createDisconnectRequest(): Request<out Any>? + } + + override val isAttached: Boolean + get() = channel != null + + override fun detach(): Promise<*> { + val currentChannel = channel ?: return nullPromise() + + messageProcessor.cancelWaitingRequests() + val disconnectRequest = (vm as? VmEx)?.createDisconnectRequest() + val promise = AsyncPromise<Any?>() + if (disconnectRequest == null) { + messageProcessor.closed() + channel = null + } + else { + messageProcessor.send(disconnectRequest) + .onError { + if (it.message != CONNECTION_CLOSED_MESSAGE) { + LOG.errorIfNotMessage(it) + } + } + // we don't wait response because 1) no response to "disconnect" message (V8 for example) 2) closed message manager just ignore any incoming messages + currentChannel.flush() + messageProcessor.closed() + channel = null + messageProcessor.cancelWaitingRequests() + } + closeChannel(currentChannel, promise) + return promise + } + + protected open fun closeChannel(channel: Channel, promise: AsyncPromise<Any?>) { + doCloseChannel(channel, promise) + } +} + +fun doCloseChannel(channel: Channel, promise: AsyncPromise<Any?>) { + channel.close().addChannelListener { + try { + it.channel().eventLoop().shutdownIfOio() + } + finally { + val error = it.cause() + if (error == null) { + promise.setResult(null) + } + else { + promise.setError(error) + } + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/SuspendContext.kt b/platform/script-debugger/backend/src/debugger/SuspendContext.kt new file mode 100755 index 00000000..2b280e2e --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/SuspendContext.kt @@ -0,0 +1,66 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.values.ValueManager + +/** + * An object that matches the execution state of the VM while suspended + */ +interface SuspendContext<out CALL_FRAME : CallFrame> { + + val script: Script? + get() = topFrame?.let { vm.scriptManager.getScript(it) } + + /** + * @return the current exception state if execution was paused because of exception, or `null` otherwise. + */ + val exceptionData: ExceptionData? + get() = null + + val topFrame: CALL_FRAME? + + /** + * Call frames for the current suspended state (from the innermost (top) frame to the main (bottom) frame) + */ + val frames: Promise<Array<CallFrame>> + + /** + * list of the breakpoints hit on VM suspension with which this + * context is associated. An empty collection if the suspension was + * not related to hitting breakpoints (e.g. a step end) + */ + val breakpointsHit: List<Breakpoint> + + val hasUnresolvedBreakpointsHit: Boolean + get() = false + + val valueManager: ValueManager + + val vm: Vm + get() = throw UnsupportedOperationException() +} + +abstract class ContextDependentAsyncResultConsumer<T>(private val context: SuspendContext<*>) : java.util.function.Consumer<T> { + final override fun accept(result: T) { + val vm = context.vm + if (vm.attachStateManager.isAttached && !vm.suspendContextManager.isContextObsolete(context)) { + accept(result, vm) + } + } + + protected abstract fun accept(result: T, vm: Vm) +} + + +inline fun <T> Promise<T>.onSuccess(context: SuspendContext<*>, crossinline handler: (result: T) -> Unit): Promise<T> { + return onSuccess(object : ContextDependentAsyncResultConsumer<T>(context) { + override fun accept(result: T, vm: Vm) = handler(result) + }) +} + +inline fun Promise<*>.onError(context: SuspendContext<*>, crossinline handler: (error: Throwable) -> Unit): Promise<out Any> { + return onError(object : ContextDependentAsyncResultConsumer<Throwable>(context) { + override fun accept(result: Throwable, vm: Vm) = handler(result) + }) +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/SuspendContextBase.kt b/platform/script-debugger/backend/src/debugger/SuspendContextBase.kt new file mode 100644 index 00000000..5e7018a5 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/SuspendContextBase.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +abstract class SuspendContextBase<F : CallFrame> : SuspendContext<F> { +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/SuspendContextManager.kt b/platform/script-debugger/backend/src/debugger/SuspendContextManager.kt new file mode 100644 index 00000000..275ba088 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/SuspendContextManager.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise + +interface SuspendContextManager<CALL_FRAME : CallFrame> { + /** + * Tries to suspend VM. If successful, [DebugEventListener.suspended] will be called. + */ + fun suspend(): Promise<*> + + val context: SuspendContext<CALL_FRAME>? + + val contextOrFail: SuspendContext<CALL_FRAME> + + fun isContextObsolete(context: SuspendContext<*>): Boolean = this.context !== context + + fun setOverlayMessage(message: String?) + + /** + * Resumes the VM execution. This context becomes invalid until another context is supplied through the + * [DebugEventListener.suspended] event. + * @param stepAction to perform + * * + * @param stepCount steps to perform (not used if `stepAction == CONTINUE`) + */ + fun continueVm(stepAction: StepAction, stepCount: Int = 1): Promise<*> + + val isRestartFrameSupported: Boolean + + /** + * Restarts a frame (all frames above are dropped from the stack, this frame is started over). + * for success the boolean parameter + * is true if VM has been resumed and is expected to get suspended again in a moment (with + * a standard 'resumed' notification), and is false if call frames list is already updated + * without VM state change (this case presently is never actually happening) + */ + fun restartFrame(callFrame: CALL_FRAME): Promise<Boolean> + + /** + * @return whether reset operation is supported for the particular callFrame + */ + fun canRestartFrame(callFrame: CallFrame): Boolean +} + +enum class StepAction { + /** + * Resume the JavaScript execution. + */ + CONTINUE, + + /** + * Step into the current statement. + */ + IN, + + /** + * Step over the current statement. + */ + OVER, + + /** + * Step out of the current function. + */ + OUT +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/SuspendContextManagerBase.kt b/platform/script-debugger/backend/src/debugger/SuspendContextManagerBase.kt new file mode 100644 index 00000000..3b3b3dea --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/SuspendContextManagerBase.kt @@ -0,0 +1,64 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.rejectedPromise +import org.jetbrains.concurrency.resolvedPromise +import java.util.concurrent.atomic.AtomicReference + +abstract class SuspendContextManagerBase<T : SuspendContext<CALL_FRAME>, CALL_FRAME : CallFrame> : SuspendContextManager<CALL_FRAME> { + val contextRef: AtomicReference<T> = AtomicReference() + + protected abstract val debugListener: DebugEventListener + + fun setContext(newContext: T) { + if (!contextRef.compareAndSet(null, newContext)) { + throw IllegalStateException("Attempt to set context, but current suspend context is already exists") + } + } + + open fun updateContext(newContext: SuspendContext<*>) { + } + + // dismiss context on resumed + protected fun dismissContext() { + contextRef.get()?.let { + contextDismissed(it) + } + } + + protected fun dismissContextOnDone(promise: Promise<*>): Promise<*> { + val context = contextOrFail + promise.onSuccess { contextDismissed(context) } + return promise + } + + fun contextDismissed(context: T) { + if (!contextRef.compareAndSet(context, null)) { + throw IllegalStateException("Expected $context, but another suspend context exists") + } + context.valueManager.markObsolete() + debugListener.resumed(context.vm) + } + + override val context: SuspendContext<CALL_FRAME>? + get() = contextRef.get() + + override val contextOrFail: T + get() = contextRef.get() ?: throw IllegalStateException("No current suspend context") + + override fun suspend(): Promise<out Any?> = if (context == null) doSuspend() else resolvedPromise() + + protected abstract fun doSuspend(): Promise<*> + + override fun setOverlayMessage(message: String?) { + } + + override fun restartFrame(callFrame: CALL_FRAME): Promise<Boolean> = restartFrame(callFrame, contextOrFail) + + protected open fun restartFrame(callFrame: CALL_FRAME, currentContext: T): Promise<Boolean> = rejectedPromise<Boolean>("Unsupported") + + override fun canRestartFrame(callFrame: CallFrame): Boolean = false + + override val isRestartFrameSupported: Boolean = false +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ValueModifier.kt b/platform/script-debugger/backend/src/debugger/ValueModifier.kt new file mode 100644 index 00000000..4691fc81 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ValueModifier.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.values.Value + +interface ValueModifier { + // expression can contains reference to another variables in current scope, so, we should evaluate it before set + // https://youtrack.jetbrains.com/issue/WEB-2342#comment=27-512122 + + // we don't worry about performance in case of simple primitive values - boolean/string/numbers, + // it works quickly and we don't want to complicate our code and debugger SDK + fun setValue(variable: Variable, newValue: String, evaluateContext: EvaluateContext): Promise<*> + + fun setValue(variable: Variable, newValue: Value, evaluateContext: EvaluateContext): Promise<*> + + fun evaluateGet(variable: Variable, evaluateContext: EvaluateContext): Promise<Value> +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ValueModifierUtil.kt b/platform/script-debugger/backend/src/debugger/ValueModifierUtil.kt new file mode 100644 index 00000000..bdd75c63 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ValueModifierUtil.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.thenAsyncAccept +import org.jetbrains.debugger.values.Value +import org.jetbrains.io.JsonUtil +import java.util.* +import java.util.regex.Pattern + +private val KEY_NOTATION_PROPERTY_NAME_PATTERN = Pattern.compile("[\\p{L}_$]+[\\d\\p{L}_$]*") + +object ValueModifierUtil { + fun setValue(variable: Variable, + newValue: String, + evaluateContext: EvaluateContext, + modifier: ValueModifier): Promise<Any?> = evaluateContext.evaluate(newValue) + .thenAsyncAccept { modifier.setValue(variable, it.value, evaluateContext) } + + fun evaluateGet(variable: Variable, + host: Any, + evaluateContext: EvaluateContext, + selfName: String): Promise<Value> { + val builder = StringBuilder(selfName) + appendUnquotedName(builder, variable.name) + return evaluateContext.evaluate(builder.toString(), Collections.singletonMap(selfName, host), false) + .then { + variable.value = it.value + it.value + } + } + + fun propertyNamesToString(list: List<String>, quotedAware: Boolean): String { + val builder = StringBuilder() + for (i in list.indices.reversed()) { + val name = list[i] + doAppendName(builder, name, quotedAware && (name[0] == '"' || name[0] == '\'')) + } + return builder.toString() + } + + fun appendUnquotedName(builder: StringBuilder, name: String) { + doAppendName(builder, name, false) + } +} + +private fun doAppendName(builder: StringBuilder, name: String, quoted: Boolean) { + val isProperty = !builder.isEmpty() + if (isProperty) { + val useKeyNotation = !quoted && KEY_NOTATION_PROPERTY_NAME_PATTERN.matcher(name).matches() + if (useKeyNotation) { + builder.append('.').append(name) + } + else { + builder.append('[') + if (quoted) builder.append(name) + else JsonUtil.escape(name, builder) + builder.append(']') + } + } + else { + builder.append(name) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/Variable.java b/platform/script-debugger/backend/src/debugger/Variable.java new file mode 100755 index 00000000..98cfe3e9 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/Variable.java @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.values.Value; + +public interface Variable { + /** + * @return whether it is possible to read this variable + */ + boolean isReadable(); + + /** + * Returns the value of this variable. + * + * @return a Value corresponding to this variable. {@code null} if the property has accessor descriptor + * @see #isReadable() + */ + @Nullable + Value getValue(); + + void setValue(Value value); + + @NotNull + String getName(); + + /** + * @return whether it is possible to modify this variable + */ + boolean isMutable(); + + @Nullable + ValueModifier getValueModifier(); +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/VariableImpl.java b/platform/script-debugger/backend/src/debugger/VariableImpl.java new file mode 100644 index 00000000..178420c3 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/VariableImpl.java @@ -0,0 +1,75 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.values.Value; + +public class VariableImpl implements Variable { + protected volatile Value value; + private final String name; + + private final ValueModifier valueModifier; + + public VariableImpl(@NotNull String name, @Nullable Value value, @Nullable ValueModifier valueModifier) { + this.name = name; + this.value = value; + this.valueModifier = valueModifier; + } + + public VariableImpl(@NotNull String name, @NotNull Value value) { + this(name, value, null); + } + + @Nullable + @Override + public final ValueModifier getValueModifier() { + return valueModifier; + } + + @NotNull + @Override + public final String getName() { + return name; + } + + @Nullable + @Override + public final Value getValue() { + return value; + } + + @Override + public void setValue(Value value) { + this.value = value; + } + + @Override + public boolean isMutable() { + return valueModifier != null; + } + + @Override + public boolean isReadable() { + return true; + } + + @Override + public String toString() { + return "[Variable: name=" + getName() + ", value=" + getValue() + ']'; + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/VariablesHost.java b/platform/script-debugger/backend/src/debugger/VariablesHost.java new file mode 100644 index 00000000..ba9823b6 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/VariablesHost.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.concurrency.Promise; +import org.jetbrains.concurrency.PromiseManager; +import org.jetbrains.concurrency.Promises; +import org.jetbrains.debugger.values.ValueManager; + +import java.util.List; + +public abstract class VariablesHost<VALUE_MANAGER extends ValueManager> { + @SuppressWarnings("unchecked") + private static final PromiseManager<VariablesHost, List<Variable>> VARIABLES_LOADER = + new PromiseManager<VariablesHost, List<Variable>>(VariablesHost.class) { + @Override + public boolean isUpToDate(@NotNull VariablesHost host, @NotNull List<Variable> data) { + return host.valueManager.getCacheStamp() == host.cacheStamp; + } + + @NotNull + @Override + public Promise load(@NotNull VariablesHost host) { + return host.valueManager.isObsolete() ? Promises.cancelledPromise() : host.load(); + } + }; + + @SuppressWarnings("UnusedDeclaration") + private volatile Promise<List<Variable>> result; + + private volatile int cacheStamp = -1; + + public final VALUE_MANAGER valueManager; + + public VariablesHost(@NotNull VALUE_MANAGER manager) { + valueManager = manager; + } + + /** + * You must call {@link #updateCacheStamp()} when data loaded + */ + @NotNull + public final Promise<List<Variable>> get() { + return VARIABLES_LOADER.get(this); + } + + @Nullable + public final Promise.State getState() { + return VARIABLES_LOADER.getState(this); + } + + public final void set(@NotNull List<Variable> result) { + updateCacheStamp(); + VARIABLES_LOADER.set(this, result); + } + + @NotNull + protected abstract Promise<List<Variable>> load(); + + public final void updateCacheStamp() { + cacheStamp = valueManager.getCacheStamp(); + } + + /** + * Some backends requires to reload the whole call stack on scope variable modification, but not all API is asynchronous (compromise, to not increase complexity), + * for example, {@link CallFrame#getVariableScopes()} is not asynchronous method. So, you must use returned callback to postpone your code working with updated data. + */ + public Promise<?> clearCaches() { + cacheStamp = -1; + VARIABLES_LOADER.reset(this); + return Promises.resolvedPromise(); + } +} diff --git a/platform/script-debugger/backend/src/debugger/Vm.kt b/platform/script-debugger/backend/src/debugger/Vm.kt new file mode 100644 index 00000000..b7ef99b4 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/Vm.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.util.UserDataHolderEx +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.nullPromise + +interface AttachStateManager { + fun detach(): Promise<*> = nullPromise() + + val isAttached: Boolean + get() = true +} + +interface Vm : UserDataHolderEx { + val debugListener: DebugEventListener + + val attachStateManager: AttachStateManager + + val evaluateContext: EvaluateContext? + + val scriptManager: ScriptManager + + val breakpointManager: BreakpointManager + + val suspendContextManager: SuspendContextManager<out CallFrame> + + /** + * Controls whether VM stops on exceptions + */ + fun setBreakOnException(catchMode: ExceptionCatchMode): Promise<*> = nullPromise() + + val presentableName: String + get() = "main loop" + + val childVMs: MutableList<Vm> +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/VmBase.kt b/platform/script-debugger/backend/src/debugger/VmBase.kt new file mode 100644 index 00000000..6af35c76 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/VmBase.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.util.containers.ContainerUtil + +abstract class VmBase(override val debugListener: DebugEventListener) : Vm, AttachStateManager, UserDataHolderBase() { + override val evaluateContext: EvaluateContext? by lazy(LazyThreadSafetyMode.NONE) { computeEvaluateContext() } + + override val attachStateManager: AttachStateManager = this + + protected open fun computeEvaluateContext(): EvaluateContext? = null + + override val childVMs: MutableList<Vm> = ContainerUtil.createConcurrentList() +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java b/platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java new file mode 100644 index 00000000..7576f500 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java @@ -0,0 +1,79 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.sourcemap; + +final class Base64VLQ { + private Base64VLQ() { + } + + interface CharIterator { + boolean hasNext(); + char next(); + } + + // A Base64 VLQ digit can represent 5 bits, so it is base-32. + private static final int VLQ_BASE_SHIFT = 5; + private static final int VLQ_BASE = 1 << VLQ_BASE_SHIFT; + + // A mask of bits for a VLQ digit (11111), 31 decimal. + private static final int VLQ_BASE_MASK = VLQ_BASE - 1; + + // The continuation bit is the 6th bit. + private static final int VLQ_CONTINUATION_BIT = VLQ_BASE; + + /** + * Decodes the next VLQValue from the provided CharIterator. + */ + public static int decode(CharIterator in) { + int result = 0; + int shift = 0; + int digit; + do { + digit = Base64.BASE64_DECODE_MAP[in.next()]; + assert (digit != -1) : "invalid char"; + + result += (digit & VLQ_BASE_MASK) << shift; + shift += VLQ_BASE_SHIFT; + } + while ((digit & VLQ_CONTINUATION_BIT) != 0); + + boolean negate = (result & 1) == 1; + result >>= 1; + return negate ? -result : result; + } + + private static final class Base64 { + /** + * A map used to convert integer values in the range 0-63 to their base64 + * values. + */ + private static final String BASE64_MAP = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789+/"; + + /** + * A map used to convert base64 character into integer values. + */ + private static final int[] BASE64_DECODE_MAP = new int[256]; + + static { + for (int i = 0; i < BASE64_MAP.length(); i++) { + BASE64_DECODE_MAP[BASE64_MAP.charAt(i)] = i; + } + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt b/platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt new file mode 100644 index 00000000..8f9aad86 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.sourcemap + +/** + * Mapping entry in the source map + */ +interface MappingEntry { + val generatedColumn: Int + + val generatedLine: Int + + val sourceLine: Int + + val sourceColumn: Int + + val source: Int + get() = -1 + + val name: String? + get() = null + + val nextGenerated: MappingEntry +} diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt b/platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt new file mode 100644 index 00000000..c87786c0 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt @@ -0,0 +1,210 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.sourcemap + +import com.intellij.openapi.editor.Document +import java.util.* + +interface Mappings { + fun get(line: Int, column: Int): MappingEntry? + + fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean = true): MappingEntry? + + fun getNext(mapping: MappingEntry): MappingEntry? + + fun getNextOnTheSameLine(mapping: MappingEntry): MappingEntry? { + val nextMapping = getNext(mapping) + return if (nextMapping != null && getLine(nextMapping) == getLine(mapping)) nextMapping else null + } + + fun indexOf(line: Int, column: Int): Int + + fun getByIndex(index: Int): MappingEntry + + fun getLine(mapping: MappingEntry): Int + + fun getColumn(mapping: MappingEntry): Int +} + +abstract class MappingList(private val mappings: List<MappingEntry>) : Mappings { + val size: Int + get() = mappings.size + + protected abstract val comparator: Comparator<MappingEntry> + + override fun indexOf(line: Int, column: Int): Int { + var low = 0 + var high = mappings.size - 1 + if (mappings.isEmpty() || getLine(mappings[low]) > line || getLine(mappings[high]) < line) { + return -1 + } + + while (low <= high) { + val middle = (low + high).ushr(1) + val mapping = mappings[middle] + val mappingLine = getLine(mapping) + if (line == mappingLine) { + if (column == getColumn(mapping)) { + // find first + var firstIndex = middle + while (firstIndex > 0) { + val prevMapping = mappings[firstIndex - 1] + if (getLine(prevMapping) == line && getColumn(prevMapping) == column) { + firstIndex-- + } + else { + break + } + } + return firstIndex + } + else if (column < getColumn(mapping)) { + if (column == 0 || column == -1) { + // find first + var firstIndex = middle + while (firstIndex > 0 && getLine(mappings[firstIndex - 1]) == line) { + firstIndex-- + } + return firstIndex + } + + if (middle == 0) { + return -1 + } + + val prevMapping = mappings[middle - 1] + when { + line != getLine(prevMapping) -> return -1 + column >= getColumn(prevMapping) -> return middle - 1 + else -> high = middle - 1 + } + } + else { + // https://code.google.com/p/google-web-toolkit/issues/detail?id=9103 + // We skipIfColumnEquals because GWT has two entries - source position equals, but generated no. We must use first entry (at least, in case of GWT it is correct) + val nextMapping = getNextOnTheSameLine(middle) + if (nextMapping == null) { + return middle + } + else { + low = middle + 1 + } + } + } + else if (line > mappingLine) { + low = middle + 1 + } + else { + high = middle - 1 + } + } + + return -1 + } + + // todo honor Google Chrome bug related to paused location + override fun get(line: Int, column: Int): MappingEntry? = mappings.getOrNull(indexOf(line, column)) + + private fun getNext(index: Int) = mappings.getOrNull(index + 1) + + override fun getNext(mapping: MappingEntry): MappingEntry? { + if (comparator == MAPPING_COMPARATOR_BY_GENERATED_POSITION) { + return mapping.nextGenerated + } + + var index = mappings.binarySearch(mapping, comparator) + if (index < 0) { + return null + } + index++ + + var result: MappingEntry? + do { + result = mappings.getOrNull(index++) + } + while (mapping === result) + return result + } + + override fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean): MappingEntry? { + var nextMapping = getNext(index) ?: return null + + val mapping = getByIndex(index) + if (getLine(nextMapping) != getLine(mapping)) { + return null + } + + if (skipIfColumnEquals) { + var i = index + // several generated segments can point to one source segment, so, in mapping list ordered by source, could be several mappings equal in terms of source position + while (getColumn(nextMapping) == getColumn(mapping)) { + nextMapping = getNextOnTheSameLine(++i, false) ?: return null + } + } + + return nextMapping + } + + fun getEndOffset(mapping: MappingEntry, lineStartOffset: Int, document: Document): Int { + val nextMapping = getNextOnTheSameLine(Collections.binarySearch(mappings, mapping, comparator)) + return if (nextMapping == null) document.getLineEndOffset(getLine(mapping)) else lineStartOffset + getColumn(nextMapping) + } + + override fun getByIndex(index: Int): MappingEntry = mappings.get(index) + + // entries will be processed in this list order + fun processMappingsInLine(line: Int, entryProcessor: MappingsProcessorInLine): Boolean { + var low = 0 + var high = mappings.size - 1 + while (low <= high) { + val middle = (low + high).ushr(1) + val mapping = mappings.get(middle) + val mappingLine = getLine(mapping) + when { + line == mappingLine -> { + // find first + var firstIndex = middle + while (firstIndex > 0 && getLine(mappings.get(firstIndex - 1)) == line) { + firstIndex-- + } + + var entry: MappingEntry? = mappings.get(firstIndex) + do { + var nextEntry = mappings.getOrNull(++firstIndex) + if (nextEntry != null && getLine(nextEntry) != line) { + nextEntry = null + } + + if (!entryProcessor.process(entry!!, nextEntry)) { + return true + } + + entry = nextEntry + } + while (entry != null) + return true + } + line > mappingLine -> low = middle + 1 + else -> high = middle - 1 + } + } + return false + } +} + +interface MappingsProcessorInLine { + fun process(entry: MappingEntry, nextEntry: MappingEntry?): Boolean +} diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt b/platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt new file mode 100644 index 00000000..c4a5ebfc --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.sourcemap + +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Url +import gnu.trove.THashMap + +class NestedSourceMap(private val childMap: SourceMap, private val parentMap: SourceMap) : SourceMap { + override val sourceResolver: SourceResolver + get() = parentMap.sourceResolver + + override val sources: Array<Url> + get() = parentMap.sources + + private val sourceIndexToSourceMappings = arrayOfNulls<Mappings>(parentMap.sources.size) + + private val childMappingToTransformed = THashMap<MappingEntry, MappingEntry>() + + override val outFile: String? + get() = childMap.outFile + + override val hasNameMappings: Boolean + get() = childMap.hasNameMappings || parentMap.hasNameMappings + + override val generatedMappings: Mappings by lazy { + NestedMappings(childMap.generatedMappings, parentMap.generatedMappings, false) + } + + override fun findSourceMappings(sourceIndex: Int): Mappings { + var result = sourceIndexToSourceMappings.get(sourceIndex) + if (result == null) { + result = NestedMappings(childMap.findSourceMappings(sourceIndex), parentMap.findSourceMappings(sourceIndex), true) + sourceIndexToSourceMappings.set(sourceIndex, result) + } + return result + } + + override fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int = parentMap.findSourceIndex(sourceFile, localFileUrlOnly) + + override fun findSourceIndex(sourceUrls: List<Url>, + sourceFile: VirtualFile?, + resolver: Lazy<SourceFileResolver?>?, + localFileUrlOnly: Boolean): Int = parentMap.findSourceIndex(sourceUrls, sourceFile, resolver, localFileUrlOnly) + + override fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean { + val childSourceMappings = childMap.findSourceMappings(sourceIndex) + return (parentMap.findSourceMappings(sourceIndex) as MappingList).processMappingsInLine(sourceLine, object: MappingsProcessorInLine { + override fun process(entry: MappingEntry, nextEntry: MappingEntry?): Boolean { + val childIndex = childSourceMappings.indexOf(entry.generatedLine, entry.generatedColumn) + if (childIndex == -1) { + return true + } + + val childEntry = childSourceMappings.getByIndex(childIndex) + // todo not clear - should we resolve next child entry by current child index or by provided parent nextEntry? + val nextChildEntry = if (nextEntry == null) null else childSourceMappings.getNextOnTheSameLine(childIndex) + return mappingProcessor.process(childMappingToTransformed.getOrPut(childEntry) { NestedMappingEntry(childEntry, entry) }, + nextChildEntry?.let { childMappingToTransformed.getOrPut(it) { NestedMappingEntry(it, entry) } }) + } + }) + } +} + +private class NestedMappings(private val child: Mappings, private val parent: Mappings, private val isSourceMappings: Boolean) : Mappings { + override fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean) = parent.getNextOnTheSameLine(index, skipIfColumnEquals) + + override fun getNext(mapping: MappingEntry) = parent.getNext(mapping) + + override fun indexOf(line: Int, column: Int) = parent.indexOf(line, column) + + override fun getByIndex(index: Int) = parent.getByIndex(index) + + override fun getLine(mapping: MappingEntry) = parent.getLine(mapping) + + override fun getColumn(mapping: MappingEntry) = parent.getColumn(mapping) + + override fun get(line: Int, column: Int): MappingEntry? { + return if (isSourceMappings) { + parent.get(line, column)?.let { child.get(it.generatedLine, it.generatedColumn) } + } + else { + child.get(line, column)?.let { parent.get(it.sourceLine, it.sourceColumn) } + } + } +} + +private data class NestedMappingEntry(private val child: MappingEntry, private val parent: MappingEntry) : MappingEntry { + override val generatedLine: Int + get() = child.generatedLine + + override val generatedColumn: Int + get() = child.generatedColumn + + override val sourceLine: Int + get() = parent.sourceLine + + override val sourceColumn: Int + get() = parent.sourceColumn + + override val name: String? + get() = parent.name + + override val nextGenerated: MappingEntry + get() = child.nextGenerated +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt new file mode 100644 index 00000000..283a5b6c --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.sourcemap + +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Url + +// sources - is not originally specified, but canonicalized/normalized +// lines and columns are zero-based according to specification +interface SourceMap { + val outFile: String? + + /** + * note: Nested map returns only parent sources + */ + val sources: Array<Url> + + val generatedMappings: Mappings + val hasNameMappings: Boolean + val sourceResolver: SourceResolver + + fun findSourceMappings(sourceIndex: Int): Mappings + + fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Int + + fun findSourceMappings(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Mappings? { + val sourceIndex = findSourceIndex(sourceUrls, sourceFile, resolver, localFileUrlOnly) + return if (sourceIndex >= 0) findSourceMappings(sourceIndex) else null + } + + fun getSourceLineByRawLocation(rawLine: Int, rawColumn: Int): Int = generatedMappings.get(rawLine, rawColumn)?.sourceLine ?: -1 + + fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int + + fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean + + fun processSourceMappingsInLine(sourceUrls: List<Url>, sourceLine: Int, mappingProcessor: MappingsProcessorInLine, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Boolean { + val sourceIndex = findSourceIndex(sourceUrls, sourceFile, resolver, localFileUrlOnly) + return sourceIndex >= 0 && processSourceMappingsInLine(sourceIndex, sourceLine, mappingProcessor) + } +} + + +class OneLevelSourceMap(override val outFile: String?, + override val generatedMappings: Mappings, + private val sourceIndexToMappings: Array<MappingList?>, + override val sourceResolver: SourceResolver, + override val hasNameMappings: Boolean) : SourceMap { + override val sources: Array<Url> + get() = sourceResolver.canonicalizedUrls + + override fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Int { + val index = sourceResolver.findSourceIndex(sourceUrls, sourceFile, localFileUrlOnly) + if (index == -1 && resolver != null) { + return resolver.value?.let { sourceResolver.findSourceIndex(it) } ?: -1 + } + return index + } + + // returns SourceMappingList + override fun findSourceMappings(sourceIndex: Int): MappingList = sourceIndexToMappings.get(sourceIndex)!! + + override fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int = sourceResolver.findSourceIndexByFile(sourceFile, localFileUrlOnly) + + override fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean { + return findSourceMappings(sourceIndex).processMappingsInLine(sourceLine, mappingProcessor) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt new file mode 100644 index 00000000..3ecdd792 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt @@ -0,0 +1,372 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.sourcemap + +import com.google.gson.stream.JsonToken +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.util.registry.Registry +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.util.text.StringUtilRt +import com.intellij.util.PathUtil +import com.intellij.util.SmartList +import com.intellij.util.UriUtil +import com.intellij.util.containers.isNullOrEmpty +import org.jetbrains.debugger.sourcemap.Base64VLQ.CharIterator +import org.jetbrains.io.JsonReaderEx +import java.io.IOException +import java.util.* +import kotlin.properties.Delegates.notNull + +private val MAPPING_COMPARATOR_BY_SOURCE_POSITION = Comparator<MappingEntry> { o1, o2 -> + if (o1.sourceLine == o2.sourceLine) { + o1.sourceColumn - o2.sourceColumn + } + else { + o1.sourceLine - o2.sourceLine + } +} + +val MAPPING_COMPARATOR_BY_GENERATED_POSITION: Comparator<MappingEntry> = Comparator { o1, o2 -> + if (o1.generatedLine == o2.generatedLine) { + o1.generatedColumn - o2.generatedColumn + } + else { + o1.generatedLine - o2.generatedLine + } +} + +internal const val UNMAPPED = -1 + +// https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US +fun decodeSourceMap(`in`: CharSequence, sourceResolverFactory: (sourceUrls: List<String>, sourceContents: List<String?>?) -> SourceResolver): SourceMap? { + if (`in`.isEmpty()) { + throw IOException("source map contents cannot be empty") + } + + val reader = JsonReaderEx(`in`) + reader.isLenient = true + return parseMap(reader, 0, 0, ArrayList(), sourceResolverFactory) +} + +private fun parseMap(reader: JsonReaderEx, + line: Int, + column: Int, + mappings: MutableList<MappingEntry>, + sourceResolverFactory: (sourceUrls: List<String>, sourceContents: List<String?>?) -> SourceResolver): SourceMap? { + reader.beginObject() + var sourceRoot: String? = null + var sourcesReader: JsonReaderEx? = null + var names: List<String>? = null + var encodedMappings: String? = null + var file: String? = null + var version = -1 + var sourcesContent: MutableList<String?>? = null + while (reader.hasNext()) { + when (reader.nextName()) { + "sections" -> throw IOException("sections is not supported yet") + "version" -> { + version = reader.nextInt() + } + "sourceRoot" -> { + sourceRoot = StringUtil.nullize(readSourcePath(reader)) + if (sourceRoot != null && sourceRoot != "/") { + sourceRoot = UriUtil.trimTrailingSlashes(sourceRoot) + } + } + "sources" -> { + sourcesReader = reader.subReader() + reader.skipValue() + } + "names" -> { + reader.beginArray() + if (reader.hasNext()) { + names = ArrayList() + do { + if (reader.peek() == JsonToken.BEGIN_OBJECT) { + // polymer map + reader.skipValue() + names.add("POLYMER UNKNOWN NAME") + } + else { + names.add(reader.nextString(true)) + } + } + while (reader.hasNext()) + } + else { + names = emptyList() + } + reader.endArray() + } + "mappings" -> { + encodedMappings = reader.nextString() + } + "file" -> { + file = reader.nextNullableString() + } + "sourcesContent" -> { + reader.beginArray() + if (reader.peek() != JsonToken.END_ARRAY) { + sourcesContent = SmartList<String>() + do { + if (reader.peek() == JsonToken.STRING) { + sourcesContent.add(StringUtilRt.convertLineSeparators(reader.nextString())) + } + else if (reader.peek() == JsonToken.NULL) { + // null means source file should be resolved by url + sourcesContent.add(null) + reader.nextNull() + } + else { + logger<SourceMap>().warn("Unknown sourcesContent element: ${reader.peek().name}") + reader.skipValue() + } + } + while (reader.hasNext()) + } + reader.endArray() + } + else -> { + // skip file or extensions + reader.skipValue() + } + } + } + reader.close() + + // check it before other checks, probably it is not a sourcemap file + if (encodedMappings.isNullOrEmpty()) { + // empty map + return null + } + + if (Registry.`is`("js.debugger.fix.jspm.source.maps", false) && encodedMappings!!.startsWith(";") && file != null && file.endsWith(".ts!transpiled")) { + encodedMappings = encodedMappings.substring(1) + } + + if (version != 3) { + throw IOException("Unsupported sourcemap version: $version") + } + + if (sourcesReader == null) { + throw IOException("sources is not specified") + } + + val sources = readSources(sourcesReader, sourceRoot) + if (sources.isEmpty()) { + // empty map, meteor can report such ugly maps + return null + } + + val reverseMappingsBySourceUrl = arrayOfNulls<MutableList<MappingEntry>?>(sources.size) + readMappings(encodedMappings!!, line, column, mappings, reverseMappingsBySourceUrl, names) + + val sourceToEntries = Array<MappingList?>(reverseMappingsBySourceUrl.size) { + val entries = reverseMappingsBySourceUrl[it] + if (entries == null) { + null + } + else { + entries.sortWith(MAPPING_COMPARATOR_BY_SOURCE_POSITION) + SourceMappingList(entries) + } + } + return OneLevelSourceMap(file, GeneratedMappingList(mappings), sourceToEntries, sourceResolverFactory(sources, sourcesContent), !names.isNullOrEmpty()) +} + +private fun readSourcePath(reader: JsonReaderEx): String = PathUtil.toSystemIndependentName(reader.nextString().trim { it <= ' ' }) + +private fun readMappings(value: String, + initialLine: Int, + initialColumn: Int, + mappings: MutableList<MappingEntry>, + reverseMappingsBySourceUrl: Array<MutableList<MappingEntry>?>, + names: List<String>?) { + if (value.isEmpty()) { + return + } + + var line = initialLine + var column = initialColumn + val charIterator = CharSequenceIterator(value) + var sourceIndex = 0 + var reverseMappings: MutableList<MappingEntry> = getMapping(reverseMappingsBySourceUrl, sourceIndex) + var sourceLine = 0 + var sourceColumn = 0 + var nameIndex = 0 + var prevEntry: MutableEntry? = null + + fun addEntry(entry: MutableEntry) { + if (prevEntry != null) { + prevEntry!!.nextGenerated = entry + } + prevEntry = entry + mappings.add(entry) + } + + while (charIterator.hasNext()) { + if (charIterator.peek() == ',') { + charIterator.next() + } + else { + while (charIterator.peek() == ';') { + line++ + column = 0 + charIterator.next() + if (!charIterator.hasNext()) { + return + } + } + } + + column += Base64VLQ.decode(charIterator) + if (isSeparator(charIterator)) { + addEntry(UnmappedEntry(line, column)) + continue + } + + val sourceIndexDelta = Base64VLQ.decode(charIterator) + if (sourceIndexDelta != 0) { + sourceIndex += sourceIndexDelta + reverseMappings = getMapping(reverseMappingsBySourceUrl, sourceIndex) + } + sourceLine += Base64VLQ.decode(charIterator) + sourceColumn += Base64VLQ.decode(charIterator) + + val entry: MutableEntry + if (isSeparator(charIterator)) { + entry = UnnamedEntry(line, column, sourceIndex, sourceLine, sourceColumn) + } + else { + nameIndex += Base64VLQ.decode(charIterator) + assert(names != null) + entry = NamedEntry(names!![nameIndex], line, column, sourceIndex, sourceLine, sourceColumn) + } + reverseMappings.add(entry) + addEntry(entry) + } +} + +private fun readSources(reader: JsonReaderEx, sourceRoot: String?): List<String> { + reader.beginArray() + val sources: List<String> + if (reader.peek() == JsonToken.END_ARRAY) { + sources = emptyList() + } + else { + sources = SmartList<String>() + do { + var sourceUrl: String = readSourcePath(reader) + if (!sourceRoot.isNullOrEmpty()) { + if (sourceRoot == "/") { + sourceUrl = "/$sourceUrl" + } + else { + sourceUrl = "$sourceRoot/$sourceUrl" + } + } + sources.add(sourceUrl) + } + while (reader.hasNext()) + } + reader.endArray() + return sources +} + +private fun getMapping(reverseMappingsBySourceUrl: Array<MutableList<MappingEntry>?>, sourceIndex: Int): MutableList<MappingEntry> { + var reverseMappings = reverseMappingsBySourceUrl.get(sourceIndex) + if (reverseMappings == null) { + reverseMappings = ArrayList() + reverseMappingsBySourceUrl.set(sourceIndex, reverseMappings) + } + return reverseMappings +} + +private fun isSeparator(charIterator: CharSequenceIterator): Boolean { + if (!charIterator.hasNext()) { + return true + } + + val current = charIterator.peek() + return current == ',' || current == ';' +} + +interface MutableEntry : MappingEntry { + override var nextGenerated: MappingEntry +} + +/** + * Not mapped to a section in the original source. + */ +private data class UnmappedEntry(override val generatedLine: Int, override val generatedColumn: Int) : MappingEntry, MutableEntry { + override val sourceLine = UNMAPPED + + override val sourceColumn = UNMAPPED + + override var nextGenerated: MappingEntry by notNull() +} + +/** + * Mapped to a section in the original source. + */ +private data class UnnamedEntry(override val generatedLine: Int, + override val generatedColumn: Int, + override val source: Int, + override val sourceLine: Int, + override val sourceColumn: Int) : MappingEntry, MutableEntry { + override var nextGenerated: MappingEntry by notNull() +} + +/** + * Mapped to a section in the original source, and is associated with a name. + */ +private data class NamedEntry(override val name: String, + override val generatedLine: Int, + override val generatedColumn: Int, + override val source: Int, + override val sourceLine: Int, + override val sourceColumn: Int) : MappingEntry, MutableEntry { + override var nextGenerated: MappingEntry by notNull() +} + +// java CharacterIterator is ugly, next() impl, so, we reinvent +private class CharSequenceIterator(private val content: CharSequence) : CharIterator { + private val length = content.length + private var current = 0 + + override fun next() = content.get(current++) + + internal fun peek() = content.get(current) + + override fun hasNext() = current < length +} + +private class SourceMappingList(mappings: List<MappingEntry>) : MappingList(mappings) { + override fun getLine(mapping: MappingEntry) = mapping.sourceLine + + override fun getColumn(mapping: MappingEntry) = mapping.sourceColumn + + override val comparator = MAPPING_COMPARATOR_BY_SOURCE_POSITION +} + +private class GeneratedMappingList(mappings: List<MappingEntry>) : MappingList(mappings) { + override fun getLine(mapping: MappingEntry) = mapping.generatedLine + + override fun getColumn(mapping: MappingEntry) = mapping.generatedColumn + + override val comparator = MAPPING_COMPARATOR_BY_GENERATED_POSITION +} + diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt b/platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt new file mode 100644 index 00000000..2a9c868f --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt @@ -0,0 +1,186 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger.sourcemap + +import com.intellij.openapi.util.SystemInfo +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Url +import com.intellij.util.Urls +import com.intellij.util.containers.ObjectIntHashMap +import com.intellij.util.containers.isNullOrEmpty +import com.intellij.util.io.URLUtil +import java.io.File + +interface SourceFileResolver { + /** + * Return -1 if no match + */ + fun resolve(map: ObjectIntHashMap<Url>): Int = -1 + fun resolve(rawSources: List<String>): Int = -1 +} + +class SourceResolver(private val rawSources: List<String>, + trimFileScheme: Boolean, + baseUrl: Url?, + private val sourceContents: List<String?>?, + baseUrlIsFile: Boolean = true) { + companion object { + fun isAbsolute(path: String): Boolean = path.startsWith('/') || (SystemInfo.isWindows && (path.length > 2 && path[1] == ':')) + } + + val canonicalizedUrls: Array<Url> by lazy { + Array(rawSources.size) { canonicalizeUrl(rawSources[it], baseUrl, trimFileScheme, baseUrlIsFile) } + } + + private val canonicalizedUrlToSourceIndex: ObjectIntHashMap<Url> by lazy { + ( + if (SystemInfo.isFileSystemCaseSensitive) ObjectIntHashMap(rawSources.size) + else ObjectIntHashMap(rawSources.size, Urls.caseInsensitiveUrlHashingStrategy) + ).also { + for (i in rawSources.indices) { + it.put(canonicalizedUrls[i], i) + } + } + } + + fun getSource(entry: MappingEntry): Url? { + val index = entry.source + return if (index < 0) null else canonicalizedUrls[index] + } + + fun getSourceContent(entry: MappingEntry): String? { + if (sourceContents.isNullOrEmpty()) { + return null + } + + val index = entry.source + return if (index < 0 || index >= sourceContents!!.size) null else sourceContents[index] + } + + fun getSourceContent(sourceIndex: Int): String? { + if (sourceContents.isNullOrEmpty()) { + return null + } + return if (sourceIndex < 0 || sourceIndex >= sourceContents!!.size) null else sourceContents[sourceIndex] + } + + fun getSourceIndex(url: Url): Int = canonicalizedUrlToSourceIndex[url] + + fun getRawSource(entry: MappingEntry): String? { + val index = entry.source + return if (index < 0) null else rawSources[index] + } + + internal fun findSourceIndex(resolver: SourceFileResolver): Int { + val resolveByCanonicalizedUrls = resolver.resolve(canonicalizedUrlToSourceIndex) + return if (resolveByCanonicalizedUrls != -1) resolveByCanonicalizedUrls else resolver.resolve(rawSources) + } + + fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, localFileUrlOnly: Boolean): Int { + for (sourceUrl in sourceUrls) { + val index = canonicalizedUrlToSourceIndex.get(sourceUrl) + if (index != -1) { + return index + } + } + + if (sourceFile != null) { + return findSourceIndexByFile(sourceFile, localFileUrlOnly) + } + return -1 + } + + internal fun findSourceIndexByFile(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int { + if (!localFileUrlOnly) { + val index = canonicalizedUrlToSourceIndex.get(Urls.newFromVirtualFile(sourceFile).trimParameters()) + if (index != -1) { + return index + } + } + + if (!sourceFile.isInLocalFileSystem) { + return -1 + } + + // local file url - without "file" scheme, just path + val index = canonicalizedUrlToSourceIndex.get(Urls.newLocalFileUrl(sourceFile)) + if (index != -1) { + return index + } + + // ok, search by canonical path + val canonicalFile = sourceFile.canonicalFile + if (canonicalFile != null && canonicalFile != sourceFile) { + for (i in canonicalizedUrls.indices) { + val url = canonicalizedUrls.get(i) + if (Urls.equalsIgnoreParameters(url, canonicalFile)) { + return i + } + } + } + return -1 + } + + fun getUrlIfLocalFile(entry: MappingEntry): Url? = canonicalizedUrls.getOrNull(entry.source)?.let { if (it.isInLocalFileSystem) it else null } +} + +fun canonicalizePath(url: String, baseUrl: Url, baseUrlIsFile: Boolean): String { + var path = url + if (!FileUtil.isAbsolute(url) && !url.isEmpty() && url[0] != '/') { + val basePath = baseUrl.path + if (baseUrlIsFile) { + val lastSlashIndex = basePath.lastIndexOf('/') + val pathBuilder = StringBuilder() + if (lastSlashIndex == -1) { + pathBuilder.append('/') + } + else { + pathBuilder.append(basePath, 0, lastSlashIndex + 1) + } + path = pathBuilder.append(url).toString() + } + else { + path = "$basePath/$url" + } + } + return FileUtil.toCanonicalPath(path, '/') +} + +// see canonicalizeUri kotlin impl and https://trac.webkit.org/browser/trunk/Source/WebCore/inspector/front-end/ParsedURL.js completeURL +fun canonicalizeUrl(url: String, baseUrl: Url?, trimFileScheme: Boolean, baseUrlIsFile: Boolean = true): Url { + if (trimFileScheme && url.startsWith(StandardFileSystems.FILE_PROTOCOL_PREFIX)) { + return Urls.newLocalFileUrl(FileUtil.toCanonicalPath(VfsUtilCore.toIdeaUrl(url, true).substring(StandardFileSystems.FILE_PROTOCOL_PREFIX.length), '/')) + } + else if (baseUrl == null || url.contains(URLUtil.SCHEME_SEPARATOR) || url.startsWith("data:") || url.startsWith("blob:") || + url.startsWith("javascript:") || url.startsWith("webpack:")) { + // consider checking :/ instead of :// because scheme may be followed by path, not by authority + // https://tools.ietf.org/html/rfc3986#section-1.1.2 + // be careful with windows paths: C:/Users + return Urls.parseEncoded(url) ?: Urls.newUri(null, url) + } + else { + return doCanonicalize(url, baseUrl, baseUrlIsFile, true) + } +} + +fun doCanonicalize(url: String, baseUrl: Url, baseUrlIsFile: Boolean, asLocalFileIfAbsoluteAndExists: Boolean): Url { + val path = canonicalizePath(url, baseUrl, baseUrlIsFile) + if ((baseUrl.scheme == null && baseUrl.isInLocalFileSystem) || + asLocalFileIfAbsoluteAndExists && SourceResolver.isAbsolute(path) && File(path).exists()) { + // file:///home/user/foo.js.map, foo.ts -> /home/user/foo.ts (baseUrl is in local fs) + // http://localhost/home/user/foo.js.map, foo.ts -> /home/user/foo.ts (File(path) exists) + return Urls.newLocalFileUrl(path) + } + else if (!path.startsWith("/")) { + // http://localhost/source.js.map, C:/foo.ts webpack-dsj3c45 -> C:/foo.ts webpack-dsj3c45 + // (we can't append path suffixes unless they start with / + return Urls.parse(path, true) ?: Urls.newUnparsable(path) + } + else { + // new url from path and baseUrl's scheme and authority + val split = path.split('?', limit = 2) + return Urls.newUrl(baseUrl.scheme!!, baseUrl.authority!!, split[0], if (split.size > 1) '?' + split[1] else null) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/util.kt b/platform/script-debugger/backend/src/debugger/util.kt new file mode 100644 index 00000000..7930261d --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/util.kt @@ -0,0 +1,112 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.io.FileUtilRt +import com.intellij.openapi.util.registry.Registry +import com.intellij.util.concurrency.AppExecutorUtil +import com.intellij.util.io.CharSequenceBackedByChars +import com.intellij.util.io.addChannelListener +import io.netty.buffer.ByteBuf +import io.netty.channel.Channel +import org.jetbrains.annotations.PropertyKey +import java.io.File +import java.io.FileOutputStream +import java.nio.CharBuffer +import java.text.SimpleDateFormat +import java.util.concurrent.Future +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit + +internal class LogEntry(val message: CharSequence, val marker: String) { + internal val time = System.currentTimeMillis() +} + +class MessagingLogger internal constructor(debugFile: String) { + private val processFuture: Future<*> + private val queue = LinkedBlockingQueue<LogEntry>() + + init { + processFuture = ApplicationManager.getApplication().executeOnPooledThread { + val file = File(FileUtil.expandUserHome(debugFile)) + FileUtilRt.createParentDirs(file) + val out = FileOutputStream(file) + val writer = out.writer() + writer.write("[\n") + writer.flush() + val fileChannel = out.channel + + val dateFormatter = SimpleDateFormat("HH.mm.ss,SSS") + + try { + while (true) { + val entry = queue.take() + + writer.write("""{"timestamp": "${dateFormatter.format(entry.time)}", """) + val message = entry.message + writer.write("\"${entry.marker}\": ") + writer.flush() + + if (message is CharSequenceBackedByChars) { + fileChannel.write(message.byteBuffer) + } + else { + fileChannel.write(Charsets.UTF_8.encode(CharBuffer.wrap(message))) + } + + writer.write("},\n") + writer.flush() + } + } + catch (e: InterruptedException) { + } + finally { + writer.write("]") + writer.flush() + out.close() + } + } + } + + fun add(message: CharSequence, marker: String = "IN") { + // ignore Network events + if (!message.startsWith("{\"method\":\"Network.")) { + queue.add(LogEntry(message, marker)) + } + } + + fun add(outMessage: ByteBuf, marker: String = "OUT") { + val charSequence = outMessage.getCharSequence(outMessage.readerIndex(), outMessage.readableBytes(), Charsets.UTF_8) + add(charSequence, marker) + } + + fun close() { + AppExecutorUtil.getAppScheduledExecutorService().schedule(fun() { + processFuture.cancel(true) + }, 1, TimeUnit.SECONDS) + } + + fun closeOnChannelClose(channel: Channel) { + channel.closeFuture().addChannelListener { + try { + add("\"Closed\"", "Channel") + } + finally { + close() + } + } + } +} + +fun createDebugLogger(@PropertyKey(resourceBundle = Registry.REGISTRY_BUNDLE) key: String, suffix: String = ""): MessagingLogger? { + var debugFile = Registry.stringValue(key) + if (debugFile.isEmpty()) { + return null + } + + if (!suffix.isEmpty()) { + debugFile = debugFile.replace(".json", "$suffix.json") + } + return MessagingLogger(debugFile) +} diff --git a/platform/script-debugger/backend/src/debugger/values/ArrayValue.kt b/platform/script-debugger/backend/src/debugger/values/ArrayValue.kt new file mode 100644 index 00000000..ceed459a --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/ArrayValue.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +interface ArrayValue : ObjectValue { + /** + * Be aware - it is not equals to java array length. + * In case of sparse array `var sparseArray = [3, 4]; + * sparseArray[45] = 34; + * sparseArray[40999995] = "foo"; + ` * + * length will be equal to 40999995. + */ + val length: Int +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/FunctionValue.kt b/platform/script-debugger/backend/src/debugger/values/FunctionValue.kt new file mode 100644 index 00000000..2825bc1e --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/FunctionValue.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +import com.intellij.util.ThreeState +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.Scope + +interface FunctionValue : ObjectValue { + /** + * You must invoke [.resolve] to use any function value methods + */ + fun resolve(): Promise<FunctionValue> + + /** + * Returns position of opening parenthesis of function arguments. Position is absolute + * within resource (not relative to script start position). + + * @return position or null if position is not available + */ + val openParenLine: Int + + val openParenColumn: Int + + val scopes: Array<Scope>? + + /** + * Method could be called (it is normal and expected) for unresolved function. + * It must return quickly. Return [com.intellij.util.ThreeState.UNSURE] otherwise. + */ + fun hasScopes(): ThreeState = ThreeState.UNSURE +} diff --git a/platform/script-debugger/backend/src/debugger/values/IndexedVariablesConsumer.kt b/platform/script-debugger/backend/src/debugger/values/IndexedVariablesConsumer.kt new file mode 100644 index 00000000..aad2f00d --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/IndexedVariablesConsumer.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +import org.jetbrains.debugger.Variable + +abstract class IndexedVariablesConsumer { + // null if array is not sparse + abstract fun consumeRanges(ranges: IntArray?) + + abstract fun consumeVariables(variables: List<Variable>) + + open val isObsolete: Boolean + get() = false +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/ObjectValue.kt b/platform/script-debugger/backend/src/debugger/values/ObjectValue.kt new file mode 100755 index 00000000..55917c42 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/ObjectValue.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +import com.intellij.util.ThreeState +import org.jetbrains.concurrency.Obsolescent +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.EvaluateContext +import org.jetbrains.debugger.Variable +import org.jetbrains.debugger.VariablesHost + +/** + * A compound value that has zero or more properties + */ +interface ObjectValue : Value { + val className: String? + + val properties: Promise<List<Variable>> + + fun getProperties(names: List<String>, evaluateContext: EvaluateContext, obsolescent: Obsolescent): Promise<List<Variable>> + + val variablesHost: VariablesHost<ValueManager> + + /** + * from (inclusive) to (exclusive) ranges of array elements or elements if less than bucketThreshold + + * "to" could be -1 (sometimes length is unknown, so, you can pass -1 instead of actual elements size) + */ + fun getIndexedProperties(from: Int, to: Int, bucketThreshold: Int, consumer: IndexedVariablesConsumer, componentType: ValueType? = null): Promise<*> + + /** + * It must return quickly. Return [com.intellij.util.ThreeState.UNSURE] otherwise. + */ + fun hasProperties(): ThreeState = ThreeState.UNSURE + + /** + * It must return quickly. Return [com.intellij.util.ThreeState.UNSURE] otherwise. + */ + fun hasIndexedProperties(): ThreeState = ThreeState.NO +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/ObjectValueBase.kt b/platform/script-debugger/backend/src/debugger/values/ObjectValueBase.kt new file mode 100644 index 00000000..f56adc43 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/ObjectValueBase.kt @@ -0,0 +1,66 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger.values + +import com.intellij.util.SmartList +import org.jetbrains.concurrency.* +import org.jetbrains.debugger.EvaluateContext +import org.jetbrains.debugger.Variable +import org.jetbrains.debugger.VariablesHost +import java.util.* + +abstract class ObjectValueBase<VALUE_LOADER : ValueManager>(type: ValueType) : ValueBase(type), ObjectValue { + protected abstract val childrenManager: VariablesHost<VALUE_LOADER> + + override val properties: Promise<List<Variable>> + get() = childrenManager.get() + + internal abstract inner class MyObsolescentAsyncFunction<PARAM, RESULT>(private val obsolescent: Obsolescent) : ObsolescentFunction<PARAM, Promise<RESULT>> { + override fun isObsolete() = obsolescent.isObsolete || childrenManager.valueManager.isObsolete + } + + override fun getProperties(names: List<String>, evaluateContext: EvaluateContext, obsolescent: Obsolescent): Promise<List<Variable>> = properties + .thenAsync(object : MyObsolescentAsyncFunction<List<Variable>, List<Variable>>(obsolescent) { + override fun `fun`(variables: List<Variable>) = getSpecifiedProperties(variables, names, evaluateContext) + }) + + override val valueString: String? = null + + override fun getIndexedProperties(from: Int, to: Int, bucketThreshold: Int, consumer: IndexedVariablesConsumer, componentType: ValueType?): Promise<*> = rejectedPromise<Any?>() + + @Suppress("UNCHECKED_CAST") + override val variablesHost: VariablesHost<ValueManager> + get() = childrenManager as VariablesHost<ValueManager> +} + +fun getSpecifiedProperties(variables: List<Variable>, names: List<String>, evaluateContext: EvaluateContext): Promise<List<Variable>> { + val properties = SmartList<Variable>() + var getterCount = 0 + for (property in variables) { + if (!property.isReadable || !names.contains(property.name)) { + continue + } + + if (!properties.isEmpty()) { + Collections.sort(properties) { o1, o2 -> names.indexOf(o1.name) - names.indexOf(o2.name) } + } + + properties.add(property) + if (property.value == null) { + getterCount++ + } + } + + if (getterCount == 0) { + return resolvedPromise(properties) + } + else { + val promises = SmartList<Promise<*>>() + for (variable in properties) { + if (variable.value == null) { + val valueModifier = variable.valueModifier!! + promises.add(valueModifier.evaluateGet(variable, evaluateContext)) + } + } + return promises.all(properties) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/PrimitiveValue.kt b/platform/script-debugger/backend/src/debugger/values/PrimitiveValue.kt new file mode 100644 index 00000000..995e905c --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/PrimitiveValue.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +open class PrimitiveValue(type: ValueType, override val valueString: String) : ValueBase(type) { + + constructor(type: ValueType, value: Int) : this(type, Integer.toString(value)) { + } + + constructor(type: ValueType, value: Long) : this(type, java.lang.Long.toString(value)) { + } + + companion object { + val NA_N_VALUE: String = "NaN" + val INFINITY_VALUE: String = "Infinity" + + @JvmField + val NULL: PrimitiveValue = PrimitiveValue(ValueType.NULL, "null") + @JvmField + val UNDEFINED: PrimitiveValue = PrimitiveValue(ValueType.UNDEFINED, "undefined") + + val NAN: PrimitiveValue = PrimitiveValue(ValueType.NUMBER, NA_N_VALUE) + val INFINITY: PrimitiveValue = PrimitiveValue(ValueType.NUMBER, INFINITY_VALUE) + + private val TRUE = PrimitiveValue(ValueType.BOOLEAN, "true") + private val FALSE = PrimitiveValue(ValueType.BOOLEAN, "false") + + fun bool(value: String): PrimitiveValue { + return if (value == "true") TRUE else FALSE + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/StringValue.kt b/platform/script-debugger/backend/src/debugger/values/StringValue.kt new file mode 100644 index 00000000..8a1e8c48 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/StringValue.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +import org.jetbrains.concurrency.Promise + +interface StringValue : Value { + val isTruncated: Boolean + + val length: Int + + /** + * Asynchronously reloads object value with extended size limit + */ + val fullString: Promise<String> +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/Value.kt b/platform/script-debugger/backend/src/debugger/values/Value.kt new file mode 100755 index 00000000..79846ce9 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/Value.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +/** + * An object that represents a VM variable value (compound or atomic). + */ +interface Value { + val type: ValueType + + /** + * @return a string representation of this value + */ + val valueString: String? +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/ValueBase.kt b/platform/script-debugger/backend/src/debugger/values/ValueBase.kt new file mode 100644 index 00000000..2d62611a --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/ValueBase.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +abstract class ValueBase(override val type: ValueType) : Value
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/ValueManager.kt b/platform/script-debugger/backend/src/debugger/values/ValueManager.kt new file mode 100644 index 00000000..4d7eb248 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/ValueManager.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +import org.jetbrains.concurrency.Obsolescent +import java.util.concurrent.atomic.AtomicInteger + +/** + * The main idea of this class - don't create value for remote value handle if already exists. So, + * implementation of this class keep map of value to remote value handle. + * Also, this class maintains cache timestamp. + + * Currently WIP implementation doesn't keep such map due to protocol issue. But V8 does. + */ +abstract class ValueManager() : Obsolescent { + private val cacheStamp = AtomicInteger() + @Volatile private var obsolete = false + + open fun clearCaches() { + cacheStamp.incrementAndGet() + } + + fun getCacheStamp(): Int = cacheStamp.get() + + final override fun isObsolete(): Boolean = obsolete + + fun markObsolete() { + obsolete = true + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/ValueType.kt b/platform/script-debugger/backend/src/debugger/values/ValueType.kt new file mode 100644 index 00000000..591e827b --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/ValueType.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +private val VALUE_TYPES = ValueType.values() + +/** + * Don't forget to update NashornDebuggerSupport.ValueType and DebuggerSupport.ts respectively also + */ +enum class ValueType { + OBJECT, + NUMBER, + STRING, + FUNCTION, + BOOLEAN, + BIGINT, + + ARRAY, + NODE, + + UNDEFINED, + NULL, + SYMBOL; + + /** + * Returns whether `type` corresponds to a JsObject. Note that while 'null' is an object + * in JavaScript world, here for API consistency it has bogus type [.NULL] and is + * not a [ObjectValue] + */ + val isObjectType: Boolean + get() = this == OBJECT || this == ARRAY || this == FUNCTION || this == NODE + + companion object { + fun fromIndex(index: Int): ValueType = VALUE_TYPES.get(index) + } +}
\ No newline at end of file |