Dialplan: Hangup channel from dialplan of another channel

Is it possible, without using AGI, ARI, or SHELL(), to hangup a channel from a dialplan command of another channel? I’m playing around with parallel channels launched by originate, and I’d like a way to terminate the originated calls on initiating call hangup. I’m specifically looking for confirmation that the following two options are not possible:

  • Issuing a (non shell) command in the dialplan of one channel that hangs up another channel
  • Having a outgoing dial stop before answer or timeout based on the state of a condition.

Based on my research I don’t believe these two top options are possible. I’d like to rule them out as simple options before investigating implementing more complex or higher security risk options like AGI, ARI, or SHELL(). Thanks.

Okay, of course it’s only after asking for help that you begin to find solutions. I’ve actually identified two solutions to this problem:

  1. Originate a Local/ call and use logic in the Local/context to drive the behavior. I have a PoC that is able to hangup as needed based on using exten states combined with WaitCond and GotoIf. Not pretty, but it works.

  2. Use ChannelRedirect() to redirect the call to a hangup extension. That’s a nifty little tidbit I found over in this thread about kicking the last person in a conference: End conference when only one participant remains - Applications / Modules - FreePBX Community Forums Not sure if it works for all channel states (i.e. ringing) but for answered channels it works a treat.

hi you do know that when you hangup either one of the channels in originate the other channel will also hangup

Yes, in fact that’s exactly the behavior I’m utilizing to make this work.

So firstly, I want to emphasize that this is a “could I?” not a “should I?” thread. That’s why I limited the scope to the dialplan. In searching about this I found a number of threads asking about similar solutions, and general guidance to use AGI, but nothing to say if it was possible or not in the dialplan. So I’d like to use this thread for “here’s how you can”. I’ve already learned some decent “here’s why you’ll shouldn’t” points. And, since info on Asterisk like this fortunately tends to live a long time on the internet, I’ll just note it’s February 2024 and this was proof-of-concepted on Asterisk 24.

Basically how the origination solution I’ve cobbled together works is:

[Incoming call]
1) A call comes in that triggers this system
2) We originate out using
     (local/outbound_context_1,exten,outbound_context_2) as
     many times as needed (2 or more, otherwise this is massive
     unnecessary complexity).  Use the `a` option to not block.
3) Park the incoming call in a dedicated lot with `findslot => first`
        NOTE: Per discussion below this isn't required, but call parking
            brings some desirable features if transparency is a goal.

[Outgoing calls: outbound_context_1]
1) outbound_context_1 starts from the originate
2) Wait for the parked call exten to go INUSE.  This is a race condition
      check (race conditions will become a theme)
3) Answer() the channel.  This will launch outbound_context_2
4) WaitIfCondition on:
  - That the call is not parked
        - EXTENSION_STATE([first park slot exten]@[parkinglot])
              can be used for this.
  - That the exten dialed in outbound_context_2 is not ringing
        - EXTENSION_STATE([EXTEN]@outbound_context_2)
              can be used for this.
  - That where-ever the parked call goes after this (in my case
       a confbridge) is idle.
5) Check if where-ever the parked call goes after this is idle and if the
      parked call slot is idle.  If so:
  5a) wait a half-second or so and recheck to ensure we're not racing the
             other thread transitioning to the target.
  5b) If the recheck is still true, Hangup().  This will hangup
             the other end in outbound_context_2
6) Check if the exten we called in outbound_context_2 is INUSE.
        If so, Goto a context that:
  6a) Originate another local call that:
    - Unparks the parked call on one side
    - Launches wherever the parked call goes
          (In my case a ConfBridge())
  6b) Connects this side to wherever it needs to go
7) Check if wherever the calls are going is active.  If so:
  7a) WaitForCondition on the exten we dialed in outbound_context_2
            being INUSE
  7b) Goto the same context as 6
8) If we got here we've fallen through, and there's plenty of reasons
         that can happen.  Wait a small delay to not busy-loop and goto 4

[Outgoing calls: outbound_context_2]
- This is launched by the other side of the originate that
    launches outbound_context_1
