jmoenig / Snap

a visual programming language inspired by Scratch
http://snap.berkeley.edu
GNU Affero General Public License v3.0
1.5k stars 745 forks source link

Is there a way for JavaScript code to listen for clicks on the stop sign? #1792

Closed ToonTalk closed 6 years ago

ToonTalk commented 7 years ago

I have JavaScript code that keeps running even when the user clicks the stop sign. Is there a way I can add a listener to the stop sign click?

brianharvey commented 7 years ago

I hope not; the stop sign should stop everything, even malicious runaway programs. My offhand guess, though, is that your code is never yielding, and so any listener you set up would never get to run anyway. If you hold down the stop sign button for a few seconds, it should stop everything. If that doesn't do it, post your code.

jmoenig commented 7 years ago

Hi Ken (@ToonTalk ), attaching user defined events to the red stop button is somewhat of a problematic design issue. Since the early days of Scratch hardware people have been asking for this, so they can stop all motors of a robot, and that makes perfect sense. After all, if you press the red stop button you'd expect your (tethered) device that's controlled by Snap to also stop. The reason we do have a "when the green flag is clicked" event, but not a "when the red stop sign is clicked" one is simply that there could be long running scripts attached to it, or scripts that somehow change something or - worse - throw an error, and then users might not be able to actually stop their project again.

So, if you're using JS functions, you can, of course, replace the stop-button's clicked() method with your own one, and then let it do something more. But that's more or less hacking the system, I guess.

That said, do you have any idea, how we might address the stopping problem? In general I'd be interested in supporting this...

cycomachead commented 7 years ago

That said, do you have any idea, how we might address the stopping problem? In general I'd be interested in supporting this...

One possible idea is to have 2 events, one fired "on stop" and one fired a short time after called "kill any bad acting stop scripts". This slightly ruins the semantics of the stop block in some cases, but only in edge cases where things would be iffy, at least assuming you only use it for hardware and cleanup scripts.

The other idea is to allow a stop handler to only invoke certain functions, but this seems problematic as well.

As far as the original question, this sounds like an infinite loop to me.

brianharvey commented 7 years ago

I thought about timing a stop script, but the problem is that if it's trying to stop hardware it'll send a request to a localhost web server and then wait however long it takes to get a reply. That could in principle take minutes. You may think if it's waiting, it yielded, so no problem, but it could do this in an infinite loop, so it's not as bad as freezing up the whole system, but it still goes on forever.

ToonTalk commented 7 years ago

Thanks everyone.

In my case I am forced to use continuations that are called when some web service replies. Clicking the stop sign won't stop the callback from running (unless it knows that the stop sign has been clicked since the web service was contacted. Another example is using the browser's speech synthesis. One often needs to do things when the speech is finished so again a callback that will run even if the user clicks stop before the speech is finished. To make matters worse sometimes it is natural for one of these callbacks to make another use of a web service so the program keeps running in this way.

Regarding solutions why not after doing all the current stop actions to then inside a try/catch to fire any stop listeners? Long running scripts are still a problem, but they are problem with any user JavaScript including JavaScript blocks. I guess there is a problem as Brian mentions that if one wanted to turn off motors that if it used the current http:// block that won't work. But see #1718.

cycomachead commented 7 years ago

I thought about timing a stop script, but the problem is that if it's trying to stop hardware it'll send a request to a localhost web server and then wait however long it takes to get a reply.

Right now, when you stop a script it stops in progress http requests, so I don't think this matters. There's no waiting or yielding or any type of callback that will happen, at least for the built in http block. If you needed to actually process the response, I guess we're preventing that, but if all you need to do is send a command, then that should work just fine.

With a custom JS block, I think users might need to manually call .abort() or null out the XHR, but it should work.

brianharvey commented 7 years ago

Could we invent a convention that Snap! keeps a list of pending callbacks and zaps them all when you click stop?

cycomachead commented 7 years ago

Sure, but that doesn’t really solve the case of needing to stop hardware, where you need to execute something new on Stop.

-- Michael Ball From my iPhone michaelball.co

On Jul 7, 2017, at 12:29 PM, Brian Harvey notifications@github.com wrote:

Could we invent a convention that Snap! keeps a list of pending callbacks and zaps them all when you click stop?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

jmoenig commented 7 years ago

the convention is that callbacks don't trigger anything directly in Morphic, but only change the state of a variable that gets polled by Morphic's stepping mechanism, which in turns initiates an appropriate action, or decides not to when the time comes.

towerofnix commented 7 years ago

Could we invent a convention that Snap! keeps a list of pending callbacks and zaps them all when you click stop?

Theoretically; but in practice it takes a little more to actually stop things. For example, if you've started an <audio> element from a JS function, you actually need to call .stop() on that element for it to be stopped. Zapping the on-ended callback will do just that (the callback won't be activated); it won't actually stop the sound!

