| /* |
| * |
| * AT chat library with GLib integration |
| * |
| * Copyright (C) 2008-2011 Intel Corporation. All rights reserved. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 as |
| * published by the Free Software Foundation. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <string.h> |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include <errno.h> |
| #include <unistd.h> |
| #include <sys/signalfd.h> |
| #include <sys/socket.h> |
| #include <netinet/in.h> |
| #include <arpa/inet.h> |
| |
| #include <glib.h> |
| #include <gatchat.h> |
| #include <gattty.h> |
| #include <gatppp.h> |
| |
| #define IFCONFIG_PATH "/sbin/ifconfig" |
| |
| static const char *none_prefix[] = { NULL }; |
| static const char *cfun_prefix[] = { "+CFUN:", NULL }; |
| static const char *creg_prefix[] = { "+CREG:", NULL }; |
| static const char *cgreg_prefix[] = { "+CGREG:", NULL }; |
| |
| static gchar *option_ip = NULL; |
| static gint option_port = 0; |
| static gchar *option_modem = NULL; |
| static gchar *option_control = NULL; |
| static gint option_cid = 0; |
| static gchar *option_apn = NULL; |
| static gint option_offmode = 0; |
| static gboolean option_legacy = FALSE; |
| static gchar *option_username = NULL; |
| static gchar *option_password = NULL; |
| static gchar *option_pppdump = NULL; |
| static gboolean option_bluetooth = FALSE; |
| static gboolean option_acfc = FALSE; |
| static gboolean option_pfc = FALSE; |
| |
| static GAtPPP *ppp; |
| static GAtChat *control; |
| static GAtChat *modem; |
| static GMainLoop *event_loop; |
| |
| enum state { |
| STATE_NONE = 0, |
| STATE_REGISTERING, |
| STATE_ATTACHING, |
| STATE_ACTIVATING |
| }; |
| |
| static int state = 0; |
| static int oldmode = 0; |
| |
| static void gsmdial_debug(const char *str, void *data) |
| { |
| g_print("%s: %s\n", (const char *) data, str); |
| } |
| |
| static gboolean quit_eventloop(gpointer user_data) |
| { |
| g_main_loop_quit(event_loop); |
| return FALSE; |
| } |
| |
| static void power_down(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| g_main_loop_quit(event_loop); |
| } |
| |
| static void kill_ppp(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| g_print("kill_ppp: %d\n", ok); |
| |
| if (ok == FALSE) |
| return; |
| |
| g_at_ppp_unref(ppp); |
| ppp = NULL; |
| } |
| |
| static void ppp_suspend_ath0(gpointer user_data) |
| { |
| g_at_chat_resume(modem); |
| g_at_chat_send(modem, "ATH0", none_prefix, kill_ppp, NULL, NULL); |
| } |
| |
| static void resume_ppp(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| g_print("resume_ppp: %d\n", ok); |
| |
| if (ok == FALSE) |
| return; |
| |
| g_at_chat_suspend(modem); |
| g_at_ppp_resume(ppp); |
| } |
| |
| static void ppp_suspend_ato0(gpointer user_data) |
| { |
| g_at_chat_resume(modem); |
| g_at_chat_send(modem, "ATO0", none_prefix, resume_ppp, NULL, NULL); |
| } |
| |
| static gboolean signal_cb(GIOChannel *channel, GIOCondition cond, gpointer data) |
| { |
| static int terminated = 0; |
| int signal_fd = GPOINTER_TO_INT(data); |
| struct signalfd_siginfo si; |
| ssize_t res; |
| |
| if (cond & (G_IO_NVAL | G_IO_ERR)) |
| return FALSE; |
| |
| res = read(signal_fd, &si, sizeof(si)); |
| if (res != sizeof(si)) |
| return FALSE; |
| |
| switch (si.ssi_signo) { |
| case SIGINT: |
| case SIGTERM: |
| if (terminated == 0) { |
| g_timeout_add_seconds(10, quit_eventloop, NULL); |
| |
| if (ppp == NULL) { |
| char buf[64]; |
| sprintf(buf, "AT+CFUN=%u", option_offmode); |
| g_at_chat_send(control, buf, none_prefix, |
| power_down, NULL, NULL); |
| } else |
| g_at_ppp_shutdown(ppp); |
| } |
| |
| terminated++; |
| break; |
| case SIGUSR1: |
| if (ppp == NULL) |
| break; |
| |
| g_at_ppp_set_suspend_function(ppp, ppp_suspend_ato0, NULL); |
| g_at_ppp_suspend(ppp); |
| break; |
| case SIGUSR2: |
| if (ppp == NULL) |
| break; |
| |
| g_at_ppp_set_suspend_function(ppp, ppp_suspend_ath0, NULL); |
| g_at_ppp_suspend(ppp); |
| break; |
| default: |
| break; |
| } |
| |
| return TRUE; |
| } |
| |
| static gboolean at_util_parse_reg_unsolicited(GAtResult *result, |
| const char *prefix, int *status, |
| int *lac, int *ci, int *tech) |
| { |
| GAtResultIter iter; |
| int s; |
| int l = -1, c = -1, t = -1; |
| const char *str; |
| |
| g_at_result_iter_init(&iter, result); |
| |
| if (g_at_result_iter_next(&iter, prefix) == FALSE) |
| return FALSE; |
| |
| if (g_at_result_iter_next_number(&iter, &s) == FALSE) |
| return FALSE; |
| |
| if (g_at_result_iter_next_string(&iter, &str) == TRUE) |
| l = strtol(str, NULL, 16); |
| else |
| goto out; |
| |
| if (g_at_result_iter_next_string(&iter, &str) == TRUE) |
| c = strtol(str, NULL, 16); |
| else |
| goto out; |
| |
| g_at_result_iter_next_number(&iter, &t); |
| |
| out: |
| if (status) |
| *status = s; |
| |
| if (lac) |
| *lac = l; |
| |
| if (ci) |
| *ci = c; |
| |
| if (tech) |
| *tech = t; |
| |
| return TRUE; |
| } |
| |
| static gboolean at_util_parse_reg(GAtResult *result, const char *prefix, |
| int *mode, int *status, |
| int *lac, int *ci, int *tech) |
| { |
| GAtResultIter iter; |
| int m, s; |
| int l = -1, c = -1, t = -1; |
| const char *str; |
| |
| g_at_result_iter_init(&iter, result); |
| |
| while (g_at_result_iter_next(&iter, prefix)) { |
| g_at_result_iter_next_number(&iter, &m); |
| |
| /* Sometimes we get an unsolicited CREG/CGREG here, skip it */ |
| if (g_at_result_iter_next_number(&iter, &s) == FALSE) |
| continue; |
| |
| if (g_at_result_iter_next_string(&iter, &str) == TRUE) |
| l = strtol(str, NULL, 16); |
| else |
| goto out; |
| |
| if (g_at_result_iter_next_string(&iter, &str) == TRUE) |
| c = strtol(str, NULL, 16); |
| else |
| goto out; |
| |
| g_at_result_iter_next_number(&iter, &t); |
| |
| out: |
| if (mode) |
| *mode = m; |
| |
| if (status) |
| *status = s; |
| |
| if (lac) |
| *lac = l; |
| |
| if (ci) |
| *ci = c; |
| |
| if (tech) |
| *tech = t; |
| |
| return TRUE; |
| } |
| |
| return FALSE; |
| } |
| |
| static gboolean execute(const char *cmd) |
| { |
| int status; |
| |
| status = system(cmd); |
| if (status < 0) { |
| g_print("Failed to execute command: %s\n", strerror(errno)); |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| static void ppp_connect(const char *iface, const char *local, const char *peer, |
| const char *dns1, const char *dns2, |
| gpointer user_data) |
| { |
| char buf[512]; |
| |
| /* print out the negotiated address and dns server */ |
| g_print("Network Device: %s\n", iface); |
| g_print("IP Address: %s\n", local); |
| g_print("Peer IP Address: %s\n", peer); |
| g_print("Primary DNS Server: %s\n", dns1); |
| g_print("Secondary DNS Server: %s\n", dns2); |
| |
| if (getuid() != 0) { |
| g_print("Need root privilege to config PPP interface\n"); |
| return; |
| } |
| |
| snprintf(buf, sizeof(buf), "%s %s up", IFCONFIG_PATH, iface); |
| execute(buf); |
| |
| snprintf(buf, sizeof(buf), "%s %s %s pointopoint %s", IFCONFIG_PATH, |
| iface, local, peer); |
| execute(buf); |
| } |
| |
| static void no_carrier_notify(GAtResult *result, gpointer user_data) |
| { |
| char buf[64]; |
| |
| if (option_bluetooth) { |
| g_main_loop_quit(event_loop); |
| return; |
| } |
| |
| sprintf(buf, "AT+CFUN=%u", option_offmode); |
| g_at_chat_send(control, buf, none_prefix, power_down, NULL, NULL); |
| } |
| |
| static void ppp_disconnect(GAtPPPDisconnectReason reason, gpointer user_data) |
| { |
| g_print("PPP Link down: %d\n", reason); |
| |
| g_at_ppp_unref(ppp); |
| ppp = NULL; |
| |
| if (option_modem == NULL) |
| g_at_chat_set_debug(modem, gsmdial_debug, ""); |
| else |
| g_at_chat_set_debug(modem, gsmdial_debug, "Modem"); |
| |
| g_at_chat_register(modem, "NO CARRIER", no_carrier_notify, |
| FALSE, NULL, NULL); |
| g_at_chat_resume(modem); |
| } |
| |
| static void connect_cb(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| GAtIO *io; |
| |
| if (!ok) { |
| g_print("Unable to define context\n"); |
| exit(1); |
| } |
| |
| /* get the data IO channel */ |
| io = g_at_chat_get_io(modem); |
| |
| /* |
| * shutdown gatchat or else it tries to take all the input |
| * from the modem and does not let PPP get it. |
| */ |
| g_at_chat_suspend(modem); |
| |
| /* open ppp */ |
| ppp = g_at_ppp_new(); |
| if (ppp == NULL) { |
| g_print("Unable to create PPP object\n"); |
| exit(1); |
| } |
| g_at_ppp_set_debug(ppp, gsmdial_debug, "PPP"); |
| |
| g_at_ppp_set_credentials(ppp, option_username, option_password); |
| |
| g_at_ppp_set_acfc_enabled(ppp, option_acfc); |
| g_at_ppp_set_pfc_enabled(ppp, option_pfc); |
| |
| /* set connect and disconnect callbacks */ |
| g_at_ppp_set_connect_function(ppp, ppp_connect, NULL); |
| g_at_ppp_set_disconnect_function(ppp, ppp_disconnect, NULL); |
| |
| /* open the ppp connection */ |
| g_at_ppp_open(ppp, io); |
| |
| if (option_pppdump) |
| g_at_ppp_set_recording(ppp, option_pppdump); |
| } |
| |
| static void at_cgdcont_cb(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| char buf[64]; |
| |
| if (!ok) { |
| g_print("Unable to define context\n"); |
| exit(1); |
| } |
| |
| if (option_legacy == TRUE) |
| sprintf(buf, "ATD*99***%u#", option_cid); |
| else |
| sprintf(buf, "AT+CGDATA=\"PPP\",%u", option_cid); |
| |
| g_at_chat_send(modem, buf, none_prefix, connect_cb, NULL, NULL); |
| } |
| |
| static void setup_context(int status) |
| { |
| char buf[1024]; |
| int len; |
| |
| state = STATE_ACTIVATING; |
| |
| g_print("Registered to GPRS network, roaming=%s\n", |
| status == 5 ? "true" : "false"); |
| |
| len = sprintf(buf, "AT+CGDCONT=%u,\"IP\"", option_cid); |
| snprintf(buf + len, sizeof(buf) - len - 3, ",\"%s\"", option_apn); |
| g_at_chat_send(control, buf, none_prefix, at_cgdcont_cb, NULL, NULL); |
| } |
| |
| static void cgreg_notify(GAtResult *result, gpointer user_data) |
| { |
| int status, lac, ci, tech; |
| |
| if (state != STATE_ATTACHING) |
| return; |
| |
| if (at_util_parse_reg_unsolicited(result, "+CGREG:", &status, |
| &lac, &ci, &tech) == FALSE) |
| return; |
| |
| if (status != 1 && status != 5) |
| return; |
| |
| setup_context(status); |
| } |
| |
| static void cgreg_cb(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| int status, lac, ci, tech; |
| |
| if (!ok) |
| return; |
| |
| if (at_util_parse_reg(result, "+CGREG:", NULL, &status, |
| &lac, &ci, &tech) == FALSE) |
| return; |
| |
| if (status != 1 && status != 5) { |
| g_at_chat_register(control, "+CGREG:", |
| cgreg_notify, FALSE, NULL, NULL); |
| return; |
| } |
| |
| setup_context(status); |
| } |
| |
| static void attached_cb(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| if (!ok) |
| return; |
| |
| g_at_chat_send(control, "AT+CGREG?", cgreg_prefix, |
| cgreg_cb, NULL, NULL); |
| } |
| |
| static void activate_gprs(int status) |
| { |
| state = STATE_ATTACHING; |
| g_print("Registered to network, roaming=%s\n", |
| status == 5 ? "true" : "false"); |
| |
| g_print("Activating GPRS network...\n"); |
| g_at_chat_send(control, "AT+CGATT=1", none_prefix, |
| attached_cb, NULL, NULL); |
| } |
| |
| static void creg_notify(GAtResult *result, gpointer user_data) |
| { |
| int status, lac, ci, tech; |
| |
| if (state != STATE_REGISTERING) |
| return; |
| |
| if (at_util_parse_reg_unsolicited(result, "+CREG:", &status, |
| &lac, &ci, &tech) == FALSE) |
| return; |
| |
| if (status != 1 && status != 5) |
| return; |
| |
| activate_gprs(status); |
| } |
| |
| static void creg_cb(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| int status, lac, ci, tech; |
| |
| if (!ok) |
| return; |
| |
| if (at_util_parse_reg(result, "+CREG:", NULL, &status, |
| &lac, &ci, &tech) == FALSE) |
| return; |
| |
| if (status != 1 && status != 5) { |
| g_at_chat_register(control, "+CREG:", |
| creg_notify, FALSE, NULL, NULL); |
| return; |
| } |
| |
| activate_gprs(status); |
| } |
| |
| static void register_cb(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| if (!ok) { |
| g_print("Couldn't register to network, exiting...\n"); |
| exit(1); |
| } |
| |
| state = STATE_REGISTERING; |
| g_print("Waiting for network registration...\n"); |
| |
| g_at_chat_send(control, "AT+CREG?", creg_prefix, |
| creg_cb, NULL, NULL); |
| } |
| |
| static void start_dial(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| if (!ok) { |
| g_print("Checking PIN status failed\n"); |
| exit(1); |
| } |
| |
| g_at_chat_send(control, "AT+CREG=2", none_prefix, NULL, NULL, NULL); |
| g_at_chat_send(control, "AT+CGREG=2", none_prefix, NULL, NULL, NULL); |
| |
| g_at_chat_send(control, "AT+COPS=0", none_prefix, |
| register_cb, NULL, NULL); |
| } |
| |
| static void check_pin(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| if (!ok) { |
| g_print("Turning on the modem failed\n"); |
| exit(1); |
| } |
| |
| g_at_chat_send(control, "AT+CPIN?", NULL, start_dial, NULL, NULL); |
| } |
| |
| static void check_mode(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| GAtResultIter iter; |
| |
| if (!ok) { |
| g_print("Checking modem mode failed\n"); |
| exit(1); |
| } |
| |
| g_at_result_iter_init(&iter, result); |
| g_at_result_iter_next(&iter, "+CFUN:"); |
| g_at_result_iter_next_number(&iter, &oldmode); |
| |
| g_print("Current modem mode is %d\n", oldmode); |
| |
| if (oldmode == 1) { |
| check_pin(ok, result, user_data); |
| return; |
| } |
| |
| g_at_chat_send(control, "AT+CFUN=1", NULL, check_pin, NULL, NULL); |
| } |
| |
| static int open_serial(void) |
| { |
| GAtSyntax *syntax; |
| GIOChannel *channel; |
| |
| channel = g_at_tty_open(option_control, NULL); |
| if (channel == NULL) |
| return -EIO; |
| |
| syntax = g_at_syntax_new_gsm_permissive(); |
| control = g_at_chat_new(channel, syntax); |
| g_io_channel_unref(channel); |
| g_at_syntax_unref(syntax); |
| |
| if (control == NULL) |
| return -EIO; |
| |
| if (option_modem == NULL) { |
| g_at_chat_ref(control); |
| modem = control; |
| g_at_chat_set_debug(control, gsmdial_debug, ""); |
| } else { |
| g_at_chat_set_debug(control, gsmdial_debug, "Control"); |
| |
| channel = g_at_tty_open(option_modem, NULL); |
| if (channel == NULL) |
| return -EIO; |
| |
| syntax = g_at_syntax_new_gsm_permissive(); |
| modem = g_at_chat_new(channel, syntax); |
| g_io_channel_unref(channel); |
| g_at_syntax_unref(syntax); |
| |
| if (modem == NULL) |
| return -EIO; |
| |
| g_at_chat_set_debug(modem, gsmdial_debug, "Modem"); |
| } |
| |
| return 0; |
| } |
| |
| static int open_ip(void) |
| { |
| int sk, err; |
| struct sockaddr_in addr; |
| GAtSyntax *syntax; |
| GIOChannel *channel; |
| |
| sk = socket(PF_INET, SOCK_STREAM, 0); |
| if (sk < 0) |
| return -EINVAL; |
| |
| memset(&addr, 0, sizeof(addr)); |
| addr.sin_family = AF_INET; |
| addr.sin_addr.s_addr = inet_addr(option_ip); |
| addr.sin_port = htons(option_port); |
| |
| err = connect(sk, (struct sockaddr *) &addr, sizeof(addr)); |
| if (err < 0) { |
| close(sk); |
| return err; |
| } |
| |
| channel = g_io_channel_unix_new(sk); |
| if (channel == NULL) { |
| close(sk); |
| return -ENOMEM; |
| } |
| |
| syntax = g_at_syntax_new_gsmv1(); |
| control = g_at_chat_new(channel, syntax); |
| g_io_channel_unref(channel); |
| g_at_syntax_unref(syntax); |
| |
| if (control == NULL) |
| return -ENOMEM; |
| |
| g_at_chat_ref(control); |
| modem = control; |
| g_at_chat_set_debug(control, gsmdial_debug, ""); |
| |
| return 0; |
| } |
| |
| static GOptionEntry options[] = { |
| { "ip", 'i', 0, G_OPTION_ARG_STRING, &option_ip, |
| "Specify IP" }, |
| { "port", 'p', 0, G_OPTION_ARG_INT, &option_port, |
| "Specify IP Port" }, |
| { "control", 'n', 0, G_OPTION_ARG_FILENAME, &option_control, |
| "Specify Modem Control port" }, |
| { "modem", 'm', 0, G_OPTION_ARG_FILENAME, &option_modem, |
| "Specify Modem port (ppp), if not provided" |
| " the control port will be used" }, |
| { "cid", 'c', 0, G_OPTION_ARG_INT, &option_cid, |
| "Specify CID to use" }, |
| { "apn", 'a', 0, G_OPTION_ARG_STRING, &option_apn, |
| "Specify APN" }, |
| { "offmode", 'o', 0, G_OPTION_ARG_INT, &option_offmode, |
| "Specify CFUN offmode" }, |
| { "legacy", 'l', 0, G_OPTION_ARG_NONE, &option_legacy, |
| "Use ATD*99***<cid>#" }, |
| { "bluetooth", 'b', 0, G_OPTION_ARG_NONE, &option_bluetooth, |
| "Use only ATD*99" }, |
| { "username", 'u', 0, G_OPTION_ARG_STRING, &option_username, |
| "Specify PPP username" }, |
| { "password", 'w', 0, G_OPTION_ARG_STRING, &option_password, |
| "Specify PPP password" }, |
| { "pppdump", 'D', 0, G_OPTION_ARG_STRING, &option_pppdump, |
| "Specify pppdump filename" }, |
| { "pfc", 0, 0, G_OPTION_ARG_NONE, &option_pfc, |
| "Use Protocol Field Compression" }, |
| { "acfc", 0, 0, G_OPTION_ARG_NONE, &option_acfc, |
| "Use Address & Control Field Compression" }, |
| { NULL }, |
| }; |
| |
| int main(int argc, char **argv) |
| { |
| GOptionContext *context; |
| GError *err = NULL; |
| sigset_t mask; |
| int signal_fd; |
| GIOChannel *signal_io; |
| int signal_source; |
| |
| context = g_option_context_new(NULL); |
| g_option_context_add_main_entries(context, options, NULL); |
| |
| if (g_option_context_parse(context, &argc, &argv, &err) == FALSE) { |
| if (err != NULL) { |
| g_printerr("%s\n", err->message); |
| g_error_free(err); |
| return 1; |
| } |
| |
| g_printerr("An unknown error occurred\n"); |
| return 1; |
| } |
| |
| g_option_context_free(context); |
| |
| if (option_control) { |
| int ret; |
| |
| g_print("Control: %s\n", option_control); |
| if (option_modem) |
| g_print("Modem: %s\n", option_modem); |
| |
| ret = open_serial(); |
| g_free(option_control); |
| g_free(option_modem); |
| |
| if (ret < 0) |
| goto out; |
| } else { |
| int ret; |
| |
| g_print("IP: %s\n", option_ip); |
| g_print("Port: %d\n", option_port); |
| ret = open_ip(); |
| g_free(option_ip); |
| |
| if (ret < 0) |
| goto out; |
| } |
| |
| g_print("APN: %s\n", option_apn); |
| g_print("CID: %d\n", option_cid); |
| |
| sigemptyset(&mask); |
| sigaddset(&mask, SIGTERM); |
| sigaddset(&mask, SIGINT); |
| sigaddset(&mask, SIGUSR1); |
| sigaddset(&mask, SIGUSR2); |
| sigaddset(&mask, SIGPIPE); |
| |
| if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0) { |
| perror("Can't set signal mask"); |
| return 1; |
| } |
| |
| signal_fd = signalfd(-1, &mask, 0); |
| if (signal_fd < 0) { |
| perror("Can't create signal filedescriptor"); |
| return 1; |
| } |
| |
| signal_io = g_io_channel_unix_new(signal_fd); |
| g_io_channel_set_close_on_unref(signal_io, TRUE); |
| signal_source = g_io_add_watch(signal_io, |
| G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, |
| signal_cb, GINT_TO_POINTER(signal_fd)); |
| g_io_channel_unref(signal_io); |
| |
| event_loop = g_main_loop_new(NULL, FALSE); |
| |
| if (option_bluetooth) { |
| g_at_chat_send(control, "ATD*99#", none_prefix, connect_cb, |
| NULL, NULL); |
| } else { |
| g_at_chat_send(control, "ATE0Q0V1", NULL, NULL, NULL, NULL); |
| g_at_chat_send(control, "AT+CFUN?", cfun_prefix, |
| check_mode, NULL, NULL); |
| } |
| |
| g_main_loop_run(event_loop); |
| g_source_remove(signal_source); |
| g_main_loop_unref(event_loop); |
| |
| out: |
| if (ppp == NULL) { |
| g_at_chat_unref(control); |
| g_at_chat_unref(modem); |
| } else |
| g_at_ppp_unref(ppp); |
| |
| g_free(option_apn); |
| |
| return 0; |
| } |