- Dial a hinted exten.  Something like:
exten => 1234,hint,PJSIP/1234
exten => 1234,n,Dial(PJSIP/1234,45)
exten => 1234,n,Hangup()
- That's it.

So that’s the how, here’s the “why not”:

  • This is rife with race conditions. When testing it initially worked great with a local SIP device that signalled quickly. When I tried dialing in from a remote device with slower signalling I started losing calls in the main wait loop. The main race point is the dual condition “are the parked slot and the parked call target both idle” check. Calls can be lost here as both conditions will briefly become true as the parked call transitions, making it look like the parked call hung up.
  • We’re matching on exten state, and there are many of those. To reduce conditional complexity my implementation only uses INUSE which is likely naive. The alternative is match more states, which means heavier conditionals with more opportunities for bugs and logic errors.
  • I’m sure this isn’t performant. I’ve only tested it with one call on a otherwise idle test Asterisk instance
  • Lots of other problems I’m sure I haven’t found yet, this is a PoC.

So, I wanted to know if you could, and yea, you can. Should you? Unless you need something low-volume, fast (as in person time to solution fast), and have a reason you can’t immediately use AGI or ARI I’d say no. This is not optimal, I wouldn’t run it on a system with any volume or even one where parallel calls into this thing are reasonably expected. I’m likely going to refactor this into a ARI once I can jump the security hoops to get that going.

And finally, just because I had a heck of a time finding this in the docs, here’s how you originate a local call:

exten => s,n,Originate(Local/1234@dialin_bridge_outbound_originator_primary,exten,dialin_bridge_outbound_originator_secondary,5678,1,1,a)

This will start a call at 1234,1 in context dialin_bridge_outbound_originator_primary, and when that answers it will also start processing 5678,1 in context dialin_bridge_outbound_originator_secondary

Posted this yesterday, and although the target problem solved was (nearly) continuous Automatic Speech Recognition eg. allowing ASR during MusicOnHold(), the Gosub() facade over parallel async Originate() of calls into a ConfBridge() with hangup handlers that perform ChannelRedirect() to ConfKick() might provide some ideas for you:

Busy thread! Welcome to the forums!

Friendly FYI but that part is hard to scroll around and read.

Per the Parking lots, you might be able to cut those out entirely, as the Conference could serve a similar function, with the original caller listening to music while they wait but immediately talking when others enter the Conference.

Not sure why you need the exten state but also not clear on your main goal.

On the Asterisk CLI, core show application <some_app> is your friend, eg. core show application Originate. EDIT: A link to the docs for Originate().

That’s a good point. Dropping the main call directly into the conference would cut out a potential race in the “caller hung up” check. I personally am using it because it gives me the ability to play ringing, not music, while the call is parked (which for my case is desirable), and a park timeout that sends the call to voicemail if none of the outgoing calls answer. Nice features if the goal is to make the system be as transparent as possible.

The exten state is how the outgoing call answered / incoming call hung up tracks if the parked call is still there and if the dial on the other side of the local channel answered or not. I’m assuming there may be a better way for one end of a local channel to know if the other end answered or not, but I wasn’t able to find it looking over the function list.

Mmmhmm, and on that page the Local/[exten]@context syntax isn’t documented. All the other args are, but the first one is just tech_data - Channel technology and data for creating the outbound channel. For example, SIP/1234.. I’ve found the documentation on Local channels to be rather light, I pieced my understanding of this together through config snippits in posts like this and simple trial and error. If there is a authoritative this is a local channel and here is how it works page I haven’t found it, it could use more incoming links.

I’ve not been using the core show help, on my box; it just shows Not available (I likely hardened it out, I’m rebuilding a prod box). Is that different from the docs linked? I’ve been referencing Asterisk 20 Documentation - Asterisk Documentation heavily.

[EDIT]
Found the core_local docs: asterisk/main/core_local.c at master · asterisk/asterisk · GitHub Not sure where that’s rendered in docs.asterisk.org, I’ll fork another thread.
[/EDIT]

Yea, looking at it again I agree. I wasn’t expecting the horozontal scrolling, usually when I use triple backtics on other sites I still get line wrapping. I typed this out quick this morning before I had to run to work and didn’t realize the formatting was bad, I’ll clean it up.