(This is a comparable problem to what @cycomachead noted.)

So, if you're using JS functions, you can, of course, replace the stop-button's clicked() method with your own one, and then let it do something more. But that's more or less hacking the system, I guess.

Note that you should consider running its old .clicked() method as well (you do still want it to stop Snap! scripts like normal, right?); so, technically, you'd do this:

var oldClicked = stopSign.clicked;
stopSign.clicked = function() {

  // ..Your code here..

  oldClicked.call(this);
};
gdavid04 commented 6 years ago

Why not add an on project loaded, on stop clicked and an on paused an on unpaused hat block? And maybe some emergency stop that wouldn't trigger the on stop clicked blocks after stopping. And a block for unpausing and starting.

brianharvey commented 6 years ago

You know, I'm always the one saying you should be able to do anything in a program that the UI can do, and yet this idea really scares me. "On pause" is even worse than "on stop," because when you click pause you really want it to pause right now before the program has time to change its state. But "on stop" is scary too, for the obvious sorcerer's-apprentice reason. I've actually wanted "on project loaded" but Jens doesn't like it because you might find a malicious project that someone has published, and now at least you get to examine it before it runs. Maybe someday we'll compromise with an "on first green flag" block that would also superimpose a big green flag on the stage.

cycomachead commented 6 years ago

We have when [boolean]

You can do a lot of this manually by writing a custom JS block that wraps Snap!s internal Stop call.

Not that we should encourage it, but this is all possible today.

-- Michael Ball From my iPhone michaelball.co

On Jun 5, 2018, at 3:17 PM, Brian Harvey notifications@github.com wrote:

You know, I'm always the one saying you should be able to do anything in a program that the UI can do, and yet this idea really scares me. "On pause" is even worse than "on stop," because when you click pause you really want it to pause right now before the program has time to change its state. But "on stop" is scary too, for the obvious sorcerer's-apprentice reason. I've actually wanted "on project loaded" but Jens doesn't like it because you might find a malicious project that someone has published, and now at least you get to examine it before it runs. Maybe someday we'll compromise with an "on first green flag" block that would also superimpose a big green flag on the stage.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

gdavid04 commented 6 years ago

I've actually wanted "on project loaded" but Jens doesn't like it because you might find a malicious project that someone has published, and now at least you get to examine it before it runs.

Ask the user if the project wants to run JavaScript, fire HTTP request or start itself (i also said a block that can unpause and one that can start the project) in the on start function. If the user allows it, it's allowed in all on start scripts. (it would first let the user see the script if it contains any of these blocks - maybe in a dialog similar to the custom block editor, but read only and with a yes and a no button)

"On pause" is even worse than "on stop," because when you click pause you really want it to pause right now before the program has time to change its state

The built-in pause button could be used in a project for example as a way to pause a game, resume it and the game could show a pause menu when paused, not just freezing everything.

There could be an "emergency" version of the pause and the stop buttons that won't fire the events and forces all scripts to pause / stop. Any script somehow starting after stop or pause would get stopped / paused immediately until the user hits the start button (or the "emergency" stop / pause button again).

ToonTalk commented 6 years ago

The following seems to be working. Is this a good solution?

let original_stopAllScripts = morph_ide.stopAllScripts.bind(morph_ide);
morph_ide.stopAllScripts = function () {
          console.log("doing stuff before everything stops");
          original_stopAllScripts();
 };

Note that perhaps stage.fireStopAllEvent would be better for wrapping since the 'esc' key triggers it as well as the stop sign. But the stage can change after Snap! starts when a project is loaded. And also stopAllScripts does more:

if (this.stage.enableCustomHatBlocks) {
        this.stage.threads.pauseCustomHatBlocks =
            !this.stage.threads.pauseCustomHatBlocks;
    }

What is that all about? And why does 'esc' and the stop sign behave differently?

ToonTalk commented 6 years ago

Another example of the motivation for some sort of feature like this is the Snap! text-to-speech library

image

It can't be stopped. Though the following does stop when the stop sign is clicked.

image

jmoenig commented 6 years ago

You're right, Ken, these are good examples in favor of being able to let a script react to a stop "event". I'm going to run an experiment that will let us react to a "when I am stopped" event by executing a single step (atom) before actually stopping. This should be enough to turn off hardware and kill JS scripts without opening a sorcerer's apprentice can of worms.

towerofnix commented 6 years ago

What is that all about? And why does 'esc' and the stop sign behave differently?

@ToonTalk "pauseCustomHatBlocks" is a flag that enables or disables the "when" block. So if the stop sign is pressed, it prevents "when" blocks from running. That's so that the project definitely stops when you press the stop sign. The "escape" key is more of a shortcut for stopping the running scripts immediately (ala "oh no, I made a recursive block / loop that goes forever, and want to stop it now"), in my opinion, although obviously I'm not the one who made escape and the stop sign work differently so, Jens? :P

jmoenig commented 6 years ago

Thanks, @towerofnix , for the excellent explanation!

brianharvey commented 6 years ago

@gdavid04: In general, I'm against changing the user-visible meaning of built-in controls (such as pause). That just confuses the user. Remember that you can put a button-shaped sprite on the stage and give that the special meaning you want. (One of the things on my long-term list that Jens hates is the ability to add things to the menu bar, which would be even closer to what you want.) The stop-hardware thing that Ken wants is okay because it enhances, rather than preempting, what the user expects. (That is, a typical user would expect the robot to stop when clicking the stop sign.)

gdavid04 commented 6 years ago

Being able to customize the menu bar would be even better. A when stop clicked and a when project loaded hat block would be cool anyways. Maybe limit the execution time of these hat blocks. Also why not add an ability to hide the menu bar when stage is in fullscreen mode?

jguille2 commented 6 years ago

I only comment (in case it's useful) that we have a 'hideControls' flag.

So, if you want to fire a project in fullscreen without control buttons, you can use it.

Just like ... https://snap.berkeley.edu/snapsource/snap.html#present:hideControls&Username=jens&ProjectName=Space%20Invaders

gdavid04 commented 6 years ago

The stop-hardware thing that Ken wants is okay because it enhances, rather than preempting, what the user expects. (That is, a typical user would expect the robot to stop when clicking the stop sign.)

What about pause? What a typical user would except from pause? The robot stops it's motors until unpaused. What happens instead? The robot won't stop the motors and continues moving. (in some cases this could also damage the robot, for example, breaks an arm that was rotating when pause was pressed)

Same as stop, we should be able to run a few instructions before actually stopping / pausing.

jguille2 commented 6 years ago

Ups! I think we are mixing here too many different things ... and therefore, following this path we will go nowhere ...

Let me show you some comments...

And I stop my roll... but I insist that it is not a good idea to mix all these casuistics to discuss a central element in the control of Snap! programming

ToonTalk commented 6 years ago

I agree about modifying the "core". As you point out stopping robots is ambiguous. But note the title of this issue - if the robotics primitives are written in JavaScript then maybe the author of the primitives knows what should happen when they are "stopped". Or if not then maybe the author of the robotics project knows what should happen and wants to express that using a new "when" block.

I now believe there are two sub-issues: (1) what hooks to give the JavaScript programmer to respond to stop, pause, resume events and (2) whether new "when" blocks are needed for the Snap! programmer who is using non-core functionality such as speech synthesis, robotics, etc.

A project-defined panic button is not a good solution to stopping lots of queued up speech utterances. Users will expect the stop sign to stop the speech just as it does for core sounds. Two stop buttons are likely to confuse users.

Regarding (1) wrapping stopAllScripts is hackish (depending upon internals that may change) and awkward but in my case adequate.

Regarding (2) I see how these "when" commands could be misused to cause problems. There have been suggestions to limit the risk but I'm not convinced they are needed. Maybe someone should generate some scenarios where bad things happen if a user misuses a when stopping block.

Regarding bad things happening often the page refresh is all that is needed. But it would be nice if the dialog saying do you really want to quit also offered to save things...

bromagosa commented 6 years ago

Why not just decorate ThreadManager >> stopAll?

var stage = this.parentThatIsA(StageMorph);

if (!stage.threads.oldStopAll) {
    stage.threads.oldStopAll = stage.threads.stopAll;
    stage.threads.stopAll = function () {
        this.oldStopAll();
        alert("Potentially dangerous code that could create havoc.");
    };
}

Attach a JS block that runs this to your initialization script and you're done:

untitled script pic

ToonTalk commented 6 years ago

It depends upon whether the JavaScript programmer knows how to implement robot.preventFromFallingFromTheTable or whether it is the Snap! programmer who is using some blocks provided by the JavaScript programmer. The Snap! programmer knows what the robot is doing and how it is built.

jmoenig commented 6 years ago

Okay, I've just added an - as of now experimental - feature that addresses this issue: d11ba70ac6840781f064cbf1e2866e2740207a15

You can test this at the dev url: http://snap.berkeley.edu/snapsource/dev

make sure to force a hard-reload to get the feature.

There's a new option in the dropdown menu of the When I am ... hat block letting you specify a script that executes when the user presses the red stop button (stopped). Scripts attached to such hat blocks will run when the user physically clicks on the red stop button or hits the esc-key on the keyboard for exactly one single atomic step ("frame") and then terminate. This means that e.g. only the first pass of a loop gets run. You can, however, use a WARP around the loop to force more iterations. Any broadcasts or forked threads will likewise run for exactly a single step within the main one (i.e. the whole operation lasts exactly one global atom). This is very experimental and might not make it into the release after all.

For now I'd be interested in your feedback, whether this addresses the issue at all.

Thanks!

cycomachead commented 6 years ago

Closing this, now since the feature was released. :)