Re-reading my post from this morning when reformatting I realized I completely glossed over this. In my steps I say:

[Check] That the call is not parked
and
[Check] That the exten dialed in outbound_context_2 is not ringing

Which I accomplished with

EXTENSION_STATE([first parked spot exten]@[parking_lot])
and
EXTENSION_STATE([EXTEN]@outbound_context_2)

respectively.

That’s where exten state comes into play. Sorry about that. I was rushed when I wrote my here is what I got working post.

No worries, clicked the diagonal arrows, and all was well (not sure why those didn’t jump out so obviously before.)

If you generate a ringing sound into a wav file and pad with some silence, then you can set that as your MusicOnHold() class for the Conference Bridge’s music_on_hold_class value. (Sounds a little hacky at first but there’s other fun things you can do in musiconhold.conf with external apps, URLs, and more.)

From the rest of the description, and again thank you for the additional details, if you were to shift from Park() – PUN INTENDED :cowboy_hat_face: ! – to “just” ConfBridge(), then it seems like the only loop you might need is to time out the conference after X seconds of waiting for others to join with the primary caller (so you can send the “lonely” OG caller to voicemail.) For that loop, you could fire it off with one additional Originate() between your [Incoming call] steps 1 and 2, and in that loop include a Wait(X) before you look at CONFBRIDGE_INFO function output and either ChannelRedirect() or ConfKick() (or Hangup() the loop) accordingly.

Curious if you find the pngnpbx-abc-itm context of the Always Be Conferencing dialplan file useful for your purposes: always-be-conferencing/extensions-pngnpbx-abc-v20m.ael2 at master · chrsmj/always-be-conferencing · GitHub – written in AEL and not standard CONF format – but other than the “lonely” caller scenario, it does most of what you describe, and a bit more as described on the Yurt.tel website (a hosted service that runs on ABC.)

If you generate a ringing sound into a wav file and pad with some silence, then you can set that as your MusicOnHold() class for the Conference Bridge’s music_on_hold_class value. (Sounds a little hacky at first but there’s other fun things you can do in musiconhold.conf with external apps, URLs, and more.)

I briefly considered that but I don’t like the idea of faking the signalling with audio. I want this system to be overall as transparent as possible. Don’t get me wrong I’ll absolutely play ringing to telemarketers and unknown callers, but on this use I still haven’t gotten over the “hacky” factor and want to keep the signalling correct. At least as of right now.

I’ve looked over that ABC dialplan a couple times but it’s pretty big, I haven’t fully grasped it yet. I’ve also changed focus to lifting and shifting my old Asterisk server, it’s long overdue for maintenance so I’m using this project as motivation to redo it. Once I get the new server doing the job of the old server I’m going to pick this back up. I’ll probably look into the REST API as well as I’m thinking this might be a lot easier in a sidecar application than dialplan, and my new deployment setup will support that.

After including the ABC file in your extensions.ael file, you can access the Insta Teams Mode from one line in your extensions.conf, for example:

exten => 222486,1,Goto(pngnpbx-abc-itm,s,1)

Then when you dial A-B-C-I-T-M (222486) from your desk phone:

  1. Enter your extension number at the prompt, followed by # key (a good default “access number” is the same as your extension number.)
  2. When you hear the “please leave your message after the tone” voicemail greeting, press * key.
  3. Enter your voicemail password and press #.
  4. At the ABC ITM IVR, press 5 to add numbers you wish to dial into your conference. Repeat as necessary.
  5. To start dialing them, from the ABC ITM IVR, press 1, record your invitational announcement, and then wait for them to answer and accept your invitation (they need to press 1 to join.)

Step #4 is only necessary when you want to add/change/remove numbers. If the conference attendees will be the same next time, then you can skip this step in the future. However, on subsequent future runs, in place of this step #4 will be an option to select your Yurt number (on the first run, it defaults to Yurt #1.)

Also, if your desk phone supports it, eg. Polycoms, there are helpful “visual” caller ID displays that help navigate the menus on-screen.